Skip to content

Commit f8e9406

Browse files
ameer2468Brendonovichcoderabbitai[bot]
authored
Feature: notifications (#833)
* notifications ui * tweaks * Update DashboardInner.tsx * Add notifications support * clearup icons and add replies to dropdown * various fixes and tweaks * migrations * more cleanup * go back to denormalized + use ts-rest * move to dashboard folder * derive createNotification api from web api contract * Update apps/web/app/(org)/dashboard/_components/Notifications/index.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * cleanup * fix notifications ui * fix frontend again * fix useApiClient * properly extract authorId in sql * fix json param * plz this time * fix notif counts * add border * Update apps/web/utils/web-api.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * remove md for now --------- Co-authored-by: Brendan Allan <brendonovich@outlook.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent a1aa344 commit f8e9406

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+5482
-274
lines changed

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"uuid": "^9.0.1",
6767
"vinxi": "^0.5.6",
6868
"webcodecs": "^0.1.0",
69-
"zod": "^3.24.2"
69+
"zod": "^3.25.76"
7070
},
7171
"devDependencies": {
7272
"@fontsource/geist-sans": "^5.0.3",

apps/discord-bot/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"jose": "^5.10.0",
2323
"octokit": "^4.1.2",
2424
"tweetnacl": "^1.0.3",
25-
"valibot": "1.0.0-rc.1"
25+
"valibot": "1.0.0-rc.1",
26+
"zod": "^3"
2627
}
2728
}

