mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat: Implement notification settings and logic for new video versions.
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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('▶', '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 →', 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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user