feat: Implement notification settings and logic for new video versions.

This commit is contained in:
Yusuf İpek
2026-02-20 13:51:00 +03:00
parent cdd46cf05e
commit fe496b7fa5
5 changed files with 60 additions and 0 deletions
+10
View File
@@ -26,6 +26,7 @@ interface NotificationSettings {
telegramEnabled: boolean; telegramEnabled: boolean;
emailEnabled: boolean; emailEnabled: boolean;
onNewVideo: boolean; onNewVideo: boolean;
onNewVersion: boolean;
onNewComment: boolean; onNewComment: boolean;
onNewReply: boolean; onNewReply: boolean;
timezone: string; timezone: string;
@@ -83,6 +84,7 @@ export default function SettingsPage() {
telegramEnabled: false, telegramEnabled: false,
emailEnabled: false, emailEnabled: false,
onNewVideo: true, onNewVideo: true,
onNewVersion: true,
onNewComment: true, onNewComment: true,
onNewReply: true, onNewReply: true,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
@@ -257,6 +259,14 @@ export default function SettingsPage() {
label="New Video Added" label="New Video Added"
description="When a new video is added to one of your projects" description="When a new video is added to one of your projects"
/> />
<ToggleButton
enabled={settings.onNewVersion}
onToggle={() =>
setSettings((s) => ({ ...s, onNewVersion: !s.onNewVersion }))
}
label="New Version Added"
description="When a new version is added to an existing video"
/>
<ToggleButton <ToggleButton
enabled={settings.onNewComment} enabled={settings.onNewComment}
onToggle={() => onToggle={() =>
@@ -4,6 +4,7 @@ import { auth } from '@/lib/auth';
import { ProjectMemberRole, WorkspaceMemberRole } from '@prisma/client'; import { ProjectMemberRole, WorkspaceMemberRole } from '@prisma/client';
import { validateUrl, validateOptionalUrl } from '@/lib/validation'; import { validateUrl, validateOptionalUrl } from '@/lib/validation';
import { rateLimit } from '@/lib/rate-limit'; import { rateLimit } from '@/lib/rate-limit';
import { notifyProjectOwner } from '@/lib/notifications';
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
type RouteParams = { params: Promise<{ projectId: string; videoId: string }> }; type RouteParams = { params: Promise<{ projectId: string; videoId: string }> };
@@ -145,6 +146,19 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
}); });
}); });
// Notify project owner (fire-and-forget, skip if they added it themselves)
if (video.project.ownerId !== session.user.id) {
const baseUrl = process.env.NEXTAUTH_URL || '';
notifyProjectOwner(video.project.ownerId, {
type: 'new_version',
projectName: video.project.name,
videoTitle: video.title,
versionLabel: version.versionLabel || `Version ${version.versionNumber}`,
addedBy: session.user.name || 'A team member',
url: `${baseUrl}/watch/${video.id}`,
}).catch((err) => console.error('Notification failed:', err));
}
const response = successResponse(version, 201); const response = successResponse(version, 201);
return withCacheControl(response, 'private, no-store'); return withCacheControl(response, 'private, no-store');
} catch (error) { } catch (error) {
+4
View File
@@ -26,6 +26,7 @@ export async function GET() {
telegramEnabled: false, telegramEnabled: false,
emailEnabled: false, emailEnabled: false,
onNewVideo: true, onNewVideo: true,
onNewVersion: true,
onNewComment: true, onNewComment: true,
onNewReply: true, onNewReply: true,
timezone: 'UTC', timezone: 'UTC',
@@ -57,6 +58,7 @@ export async function PUT(request: NextRequest) {
telegramEnabled, telegramEnabled,
emailEnabled, emailEnabled,
onNewVideo, onNewVideo,
onNewVersion,
onNewComment, onNewComment,
onNewReply, onNewReply,
timezone, timezone,
@@ -76,6 +78,7 @@ export async function PUT(request: NextRequest) {
telegramEnabled: !!telegramEnabled, telegramEnabled: !!telegramEnabled,
emailEnabled: !!emailEnabled, emailEnabled: !!emailEnabled,
onNewVideo: onNewVideo ?? true, onNewVideo: onNewVideo ?? true,
onNewVersion: onNewVersion ?? true,
onNewComment: onNewComment ?? true, onNewComment: onNewComment ?? true,
onNewReply: onNewReply ?? true, onNewReply: onNewReply ?? true,
timezone: timezone || 'UTC', timezone: timezone || 'UTC',
@@ -86,6 +89,7 @@ export async function PUT(request: NextRequest) {
telegramEnabled: !!telegramEnabled, telegramEnabled: !!telegramEnabled,
emailEnabled: !!emailEnabled, emailEnabled: !!emailEnabled,
onNewVideo: onNewVideo ?? true, onNewVideo: onNewVideo ?? true,
onNewVersion: onNewVersion ?? true,
onNewComment: onNewComment ?? true, onNewComment: onNewComment ?? true,
onNewReply: onNewReply ?? true, onNewReply: onNewReply ?? true,
timezone: timezone || 'UTC', timezone: timezone || 'UTC',
+31
View File
@@ -95,6 +95,7 @@ async function sendEmail(to: string, subject: string, html: string): Promise<boo
export type NotificationEvent = export type NotificationEvent =
| { type: 'new_video'; projectName: string; videoTitle: string; addedBy: string; url: string } | { type: 'new_video'; projectName: string; videoTitle: string; addedBy: string; url: string }
| { type: 'new_version'; projectName: string; videoTitle: string; versionLabel: string; addedBy: string; url: string }
| { type: 'new_comment'; projectName: string; videoTitle: string; commentAuthor: string; commentText: string; timestamp: string; url: string } | { type: 'new_comment'; projectName: string; videoTitle: string; commentAuthor: string; commentText: string; timestamp: string; url: string }
| { type: 'new_reply'; projectName: string; videoTitle: string; replyAuthor: string; replyText: string; parentAuthor: string; timestamp: string; url: string }; | { type: 'new_reply'; projectName: string; videoTitle: string; replyAuthor: string; replyText: string; parentAuthor: string; timestamp: string; url: string };
@@ -123,6 +124,18 @@ function formatTelegramMessage(event: NotificationEvent, timezone: string): Tele
buttonLabel: 'View Video', buttonLabel: 'View Video',
buttonUrl: event.url, buttonUrl: event.url,
}; };
case 'new_version':
return {
text:
`🎬 New Version Added\n\n` +
`▸ Project: ${event.projectName}\n` +
`▸ Video: ${event.videoTitle}\n` +
`▸ Version: ${event.versionLabel}\n` +
`▸ Added by: ${event.addedBy}\n` +
`${now}`,
buttonLabel: 'View Version',
buttonUrl: event.url,
};
case 'new_comment': case 'new_comment':
return { return {
text: text:
@@ -254,6 +267,23 @@ function formatEmail(event: NotificationEvent, timezone: string): { subject: str
</td></tr> </td></tr>
`), `),
}; };
case 'new_version':
return {
subject: `[OpenFrame] New version of ${event.videoTitle} in ${event.projectName}`,
html: emailTemplate(`
<tr>${emailHeading('&#9654;', 'New Version Added')}</tr>
<tr><td style="padding:20px;">
<table cellpadding="0" cellspacing="0" style="width:100%;margin-bottom:20px;">
${emailRow('Project', escapeHtml(event.projectName), true)}
${emailRow('Video', escapeHtml(event.videoTitle), true)}
${emailRow('Version', escapeHtml(event.versionLabel))}
${emailRow('Added by', escapeHtml(event.addedBy))}
${emailRow('When', now)}
</table>
${emailButton('View Version &#8594;', event.url)}
</td></tr>
`),
};
case 'new_comment': case 'new_comment':
return { return {
subject: `[OpenFrame] New comment on ${event.videoTitle}`, subject: `[OpenFrame] New comment on ${event.videoTitle}`,
@@ -329,6 +359,7 @@ export async function notifyProjectOwner(ownerId: string, event: NotificationEve
const shouldNotify = const shouldNotify =
(event.type === 'new_video' && settings.onNewVideo) || (event.type === 'new_video' && settings.onNewVideo) ||
(event.type === 'new_version' && settings.onNewVersion) ||
(event.type === 'new_comment' && settings.onNewComment) || (event.type === 'new_comment' && settings.onNewComment) ||
(event.type === 'new_reply' && settings.onNewReply); (event.type === 'new_reply' && settings.onNewReply);
+1
View File
@@ -380,6 +380,7 @@ model NotificationSetting {
// Event subscriptions // Event subscriptions
onNewVideo Boolean @default(true) onNewVideo Boolean @default(true)
onNewVersion Boolean @default(true)
onNewComment Boolean @default(true) onNewComment Boolean @default(true)
onNewReply Boolean @default(true) onNewReply Boolean @default(true)