apps/storybook/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"dependencies": {
1010
"@cap/ui-solid": "workspace:*",
1111
"postcss-pseudo-companion-classes": "^0.1.1",
12-
"solid-js": "^1.9.3"
12+
"solid-js": "^1.9.3",
13+
"zod": "^3"
1314
},
1415
"devDependencies": {
1516
"@chromatic-com/storybook": "^1.6.1",

apps/tasks/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"helmet": "^7.1.0",
2828
"morgan": "^1.10.0",
2929
"ts-node": "^10.9.2",
30-
"typescript": "^5.8.3"
30+
"typescript": "^5.8.3",
31+
"zod": "^3"
3132
},
3233
"devDependencies": {
3334
"@typescript-eslint/eslint-plugin": "^7.6.0",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use server";
2+
3+
import { getCurrentUser } from "@cap/database/auth/session";
4+
import { notifications } from "@cap/database/schema";
5+
import { db } from "@cap/database";
6+
import { eq } from "drizzle-orm";
7+
import { revalidatePath } from "next/cache";
8+
9+
export const markAllAsRead = async () => {
10+
const currentUser = await getCurrentUser();
11+
if (!currentUser) {
12+
throw new Error("User not found");
13+
}
14+
15+
try {
16+
await db()
17+
.update(notifications)
18+
.set({ readAt: new Date() })
19+
.where(eq(notifications.recipientId, currentUser.id));
20+
} catch (error) {
21+
console.log(error);
22+
throw new Error("Error marking notifications as read");
23+
}
24+
25+
revalidatePath("/dashboard");
26+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"use server";
2+
3+
import { getCurrentUser } from "@cap/database/auth/session";
4+
import { db } from "@cap/database";
5+
import { users } from "@cap/database/schema";
6+
import { eq } from "drizzle-orm";
7+
import { revalidatePath } from "next/cache";
8+
9+
export const updatePreferences = async ({
10+
notifications,
11+
}: {
12+
notifications: {
13+
pauseComments: boolean;
14+
pauseReplies: boolean;
15+
pauseViews: boolean;
16+
pauseReactions: boolean;
17+
};
18+
}) => {
19+
const currentUser = await getCurrentUser();
20+
if (!currentUser) {
21+
throw new Error("User not found");
22+
}
23+
24+
try {
25+
await db()
26+
.update(users)
27+
.set({
28+
preferences: {
29+
notifications,
30+
},
31+
})
32+
.where(eq(users.id, currentUser.id));
33+
revalidatePath("/dashboard");
34+
} catch (error) {
35+
console.log(error);
36+
throw new Error("Error updating preferences");
37+
}
38+
};
Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
"use server";
22

33
import { db } from "@cap/database";
4-
import { comments } from "@cap/database/schema";
4+
import { comments, notifications } from "@cap/database/schema";
55
import { getCurrentUser } from "@cap/database/auth/session";
66
import { revalidatePath } from "next/cache";
7-
import { eq, and } from "drizzle-orm";
7+
import { eq, and, sql } from "drizzle-orm";
88

99
export async function deleteComment({
1010
commentId,
11+
parentId,
1112
videoId,
1213
}: {
1314
commentId: string;
15+
parentId?: string;
1416
videoId: string;
1517
}) {
1618
const user = await getCurrentUser();
@@ -23,25 +25,60 @@ export async function deleteComment({
2325
throw new Error("Comment ID and video ID are required");
2426
}
2527

26-
// First, verify the comment exists and belongs to the current user
27-
const existingComment = await db()
28-
.select()
29-
.from(comments)
30-
.where(and(eq(comments.id, commentId), eq(comments.authorId, user.id)))
31-
.limit(1);
32-
33-
if (existingComment.length === 0) {
34-
throw new Error(
35-
"Comment not found or you don't have permission to delete it"
36-
);
37-
}
28+
try {
29+
await db().transaction(async (tx) => {
30+
// First, verify the comment exists and belongs to the current user
31+
const [existingComment] = await tx
32+
.select({ id: comments.id })
33+
.from(comments)
34+
.where(and(eq(comments.id, commentId), eq(comments.authorId, user.id)))
35+
.limit(1);
36+
37+
if (!existingComment) {
38+
throw new Error(
39+
"Comment not found or you don't have permission to delete it"
40+
);
41+
}
3842

39-
// Delete the comment
40-
await db()
41-
.delete(comments)
42-
.where(and(eq(comments.id, commentId), eq(comments.authorId, user.id)));
43+
await tx
44+
.delete(comments)
45+
.where(and(eq(comments.id, commentId), eq(comments.authorId, user.id)));
4346

44-
revalidatePath(`/s/${videoId}`);
47+
// Delete related notifications
48+
if (parentId) {
49+
await tx
50+
.delete(notifications)
51+
.where(
52+
and(
53+
eq(notifications.type, "reply"),
54+
sql`JSON_EXTRACT(${notifications.data}, '$.comment.id') = ${commentId}`
55+
)
56+
);
57+
} else {
58+
await tx
59+
.delete(notifications)
60+
.where(
61+
and(
62+
eq(notifications.type, "comment"),
63+
sql`JSON_EXTRACT(${notifications.data}, '$.comment.id') = ${commentId}`
64+
)
65+
);
4566

46-
return { success: true, commentId };
67+
await tx
68+
.delete(notifications)
69+
.where(
70+
and(
71+
eq(notifications.type, "reply"),
72+
sql`JSON_EXTRACT(${notifications.data}, '$.comment.parentCommentId') = ${commentId}`
73+
)
74+
);
75+
}
76+
});
77+
78+
revalidatePath(`/s/${videoId}`);
79+
return { success: true };
80+
} catch (error) {
81+
console.error("Error deleting comment:", error);
82+
throw error;
83+
}
4784
}

apps/web/actions/videos/new-comment.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { comments } from "@cap/database/schema";
55
import { getCurrentUser } from "@cap/database/auth/session";
66
import { revalidatePath } from "next/cache";
77
import { nanoId } from "@cap/database/helpers";
8+
import { createNotification } from "@/lib/Notification";
89

910
export async function newComment(data: {
1011
content: string;
@@ -22,6 +23,11 @@ export async function newComment(data: {
2223
const videoId = data.videoId;
2324
const type = data.type;
2425
const parentCommentId = data.parentCommentId;
26+
const conditionalType = parentCommentId
27+
? "reply"
28+
: type === "emoji"
29+
? "reaction"
30+
: "comment";
2531

2632
if (!content || !videoId) {
2733
throw new Error("Content and videoId are required");
@@ -42,6 +48,17 @@ export async function newComment(data: {
4248

4349
await db().insert(comments).values(newComment);
4450

51+
try {
52+
await createNotification({
53+
type: conditionalType,
54+
videoId,
55+
authorId: user.id,
56+
comment: { id, content },
57+
});
58+
} catch (error) {
59+
console.error("Failed to create notification:", error);
60+
}
61+
4562
// Add author name to the returned data
4663
const commentWithAuthor = {
4764
...newComment,

apps/web/app/(org)/dashboard/Contexts.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { createContext, useContext, useEffect, useState } from "react";
66
import { UpgradeModal } from "@/components/UpgradeModal";
77
import { usePathname } from "next/navigation";
88
import { buildEnv } from "@cap/env";
9-
import { Organization, Spaces } from "./dashboard-data";
9+
import { Organization, Spaces, UserPreferences } from "./dashboard-data";
1010

1111
type SharedContext = {
1212
organizationData: Organization[] | null;
@@ -18,6 +18,8 @@ type SharedContext = {
1818
user: typeof users.$inferSelect;
1919
isSubscribed: boolean;
2020
toggleSidebarCollapsed: () => void;
21+
anyNewNotifications: boolean;
22+
userPreferences: UserPreferences;
2123
sidebarCollapsed: boolean;
2224
upgradeModalOpen: boolean;
2325
setUpgradeModalOpen: (open: boolean) => void;
@@ -32,7 +34,7 @@ const ThemeContext = createContext<{
3234
setThemeHandler: (newTheme: ITheme) => void;
3335
}>({
3436
theme: "light",
35-
setThemeHandler: () => {},
37+
setThemeHandler: () => { },
3638
});
3739

3840
export const useTheme = () => useContext(ThemeContext);
@@ -46,6 +48,8 @@ export function DashboardContexts({
4648
spacesData,
4749
user,
4850
isSubscribed,
51+
userPreferences,
52+
anyNewNotifications,
4953
initialTheme,
5054
initialSidebarCollapsed,
5155
}: {
@@ -55,6 +59,8 @@ export function DashboardContexts({
5559
spacesData: SharedContext["spacesData"];
5660
user: SharedContext["user"];
5761
isSubscribed: SharedContext["isSubscribed"];
62+
userPreferences: SharedContext["userPreferences"];
63+
anyNewNotifications: boolean;
5864
initialTheme: ITheme;
5965
initialSidebarCollapsed: boolean;
6066
}) {
@@ -133,6 +139,8 @@ export function DashboardContexts({
133139
organizationData,
134140
activeOrganization,
135141
spacesData,
142+
anyNewNotifications,
143+
userPreferences,
136144
userSpaces,
137145
sharedSpaces,
138146
activeSpace,

0 commit comments

Comments
 (0)