Skip to content

Commit

Permalink
Add mark read button for individual notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
InfiniteStash committed Jan 11, 2025
1 parent 41b82d9 commit d06cb20
Show file tree
Hide file tree
Showing 12 changed files with 336 additions and 22 deletions.
3 changes: 3 additions & 0 deletions frontend/src/graphql/mutations/MarkNotificationRead.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mutation MarkNotificationRead($notification: MarkNotificationReadInput!) {
markNotificationsRead(notification: $notification)
}
15 changes: 15 additions & 0 deletions frontend/src/graphql/mutations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ import {
UpdateNotificationSubscriptionsMutation,
UpdateNotificationSubscriptionsMutationVariables,
MarkNotificationsReadDocument,
MarkNotificationReadDocument,
MarkNotificationReadMutationVariables,
MeQuery,
} from "../types";

Expand Down Expand Up @@ -466,3 +468,16 @@ export const useMarkNotificationsRead = () =>
}
},
});

export const useMarkNotificationRead = (
variables: MarkNotificationReadMutationVariables,
) =>
useMutation(MarkNotificationReadDocument, {
variables,
update(cache, { data }) {
if (data?.markNotificationsRead) {
cache.evict({ fieldName: "queryNotifications" });
cache.evict({ fieldName: "getUnreadNotificationCount" });
}
},
});
66 changes: 66 additions & 0 deletions frontend/src/graphql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,11 @@ export type InviteKey = {
uses?: Maybe<Scalars["Int"]["output"]>;
};

export type MarkNotificationReadInput = {
id: Scalars["ID"]["input"];
type: NotificationEnum;
};

