Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c8e32b0
notifications ui
ameer2468 May 15, 2025
5fdfd50
Merge branch 'main' into notifications
ameer2468 Jun 25, 2025
2ca7eef
tweaks
ameer2468 Jun 25, 2025
e78b8e2
Update DashboardInner.tsx
ameer2468 Jun 25, 2025
adb4d27
Merge branch 'main' into notifications
ameer2468 Jun 27, 2025
bbfa1e1
Merge branch 'main' into notifications
ameer2468 Jul 19, 2025
77cb10e
Merge branch 'main' into notifications
ameer2468 Jul 31, 2025
fcf5676
Add notifications support
ameer2468 Aug 4, 2025
8aa4fa1
clearup icons and add replies to dropdown
ameer2468 Aug 4, 2025
6915a52
various fixes and tweaks
ameer2468 Aug 4, 2025
c195c46
migrations
ameer2468 Aug 4, 2025
42ad420
more cleanup
ameer2468 Aug 4, 2025
3fbd9f6
go back to denormalized + use ts-rest
Brendonovich Aug 8, 2025
e85fed9
move to dashboard folder
Brendonovich Aug 8, 2025
d61fe55
derive createNotification api from web api contract
Brendonovich Aug 8, 2025
670b8f5
Update apps/web/app/(org)/dashboard/_components/Notifications/index.tsx
Brendonovich Aug 8, 2025
2e1dab1
cleanup
Brendonovich Aug 8, 2025
8fb567f
Merge branch 'notifications' of https://github.com/CapSoftware/Cap in…
Brendonovich Aug 8, 2025
f9cdb27
Merge branch 'main' into notifications
Brendonovich Aug 8, 2025
42c8506
fix notifications ui
Brendonovich Aug 8, 2025
023e6e6
fix frontend again
Brendonovich Aug 8, 2025
fc2552f
fix useApiClient
Brendonovich Aug 8, 2025
3244794
properly extract authorId in sql
Brendonovich Aug 8, 2025
d798888
fix json param
Brendonovich Aug 8, 2025
cf27f98
plz this time
Brendonovich Aug 8, 2025
d5acd6f
fix notif counts
Brendonovich Aug 8, 2025
93fc83f
add border
Brendonovich Aug 8, 2025
61a73a5
Update apps/web/utils/web-api.ts
Brendonovich Aug 8, 2025
5b55f28
remove md for now
ameer2468 Aug 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"uuid": "^9.0.1",
"vinxi": "^0.5.6",
"webcodecs": "^0.1.0",
"zod": "^3.24.2"
"zod": "^3.25.76"
},
"devDependencies": {
"@fontsource/geist-sans": "^5.0.3",
Expand Down
3 changes: 2 additions & 1 deletion apps/discord-bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"jose": "^5.10.0",
"octokit": "^4.1.2",
"tweetnacl": "^1.0.3",
"valibot": "1.0.0-rc.1"
"valibot": "1.0.0-rc.1",
"zod": "^3"
}
}
3 changes: 2 additions & 1 deletion apps/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"dependencies": {
"@cap/ui-solid": "workspace:*",
"postcss-pseudo-companion-classes": "^0.1.1",
"solid-js": "^1.9.3"
"solid-js": "^1.9.3",
"zod": "^3"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.6.1",
Expand Down
3 changes: 2 additions & 1 deletion apps/tasks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"zod": "^3"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.6.0",
Expand Down
26 changes: 26 additions & 0 deletions apps/web/actions/notifications/mark-all-as-read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use server";

import { getCurrentUser } from "@cap/database/auth/session";
import { notifications } from "@cap/database/schema";
import { db } from "@cap/database";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export const markAllAsRead = async () => {
const currentUser = await getCurrentUser();
if (!currentUser) {
throw new Error("User not found");
}

try {
await db()
.update(notifications)
.set({ readAt: new Date() })
.where(eq(notifications.recipientId, currentUser.id));
} catch (error) {
console.log(error);
throw new Error("Error marking notifications as read");
}

revalidatePath("/dashboard");
};
38 changes: 38 additions & 0 deletions apps/web/actions/notifications/update-preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use server";

import { getCurrentUser } from "@cap/database/auth/session";
import { db } from "@cap/database";
import { users } from "@cap/database/schema";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export const updatePreferences = async ({
notifications,
}: {
notifications: {
pauseComments: boolean;
pauseReplies: boolean;
pauseViews: boolean;
pauseReactions: boolean;
};
}) => {
const currentUser = await getCurrentUser();
if (!currentUser) {
throw new Error("User not found");
}

try {
await db()
.update(users)
.set({
preferences: {
notifications,
},
})
.where(eq(users.id, currentUser.id));
revalidatePath("/dashboard");
} catch (error) {
console.log(error);
throw new Error("Error updating preferences");
}
};
77 changes: 57 additions & 20 deletions apps/web/actions/videos/delete-comment.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"use server";

import { db } from "@cap/database";
import { comments } from "@cap/database/schema";
import { comments, notifications } from "@cap/database/schema";
import { getCurrentUser } from "@cap/database/auth/session";
import { revalidatePath } from "next/cache";
import { eq, and } from "drizzle-orm";
import { eq, and, sql } from "drizzle-orm";

export async function deleteComment({
commentId,
parentId,
videoId,
}: {
commentId: string;
parentId?: string;
videoId: string;
}) {
const user = await getCurrentUser();
Expand All @@ -23,25 +25,60 @@ export async function deleteComment({
throw new Error("Comment ID and video ID are required");
}

// First, verify the comment exists and belongs to the current user
const existingComment = await db()
.select()
.from(comments)
.where(and(eq(comments.id, commentId), eq(comments.authorId, user.id)))
.limit(1);

if (existingComment.length === 0) {
throw new Error(
"Comment not found or you don't have permission to delete it"
);
}
try {
await db().transaction(async (tx) => {
// First, verify the comment exists and belongs to the current user
const [existingComment] = await tx
.select({ id: comments.id })
.from(comments)
.where(and(eq(comments.id, commentId), eq(comments.authorId, user.id)))
.limit(1);

if (!existingComment) {
throw new Error(
"Comment not found or you don't have permission to delete it"
);
}

// Delete the comment
await db()
.delete(comments)
.where(and(eq(comments.id, commentId), eq(comments.authorId, user.id)));
await tx
.delete(comments)
.where(and(eq(comments.id, commentId), eq(comments.authorId, user.id)));

revalidatePath(`/s/${videoId}`);
// Delete related notifications
if (parentId) {
await tx
.delete(notifications)
.where(
and(
eq(notifications.type, "reply"),
sql`JSON_EXTRACT(${notifications.data}, '$.comment.id') = ${commentId}`
)
);
} else {
await tx
.delete(notifications)
.where(
and(
eq(notifications.type, "comment"),
sql`JSON_EXTRACT(${notifications.data}, '$.comment.id') = ${commentId}`
)
);

return { success: true, commentId };
await tx
.delete(notifications)
.where(
and(
eq(notifications.type, "reply"),
sql`JSON_EXTRACT(${notifications.data}, '$.comment.parentCommentId') = ${commentId}`
)
);
}
});

revalidatePath(`/s/${videoId}`);
return { success: true };
} catch (error) {
console.error("Error deleting comment:", error);
throw error;
}
}
17 changes: 17 additions & 0 deletions apps/web/actions/videos/new-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { comments } from "@cap/database/schema";
import { getCurrentUser } from "@cap/database/auth/session";
import { revalidatePath } from "next/cache";
import { nanoId } from "@cap/database/helpers";
import { createNotification } from "@/lib/Notification";

export async function newComment(data: {
content: string;
Expand All @@ -22,6 +23,11 @@ export async function newComment(data: {
const videoId = data.videoId;
const type = data.type;
const parentCommentId = data.parentCommentId;
const conditionalType = parentCommentId
? "reply"
: type === "emoji"
? "reaction"
: "comment";

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

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

try {
await createNotification({
type: conditionalType,
videoId,
authorId: user.id,
comment: { id, content },
});
} catch (error) {
console.error("Failed to create notification:", error);
}

// Add author name to the returned data
const commentWithAuthor = {
...newComment,
Expand Down
12 changes: 10 additions & 2 deletions apps/web/app/(org)/dashboard/Contexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createContext, useContext, useEffect, useState } from "react";
import { UpgradeModal } from "@/components/UpgradeModal";
import { usePathname } from "next/navigation";
import { buildEnv } from "@cap/env";
import { Organization, Spaces } from "./dashboard-data";
import { Organization, Spaces, UserPreferences } from "./dashboard-data";

type SharedContext = {
organizationData: Organization[] | null;
Expand All @@ -18,6 +18,8 @@ type SharedContext = {
user: typeof users.$inferSelect;
isSubscribed: boolean;
toggleSidebarCollapsed: () => void;
anyNewNotifications: boolean;
userPreferences: UserPreferences;
sidebarCollapsed: boolean;
upgradeModalOpen: boolean;
setUpgradeModalOpen: (open: boolean) => void;
Expand All @@ -32,7 +34,7 @@ const ThemeContext = createContext<{
setThemeHandler: (newTheme: ITheme) => void;
}>({
theme: "light",
setThemeHandler: () => {},
setThemeHandler: () => { },
});

export const useTheme = () => useContext(ThemeContext);
Expand All @@ -46,6 +48,8 @@ export function DashboardContexts({
spacesData,
user,
isSubscribed,
userPreferences,
anyNewNotifications,
initialTheme,
initialSidebarCollapsed,
}: {
Expand All @@ -55,6 +59,8 @@ export function DashboardContexts({
spacesData: SharedContext["spacesData"];
user: SharedContext["user"];
isSubscribed: SharedContext["isSubscribed"];
userPreferences: SharedContext["userPreferences"];
anyNewNotifications: boolean;
initialTheme: ITheme;
initialSidebarCollapsed: boolean;
}) {
Expand Down Expand Up @@ -133,6 +139,8 @@ export function DashboardContexts({
organizationData,
activeOrganization,
spacesData,
anyNewNotifications,
userPreferences,
userSpaces,
sharedSpaces,
activeSpace,
Expand Down
Loading
Loading