export type Measurements = {
__typename: "Measurements";
band_size?: Maybe<Scalars["Int"]["output"]>;
Expand Down Expand Up @@ -660,6 +665,10 @@ export type MutationImageDestroyArgs = {
input: ImageDestroyInput;
};

export type MutationMarkNotificationsReadArgs = {
notification?: InputMaybe<MarkNotificationReadInput>;
};

export type MutationNewUserArgs = {
input: NewUserInput;
};
Expand Down Expand Up @@ -4739,6 +4748,15 @@ export type GrantInviteMutation = {
grantInvite: number;
};

export type MarkNotificationReadMutationVariables = Exact<{
notification: MarkNotificationReadInput;
}>;

export type MarkNotificationReadMutation = {
__typename: "Mutation";
markNotificationsRead: boolean;
};

export type MarkNotificationsReadMutationVariables = Exact<{
[key: string]: never;
}>;
Expand Down Expand Up @@ -38652,6 +38670,54 @@ export const GrantInviteDocument = {
},
],
} as unknown as DocumentNode<GrantInviteMutation, GrantInviteMutationVariables>;
export const MarkNotificationReadDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "MarkNotificationRead" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: {
kind: "Variable",
name: { kind: "Name", value: "notification" },
},
type: {
kind: "NonNullType",
type: {
kind: "NamedType",
name: { kind: "Name", value: "MarkNotificationReadInput" },
},
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "markNotificationsRead" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "notification" },
value: {
kind: "Variable",
name: { kind: "Name", value: "notification" },
},
},
],
},
],
},
},
],
} as unknown as DocumentNode<
MarkNotificationReadMutation,
MarkNotificationReadMutationVariables
>;
export const MarkNotificationsReadDocument = {
kind: "Document",
definitions: [
Expand Down
93 changes: 84 additions & 9 deletions frontend/src/pages/notifications/Notification.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from "react";
import { Button } from "react-bootstrap";
import { Link } from "react-router-dom";
import { faEnvelope, faEnvelopeOpen } from "@fortawesome/free-solid-svg-icons";
import { Icon } from "src/components/fragments";
import { editHref } from "src/utils";
import { useMarkNotificationRead, NotificationEnum } from "src/graphql";
import {
NotificationType,
isSceneNotification,
Expand All @@ -10,13 +14,71 @@ import {
import { CommentNotification } from "./CommentNotification";
import { SceneNotification } from "./sceneNotification";
import { EditNotification } from "./EditNotification";
import { editHref } from "src/utils";
import { Link } from "react-router-dom";

interface Props {
notification: NotificationType;
}

const createMarkNotificationReadInput = (notification: NotificationType) => {
switch (notification.data.__typename) {
case "CommentOwnEdit":
return {
type: NotificationEnum.COMMENT_OWN_EDIT,
id: notification.data.comment.id,
};
case "CommentCommentedEdit":
return {
type: NotificationEnum.COMMENT_COMMENTED_EDIT,
id: notification.data.comment.id,
};
case "CommentVotedEdit":
return {
type: NotificationEnum.COMMENT_VOTED_EDIT,
id: notification.data.comment.id,
};
case "DownvoteOwnEdit":
return {
type: NotificationEnum.DOWNVOTE_OWN_EDIT,
id: notification.data.edit.id,
};
case "FailedOwnEdit":
return {
type: NotificationEnum.FAILED_OWN_EDIT,
id: notification.data.edit.id,
};
case "FavoritePerformerEdit":
return {
type: NotificationEnum.FAVORITE_PERFORMER_EDIT,
id: notification.data.edit.id,
};
case "FavoriteStudioEdit":
return {
type: NotificationEnum.FAVORITE_STUDIO_EDIT,
id: notification.data.edit.id,
};
case "FingerprintedSceneEdit":
return {
type: NotificationEnum.FINGERPRINTED_SCENE_EDIT,
id: notification.data.edit.id,
};
case "UpdatedEdit":
return {
type: NotificationEnum.UPDATED_EDIT,
id: notification.data.edit.id,
};
case "FavoritePerformerScene":
return {
type: NotificationEnum.FAVORITE_PERFORMER_SCENE,
id: notification.data.scene.id,
};
case "FavoriteStudioScene":
return {
type: NotificationEnum.FAVORITE_STUDIO_SCENE,
id: notification.data.scene.id,
};
}
};

const NotificationBody = ({
notification,
}: {
Expand All @@ -35,6 +97,10 @@ const NotificationHeader = ({
}: {
notification: NotificationType;
}) => {
const [markRead, { loading }] = useMarkNotificationRead({
notification: createMarkNotificationReadInput(notification),
});

const headerText = () => {
if (isCommentNotification(notification)) {
const editLink = (
Expand Down Expand Up @@ -97,20 +163,29 @@ const NotificationHeader = ({
};

return (
<h5>
<Icon
icon={notification.read ? faEnvelopeOpen : faEnvelope}
variant={!notification.read ? "warning" : undefined}
className="me-2"
/>
<h5 className="d-flex gap-2">
<div className="Notification-read-state">
{notification.read && <Icon icon={faEnvelopeOpen} />}
{!notification.read && (
<Button
variant="link"
onClick={() => markRead()}
title="Mark notification as read"
disabled={loading}
>
<Icon icon={faEnvelope} variant={"warning"} />
<Icon icon={faEnvelopeOpen} />
</Button>
)}
</div>
{headerText()}
</h5>
);
};

export const Notification: React.FC<Props> = ({ notification }) => {
return (
<div className="notification">
<div className="Notification">
<NotificationHeader notification={notification} />
<NotificationBody notification={notification} />
</div>
Expand Down
39 changes: 38 additions & 1 deletion frontend/src/pages/notifications/styles.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
.notification {
.Notification {
.SceneCard {
width: 300px;
}

&-read-state {
width: 20px;
height: 20px;
display: flex;
align-self: center;
align-items: center;
justify-content: center;

.btn {
border: none;
border-bottom: 2px solid transparent;
border-radius: 0;
padding: 4px 0;

.fa-envelope {
display: block;
}

.fa-envelope-open {
display: none;
}

&:hover {
border-color: white;

.fa-envelope {
display: none;
}

.fa-envelope-open {
display: block;
color: white !important;
}
}
}
}
}
2 changes: 1 addition & 1 deletion graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ type Mutation {
favoriteStudio(id: ID!, favorite: Boolean!): Boolean! @hasRole(role: READ)

"""Mark all of the current users notifications as read."""
markNotificationsRead: Boolean! @hasRole(role: READ)
markNotificationsRead(notification: MarkNotificationReadInput): Boolean! @hasRole(role: READ)
"""Update notification subscriptions for current user."""
updateNotificationSubscriptions(subscriptions: [NotificationEnum!]!): Boolean! @hasRole(role: EDIT)
}
Expand Down
5 changes: 5 additions & 0 deletions graphql/schema/types/notifications.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,8 @@ type QueryNotificationsResult {
count: Int!
notifications: [Notification!]!
}

input MarkNotificationReadInput {
type: NotificationEnum!
id: ID!
}
9 changes: 7 additions & 2 deletions pkg/api/resolver_mutation_notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ import (
"github.com/stashapp/stash-box/pkg/models"
)

func (r *mutationResolver) MarkNotificationsRead(ctx context.Context) (bool, error) {
func (r *mutationResolver) MarkNotificationsRead(ctx context.Context, notification *models.MarkNotificationReadInput) (bool, error) {
user := getCurrentUser(ctx)
fac := r.getRepoFactory(ctx)
err := fac.WithTxn(func() error {
qb := fac.Notification()
return qb.MarkRead(user.ID)

if notification == nil {
return qb.MarkAllRead(user.ID)
} else {

Check failure on line 17 in pkg/api/resolver_mutation_notifications.go

View workflow job for this annotation

GitHub Actions / lint

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)
return qb.MarkRead(user.ID, notification.Type, notification.ID)
}
})
return err == nil, err
}
Expand Down
Loading

0 comments on commit d06cb20

Please sign in to comment.