@agent-native/core 0.140.0 → 0.141.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-stay-complete-and-atomic.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +0 -2
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +11 -6
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +8 -18
- package/corpus/templates/analytics/server/plugins/db.ts +174 -0
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +38 -38
- package/corpus/templates/calendar/changelog/2026-08-05-out-of-office-blocks-now-open-from-their-scheduled-time-rang.md +6 -0
- package/corpus/templates/calendar/server/lib/booking-emails.ts +70 -154
- package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +28 -64
- package/corpus/templates/clips/actions/get-recording-player-data.ts +1 -0
- package/corpus/templates/clips/actions/invite-member.ts +16 -39
- package/corpus/templates/clips/app/components/player/comments-panel.tsx +2 -3
- package/corpus/templates/clips/app/components/player/reaction-emojis.ts +10 -0
- package/corpus/templates/clips/app/components/player/reactions-tray.tsx +2 -2
- package/corpus/templates/clips/app/components/player/video-player.tsx +20 -6
- package/corpus/templates/clips/app/routes/embed.$shareId.tsx +33 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +5 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +27 -1
- package/corpus/templates/clips/changelog/2026-08-05-comment-and-video-reaction-pickers-now-include-eyes-and-mind.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-fresh-recordings-now-open-with-the-complete-video.md +6 -0
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +0 -24
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +26 -57
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +1 -44
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1 -265
- package/corpus/templates/design/actions/create-design-system.ts +10 -9
- package/corpus/templates/design/actions/get-design-system.ts +0 -53
- package/corpus/templates/design/actions/import-design-tokens.ts +71 -5
- package/corpus/templates/design/actions/index-design-tokens.ts +80 -61
- package/corpus/templates/design/actions/update-design-system.ts +10 -7
- package/corpus/templates/design/app/components/design/EditPanel.tsx +2 -45
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +0 -12
- package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +6 -10
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +3 -19
- package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +10 -36
- package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +0 -15
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +1 -19
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +0 -6
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +23 -53
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +4 -142
- package/corpus/templates/design/app/components/design/types.ts +0 -6
- package/corpus/templates/design/app/i18n-data.ts +0 -20
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +1 -1
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -78
- package/corpus/templates/design/app/pages/design-editor/html-layer-positioning.ts +1 -6
- package/corpus/templates/design/e2e/helpers.ts +0 -17
- package/corpus/templates/design/shared/resolve-tweaks.ts +18 -10
- package/corpus/templates/forms/server/lib/response-email.ts +0 -2
- package/corpus/templates/plan/actions/request-plan-access.ts +12 -33
- package/corpus/templates/plan/server/lib/comment-notifications.ts +27 -53
- package/corpus/templates/slides/server/lib/comment-notifications.ts +21 -44
- package/dist/a2a/handlers.js +9 -10
- package/dist/brand-kit/index.d.ts +0 -1
- package/dist/brand-kit/index.js +0 -1
- package/dist/brand-kit/types.d.ts +0 -25
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.js +8 -8
- package/dist/db/migrations.js +39 -19
- package/dist/localization/default-messages.d.ts +0 -45
- package/dist/localization/default-messages.js +0 -45
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/handlers.js +1 -8
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-discovery.js +0 -22
- package/dist/server/auth.js +3 -1
- package/dist/server/better-auth-instance.js +2 -17
- package/dist/server/email.d.ts +0 -9
- package/dist/server/email.js +3 -44
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.js +0 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +3 -2
- package/docs/content/authentication.mdx +25 -19
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +3 -2
- package/corpus/templates/analytics/server/lib/emails.ts +0 -51
- package/corpus/templates/analytics/server/plugins/transactional-emails.ts +0 -5
- package/corpus/templates/calendar/server/lib/emails.ts +0 -167
- package/corpus/templates/calendar/server/plugins/transactional-emails.ts +0 -5
- package/corpus/templates/clips/server/lib/emails.ts +0 -256
- package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +0 -5
- package/corpus/templates/content/server/lib/emails.ts +0 -70
- package/corpus/templates/content/server/plugins/transactional-emails.ts +0 -5
- package/corpus/templates/design/app/components/design/edit-panel/design-system-swatches.ts +0 -215
- package/corpus/templates/design/changelog/2026-08-04-design-system-tokens-show-by-name-in-the-editor.md +0 -6
- package/corpus/templates/design/server/lib/design-system-tokens.ts +0 -35
- package/corpus/templates/design/shared/authored-color-styles.ts +0 -401
- package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +0 -4
- package/corpus/templates/dispatch/app/routes/transactional-email.tsx +0 -4
- package/corpus/templates/forms/server/lib/emails.ts +0 -52
- package/corpus/templates/forms/server/plugins/transactional-emails.ts +0 -5
- package/corpus/templates/plan/server/lib/emails.ts +0 -67
- package/corpus/templates/plan/server/plugins/transactional-emails.ts +0 -5
- package/corpus/templates/slides/server/lib/emails.ts +0 -40
- package/corpus/templates/slides/server/plugins/transactional-emails.ts +0 -5
- package/dist/brand-kit/tokens.d.ts +0 -92
- package/dist/brand-kit/tokens.js +0 -280
- package/dist/email-catalog/actions/list-email-activity.d.ts +0 -6
- package/dist/email-catalog/actions/list-email-activity.js +0 -15
- package/dist/email-catalog/actions/list-email-engagement.d.ts +0 -6
- package/dist/email-catalog/actions/list-email-engagement.js +0 -15
- package/dist/email-catalog/actions/list-email-log.d.ts +0 -8
- package/dist/email-catalog/actions/list-email-log.js +0 -22
- package/dist/email-catalog/actions/list-transactional-emails.d.ts +0 -23
- package/dist/email-catalog/actions/list-transactional-emails.js +0 -61
- package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +0 -10
- package/dist/email-catalog/actions/render-transactional-email-preview.js +0 -24
- package/dist/email-catalog/authorize.d.ts +0 -2
- package/dist/email-catalog/authorize.js +0 -10
- package/dist/email-catalog/log.d.ts +0 -57
- package/dist/email-catalog/log.js +0 -117
- package/dist/email-catalog/provider-metrics.d.ts +0 -34
- package/dist/email-catalog/provider-metrics.js +0 -165
- package/dist/email-catalog/registry.d.ts +0 -79
- package/dist/email-catalog/registry.js +0 -71
- package/dist/email-catalog/schema.d.ts +0 -212
- package/dist/email-catalog/schema.js +0 -49
- package/dist/email-catalog/system-emails.d.ts +0 -14
- package/dist/email-catalog/system-emails.js +0 -68
|
@@ -11,12 +11,6 @@ import {
|
|
|
11
11
|
DEFAULT_BOOKING_TIMEZONE,
|
|
12
12
|
safeBookingTimeZone,
|
|
13
13
|
} from "./booking-timezone.js";
|
|
14
|
-
import {
|
|
15
|
-
CALENDAR_BOOKING_CANCELLED_EMAIL_ID,
|
|
16
|
-
CALENDAR_BOOKING_CANCELLED_HOST_EMAIL_ID,
|
|
17
|
-
CALENDAR_BOOKING_CONFIRMED_EMAIL_ID,
|
|
18
|
-
CALENDAR_BOOKING_RECEIVED_EMAIL_ID,
|
|
19
|
-
} from "./emails.js";
|
|
20
14
|
|
|
21
15
|
function stripCrlf(value: string | undefined): string {
|
|
22
16
|
return (value ?? "").replace(/[\r\n]+/g, " ").trim();
|
|
@@ -63,128 +57,6 @@ async function sendBestEffort(
|
|
|
63
57
|
}
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
export function renderBookingConfirmedEmail({
|
|
67
|
-
title,
|
|
68
|
-
when,
|
|
69
|
-
host,
|
|
70
|
-
manageUrl,
|
|
71
|
-
meetingLink,
|
|
72
|
-
}: {
|
|
73
|
-
title: string;
|
|
74
|
-
when: string;
|
|
75
|
-
host: string;
|
|
76
|
-
manageUrl: string;
|
|
77
|
-
meetingLink?: string | null;
|
|
78
|
-
}) {
|
|
79
|
-
const paragraphs = [
|
|
80
|
-
`You're booked for ${emailStrong(title)} with ${emailStrong(host)}.`,
|
|
81
|
-
`Time: ${emailStrong(when)}.`,
|
|
82
|
-
];
|
|
83
|
-
if (meetingLink) {
|
|
84
|
-
paragraphs.push(`Meeting link: ${emailLink("Join meeting", meetingLink)}.`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
subject: `Confirmed: ${title}`,
|
|
89
|
-
...renderEmail({
|
|
90
|
-
preheader: `You're booked for ${title} on ${when}.`,
|
|
91
|
-
heading: "Your meeting is booked",
|
|
92
|
-
paragraphs,
|
|
93
|
-
cta: { label: "Manage booking", url: manageUrl },
|
|
94
|
-
footer:
|
|
95
|
-
"Use the manage link if you need to cancel or reschedule this meeting.",
|
|
96
|
-
}),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function renderBookingReceivedEmail({
|
|
101
|
-
title,
|
|
102
|
-
when,
|
|
103
|
-
attendeeName,
|
|
104
|
-
attendee,
|
|
105
|
-
manageUrl,
|
|
106
|
-
meetingLink,
|
|
107
|
-
}: {
|
|
108
|
-
title: string;
|
|
109
|
-
when: string;
|
|
110
|
-
attendeeName: string;
|
|
111
|
-
attendee: string;
|
|
112
|
-
manageUrl: string;
|
|
113
|
-
meetingLink?: string | null;
|
|
114
|
-
}) {
|
|
115
|
-
return {
|
|
116
|
-
subject: `New booking: ${title}`,
|
|
117
|
-
...renderEmail({
|
|
118
|
-
preheader: `${attendeeName} booked ${title} on ${when}.`,
|
|
119
|
-
heading: "New booking",
|
|
120
|
-
paragraphs: [
|
|
121
|
-
`${emailStrong(attendeeName)} booked ${emailStrong(title)}.`,
|
|
122
|
-
`Time: ${emailStrong(when)}.`,
|
|
123
|
-
`Guest: ${emailStrong(attendee)}.`,
|
|
124
|
-
...(meetingLink
|
|
125
|
-
? [`Meeting link: ${emailLink("Join meeting", meetingLink)}.`]
|
|
126
|
-
: []),
|
|
127
|
-
],
|
|
128
|
-
cta: { label: "View booking", url: manageUrl },
|
|
129
|
-
footer: "This booking was created from your calendar booking link.",
|
|
130
|
-
}),
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function renderBookingCancelledEmail({
|
|
135
|
-
title,
|
|
136
|
-
when,
|
|
137
|
-
host,
|
|
138
|
-
bookAgainUrl,
|
|
139
|
-
}: {
|
|
140
|
-
title: string;
|
|
141
|
-
when: string;
|
|
142
|
-
host: string;
|
|
143
|
-
bookAgainUrl?: string;
|
|
144
|
-
}) {
|
|
145
|
-
return {
|
|
146
|
-
subject: `Cancelled: ${title}`,
|
|
147
|
-
...renderEmail({
|
|
148
|
-
preheader: `${title} on ${when} was cancelled.`,
|
|
149
|
-
heading: "Your meeting was cancelled",
|
|
150
|
-
paragraphs: [
|
|
151
|
-
`${emailStrong(title)} with ${emailStrong(host || "the host")} has been cancelled.`,
|
|
152
|
-
`Original time: ${emailStrong(when)}.`,
|
|
153
|
-
],
|
|
154
|
-
cta: bookAgainUrl
|
|
155
|
-
? { label: "Book another time", url: bookAgainUrl }
|
|
156
|
-
: undefined,
|
|
157
|
-
footer: "If this was unexpected, contact the meeting host.",
|
|
158
|
-
}),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function renderBookingCancelledHostEmail({
|
|
163
|
-
title,
|
|
164
|
-
when,
|
|
165
|
-
attendeeName,
|
|
166
|
-
attendee,
|
|
167
|
-
}: {
|
|
168
|
-
title: string;
|
|
169
|
-
when: string;
|
|
170
|
-
attendeeName: string;
|
|
171
|
-
attendee: string;
|
|
172
|
-
}) {
|
|
173
|
-
return {
|
|
174
|
-
subject: `Cancelled booking: ${title}`,
|
|
175
|
-
...renderEmail({
|
|
176
|
-
preheader: `${attendeeName}'s booking for ${title} was cancelled.`,
|
|
177
|
-
heading: "Booking cancelled",
|
|
178
|
-
paragraphs: [
|
|
179
|
-
`${emailStrong(attendeeName)}'s booking for ${emailStrong(title)} was cancelled.`,
|
|
180
|
-
`Original time: ${emailStrong(when)}.`,
|
|
181
|
-
`Guest: ${emailStrong(attendee)}.`,
|
|
182
|
-
],
|
|
183
|
-
footer: "No further action is needed.",
|
|
184
|
-
}),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
60
|
export async function sendBookingConfirmationEmails({
|
|
189
61
|
booking,
|
|
190
62
|
hostEmail,
|
|
@@ -202,31 +74,54 @@ export async function sendBookingConfirmationEmails({
|
|
|
202
74
|
const attendee = stripCrlf(booking.email);
|
|
203
75
|
const attendeeName = stripCrlf(booking.name) || "there";
|
|
204
76
|
|
|
77
|
+
const attendeeParagraphs = [
|
|
78
|
+
`You're booked for ${emailStrong(title)} with ${emailStrong(host)}.`,
|
|
79
|
+
`Time: ${emailStrong(when)}.`,
|
|
80
|
+
];
|
|
81
|
+
if (booking.meetingLink) {
|
|
82
|
+
attendeeParagraphs.push(
|
|
83
|
+
`Meeting link: ${emailLink("Join meeting", booking.meetingLink)}.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const attendeeEmail = renderEmail({
|
|
88
|
+
preheader: `You're booked for ${title} on ${when}.`,
|
|
89
|
+
heading: "Your meeting is booked",
|
|
90
|
+
paragraphs: attendeeParagraphs,
|
|
91
|
+
cta: { label: "Manage booking", url: manageUrl },
|
|
92
|
+
footer:
|
|
93
|
+
"Use the manage link if you need to cancel or reschedule this meeting.",
|
|
94
|
+
});
|
|
95
|
+
|
|
205
96
|
await sendBestEffort("attendee confirmation", {
|
|
206
97
|
to: attendee,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
host,
|
|
211
|
-
manageUrl,
|
|
212
|
-
meetingLink: booking.meetingLink,
|
|
213
|
-
}),
|
|
98
|
+
subject: `Confirmed: ${title}`,
|
|
99
|
+
html: attendeeEmail.html,
|
|
100
|
+
text: attendeeEmail.text,
|
|
214
101
|
replyTo: host,
|
|
215
|
-
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const hostEmailMessage = renderEmail({
|
|
105
|
+
preheader: `${attendeeName} booked ${title} on ${when}.`,
|
|
106
|
+
heading: "New booking",
|
|
107
|
+
paragraphs: [
|
|
108
|
+
`${emailStrong(attendeeName)} booked ${emailStrong(title)}.`,
|
|
109
|
+
`Time: ${emailStrong(when)}.`,
|
|
110
|
+
`Guest: ${emailStrong(attendee)}.`,
|
|
111
|
+
...(booking.meetingLink
|
|
112
|
+
? [`Meeting link: ${emailLink("Join meeting", booking.meetingLink)}.`]
|
|
113
|
+
: []),
|
|
114
|
+
],
|
|
115
|
+
cta: { label: "View booking", url: manageUrl },
|
|
116
|
+
footer: "This booking was created from your calendar booking link.",
|
|
216
117
|
});
|
|
217
118
|
|
|
218
119
|
await sendBestEffort("host notification", {
|
|
219
120
|
to: host,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
attendeeName,
|
|
224
|
-
attendee,
|
|
225
|
-
manageUrl,
|
|
226
|
-
meetingLink: booking.meetingLink,
|
|
227
|
-
}),
|
|
121
|
+
subject: `New booking: ${title}`,
|
|
122
|
+
html: hostEmailMessage.html,
|
|
123
|
+
text: hostEmailMessage.text,
|
|
228
124
|
replyTo: attendee,
|
|
229
|
-
templateId: CALENDAR_BOOKING_RECEIVED_EMAIL_ID,
|
|
230
125
|
});
|
|
231
126
|
}
|
|
232
127
|
|
|
@@ -247,24 +142,45 @@ export async function sendBookingCancellationEmails({
|
|
|
247
142
|
const attendee = stripCrlf(booking.email);
|
|
248
143
|
const attendeeName = stripCrlf(booking.name) || "The guest";
|
|
249
144
|
|
|
145
|
+
const attendeeEmail = renderEmail({
|
|
146
|
+
preheader: `${title} on ${when} was cancelled.`,
|
|
147
|
+
heading: "Your meeting was cancelled",
|
|
148
|
+
paragraphs: [
|
|
149
|
+
`${emailStrong(title)} with ${emailStrong(host || "the host")} has been cancelled.`,
|
|
150
|
+
`Original time: ${emailStrong(when)}.`,
|
|
151
|
+
],
|
|
152
|
+
cta: bookAgainUrl
|
|
153
|
+
? { label: "Book another time", url: bookAgainUrl }
|
|
154
|
+
: undefined,
|
|
155
|
+
footer: "If this was unexpected, contact the meeting host.",
|
|
156
|
+
});
|
|
157
|
+
|
|
250
158
|
await sendBestEffort("attendee cancellation", {
|
|
251
159
|
to: attendee,
|
|
252
|
-
|
|
160
|
+
subject: `Cancelled: ${title}`,
|
|
161
|
+
html: attendeeEmail.html,
|
|
162
|
+
text: attendeeEmail.text,
|
|
253
163
|
replyTo: host || undefined,
|
|
254
|
-
templateId: CALENDAR_BOOKING_CANCELLED_EMAIL_ID,
|
|
255
164
|
});
|
|
256
165
|
|
|
257
166
|
if (!host) return;
|
|
258
167
|
|
|
168
|
+
const hostEmailMessage = renderEmail({
|
|
169
|
+
preheader: `${attendeeName}'s booking for ${title} was cancelled.`,
|
|
170
|
+
heading: "Booking cancelled",
|
|
171
|
+
paragraphs: [
|
|
172
|
+
`${emailStrong(attendeeName)}'s booking for ${emailStrong(title)} was cancelled.`,
|
|
173
|
+
`Original time: ${emailStrong(when)}.`,
|
|
174
|
+
`Guest: ${emailStrong(attendee)}.`,
|
|
175
|
+
],
|
|
176
|
+
footer: "No further action is needed.",
|
|
177
|
+
});
|
|
178
|
+
|
|
259
179
|
await sendBestEffort("host cancellation notification", {
|
|
260
180
|
to: host,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
attendeeName,
|
|
265
|
-
attendee,
|
|
266
|
-
}),
|
|
181
|
+
subject: `Cancelled booking: ${title}`,
|
|
182
|
+
html: hostEmailMessage.html,
|
|
183
|
+
text: hostEmailMessage.text,
|
|
267
184
|
replyTo: attendee,
|
|
268
|
-
templateId: CALENDAR_BOOKING_CANCELLED_HOST_EMAIL_ID,
|
|
269
185
|
});
|
|
270
186
|
}
|
|
@@ -6,10 +6,6 @@ import {
|
|
|
6
6
|
} from "@agent-native/core/server";
|
|
7
7
|
|
|
8
8
|
import type { CalendarEvent, DeleteEventScope } from "../../shared/api.js";
|
|
9
|
-
import {
|
|
10
|
-
CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID,
|
|
11
|
-
CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID,
|
|
12
|
-
} from "./emails.js";
|
|
13
9
|
|
|
14
10
|
export interface GuestNotificationResult {
|
|
15
11
|
requested: boolean;
|
|
@@ -100,53 +96,6 @@ function scopeLabel(scope: DeleteEventScope | undefined): string | undefined {
|
|
|
100
96
|
return "this and following events";
|
|
101
97
|
}
|
|
102
98
|
|
|
103
|
-
export function renderEventGuestNote({
|
|
104
|
-
title,
|
|
105
|
-
organizer,
|
|
106
|
-
message,
|
|
107
|
-
when,
|
|
108
|
-
kind,
|
|
109
|
-
appliesTo,
|
|
110
|
-
htmlLink,
|
|
111
|
-
}: {
|
|
112
|
-
title: string;
|
|
113
|
-
organizer: string;
|
|
114
|
-
message: string;
|
|
115
|
-
when: string;
|
|
116
|
-
kind: GuestNotificationKind;
|
|
117
|
-
appliesTo?: string;
|
|
118
|
-
htmlLink?: string | null;
|
|
119
|
-
}) {
|
|
120
|
-
const heading =
|
|
121
|
-
kind === "cancellation" ? "Event cancellation note" : "Event update note";
|
|
122
|
-
const subjectPrefix =
|
|
123
|
-
kind === "cancellation" ? "Cancellation note" : "Update note";
|
|
124
|
-
const eventAction = kind === "cancellation" ? "cancelling" : "updating";
|
|
125
|
-
const paragraphs = [
|
|
126
|
-
`${emailStrong(organizer)} added this note while ${eventAction} ${emailStrong(title)}.`,
|
|
127
|
-
messageParagraph(message),
|
|
128
|
-
`When: ${emailStrong(when)}.`,
|
|
129
|
-
];
|
|
130
|
-
if (appliesTo) {
|
|
131
|
-
paragraphs.push(`Applies to: ${emailStrong(appliesTo)}.`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
subject: `${subjectPrefix}: ${title}`,
|
|
136
|
-
...renderEmail({
|
|
137
|
-
preheader: `${subjectPrefix}: ${title}`,
|
|
138
|
-
heading,
|
|
139
|
-
paragraphs,
|
|
140
|
-
cta:
|
|
141
|
-
kind === "update" && htmlLink
|
|
142
|
-
? { label: "Open in Google Calendar", url: htmlLink }
|
|
143
|
-
: undefined,
|
|
144
|
-
footer:
|
|
145
|
-
"Google Calendar sends the calendar update separately. This message carries the organizer note.",
|
|
146
|
-
}),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
99
|
export async function sendEventGuestNotificationNote({
|
|
151
100
|
event,
|
|
152
101
|
organizerEmail,
|
|
@@ -189,14 +138,33 @@ export async function sendEventGuestNotificationNote({
|
|
|
189
138
|
};
|
|
190
139
|
}
|
|
191
140
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
kind
|
|
198
|
-
|
|
199
|
-
|
|
141
|
+
const title = stripCrlf(event.title) || "Calendar event";
|
|
142
|
+
const organizer = stripCrlf(event.organizer?.displayName) || organizerEmail;
|
|
143
|
+
const heading =
|
|
144
|
+
kind === "cancellation" ? "Event cancellation note" : "Event update note";
|
|
145
|
+
const subjectPrefix =
|
|
146
|
+
kind === "cancellation" ? "Cancellation note" : "Update note";
|
|
147
|
+
const eventAction = kind === "cancellation" ? "cancelling" : "updating";
|
|
148
|
+
const paragraphs = [
|
|
149
|
+
`${emailStrong(organizer)} added this note while ${eventAction} ${emailStrong(title)}.`,
|
|
150
|
+
messageParagraph(normalizedMessage),
|
|
151
|
+
`When: ${emailStrong(formatWhen(event))}.`,
|
|
152
|
+
];
|
|
153
|
+
const appliesTo = scopeLabel(scope);
|
|
154
|
+
if (appliesTo) {
|
|
155
|
+
paragraphs.push(`Applies to: ${emailStrong(appliesTo)}.`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rendered = renderEmail({
|
|
159
|
+
preheader: `${subjectPrefix}: ${title}`,
|
|
160
|
+
heading,
|
|
161
|
+
paragraphs,
|
|
162
|
+
cta:
|
|
163
|
+
kind === "update" && event.htmlLink
|
|
164
|
+
? { label: "Open in Google Calendar", url: event.htmlLink }
|
|
165
|
+
: undefined,
|
|
166
|
+
footer:
|
|
167
|
+
"Google Calendar sends the calendar update separately. This message carries the organizer note.",
|
|
200
168
|
});
|
|
201
169
|
|
|
202
170
|
let sentCount = 0;
|
|
@@ -206,14 +174,10 @@ export async function sendEventGuestNotificationNote({
|
|
|
206
174
|
try {
|
|
207
175
|
await sendEmail({
|
|
208
176
|
to,
|
|
209
|
-
subject:
|
|
177
|
+
subject: `${subjectPrefix}: ${title}`,
|
|
210
178
|
html: rendered.html,
|
|
211
179
|
text: rendered.text,
|
|
212
180
|
replyTo: organizerEmail,
|
|
213
|
-
templateId:
|
|
214
|
-
kind === "cancellation"
|
|
215
|
-
? CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID
|
|
216
|
-
: CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID,
|
|
217
181
|
});
|
|
218
182
|
sentCount += 1;
|
|
219
183
|
} catch (error) {
|
|
@@ -326,6 +326,7 @@ export default defineAction({
|
|
|
326
326
|
editsJson: rec.editsJson,
|
|
327
327
|
videoUrl: resolvedVideoUrl,
|
|
328
328
|
videoFormat: rec.videoFormat,
|
|
329
|
+
videoSizeBytes: rec.videoSizeBytes ?? null,
|
|
329
330
|
width: rec.width,
|
|
330
331
|
height: rec.height,
|
|
331
332
|
hasAudio: Boolean(rec.hasAudio),
|
|
@@ -24,7 +24,6 @@ import { and, eq, sql } from "drizzle-orm";
|
|
|
24
24
|
import { z } from "zod";
|
|
25
25
|
|
|
26
26
|
import { getDb } from "../server/db/index.js";
|
|
27
|
-
import { CLIPS_ORGANIZATION_INVITE_EMAIL_ID } from "../server/lib/emails.js";
|
|
28
27
|
import {
|
|
29
28
|
getCurrentOwnerEmail,
|
|
30
29
|
nanoid,
|
|
@@ -68,36 +67,6 @@ async function fetchOrgName(orgId: string): Promise<string> {
|
|
|
68
67
|
return row?.name ?? "Organization";
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
export function renderClipsInviteEmail({
|
|
72
|
-
appName,
|
|
73
|
-
orgName,
|
|
74
|
-
inviter,
|
|
75
|
-
role,
|
|
76
|
-
inviteUrl,
|
|
77
|
-
}: {
|
|
78
|
-
appName: string;
|
|
79
|
-
orgName: string;
|
|
80
|
-
inviter: string;
|
|
81
|
-
role: "admin" | "member";
|
|
82
|
-
inviteUrl: string;
|
|
83
|
-
}) {
|
|
84
|
-
return {
|
|
85
|
-
subject: `You're invited to ${orgName} on ${appName}`,
|
|
86
|
-
...renderEmail({
|
|
87
|
-
brandName: appName,
|
|
88
|
-
preheader: `${inviter} invited you to ${orgName} on ${appName}.`,
|
|
89
|
-
heading: `You're invited to join ${orgName}`,
|
|
90
|
-
paragraphs: [
|
|
91
|
-
`${emailStrong(inviter)} invited you to the ${emailStrong(orgName)} organization on ${emailStrong(appName)} as ${emailStrong(role)}.`,
|
|
92
|
-
`Click the button below to accept the invite and start collaborating.`,
|
|
93
|
-
],
|
|
94
|
-
cta: { label: "Accept invite", url: inviteUrl },
|
|
95
|
-
// guard:allow-raw-color — email HTML cannot reference CSS theme tokens; mail clients don't support custom properties.
|
|
96
|
-
brandColor: "#18181B",
|
|
97
|
-
}),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
70
|
export default defineAction({
|
|
102
71
|
description:
|
|
103
72
|
"Invite someone to the active organization by email. Creates a pending invitation. Role 'admin' maps to admin; all other Clips roles collapse to 'member'. Sends an email when a provider is configured.",
|
|
@@ -154,17 +123,25 @@ export default defineAction({
|
|
|
154
123
|
const orgName = await fetchOrgName(organizationId);
|
|
155
124
|
const inviteUrl = `${baseUrl()}/invite/${token}`;
|
|
156
125
|
|
|
126
|
+
const appName = getAppName();
|
|
127
|
+
const { html, text } = renderEmail({
|
|
128
|
+
brandName: appName,
|
|
129
|
+
preheader: `${inviter} invited you to ${orgName} on ${appName}.`,
|
|
130
|
+
heading: `You're invited to join ${orgName}`,
|
|
131
|
+
paragraphs: [
|
|
132
|
+
`${emailStrong(inviter)} invited you to the ${emailStrong(orgName)} organization on ${emailStrong(appName)} as ${emailStrong(role)}.`,
|
|
133
|
+
`Click the button below to accept the invite and start collaborating.`,
|
|
134
|
+
],
|
|
135
|
+
cta: { label: "Accept invite", url: inviteUrl },
|
|
136
|
+
// guard:allow-raw-color - email HTML cannot reference CSS theme tokens.
|
|
137
|
+
brandColor: "#18181B",
|
|
138
|
+
});
|
|
157
139
|
try {
|
|
158
140
|
await sendEmail({
|
|
159
|
-
...renderClipsInviteEmail({
|
|
160
|
-
appName: getAppName(),
|
|
161
|
-
orgName,
|
|
162
|
-
inviter,
|
|
163
|
-
role,
|
|
164
|
-
inviteUrl,
|
|
165
|
-
}),
|
|
166
141
|
to: args.email,
|
|
167
|
-
|
|
142
|
+
subject: `You're invited to ${orgName} on ${appName}`,
|
|
143
|
+
html,
|
|
144
|
+
text,
|
|
168
145
|
});
|
|
169
146
|
} catch (err) {
|
|
170
147
|
console.warn("[invite-member] email send failed:", err);
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { Textarea } from "@/components/ui/textarea";
|
|
32
32
|
import { cn } from "@/lib/utils";
|
|
33
33
|
|
|
34
|
+
import { REACTION_EMOJIS } from "./reaction-emojis";
|
|
34
35
|
import { msToClock } from "./scrubber";
|
|
35
36
|
|
|
36
37
|
function makeTempId() {
|
|
@@ -60,8 +61,6 @@ const defaultLens: CommentsLens = {
|
|
|
60
61
|
data ? { ...(data as object), comments: next } : data,
|
|
61
62
|
};
|
|
62
63
|
|
|
63
|
-
const COMMENT_EMOJIS = ["👍", "❤️", "🔥", "👏", "🎉", "😂"];
|
|
64
|
-
|
|
65
64
|
export interface Comment {
|
|
66
65
|
id: string;
|
|
67
66
|
threadId: string;
|
|
@@ -928,7 +927,7 @@ function CommentCard({
|
|
|
928
927
|
</PopoverTrigger>
|
|
929
928
|
<PopoverContent side="top" align="start" className="p-1 w-auto">
|
|
930
929
|
<div className="flex gap-0.5">
|
|
931
|
-
{
|
|
930
|
+
{REACTION_EMOJIS.map((e) => (
|
|
932
931
|
<button
|
|
933
932
|
key={e}
|
|
934
933
|
onClick={() => {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "@/components/ui/tooltip";
|
|
8
8
|
import { cn } from "@/lib/utils";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
import { REACTION_EMOJIS } from "./reaction-emojis";
|
|
11
11
|
|
|
12
12
|
export interface ReactionsTrayProps {
|
|
13
13
|
onReact: (emoji: string) => void;
|
|
@@ -38,7 +38,7 @@ export function ReactionsTray({ onReact, disabled }: ReactionsTrayProps) {
|
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
40
|
<div className="relative flex w-fit max-w-full items-center gap-0.5 rounded-full border border-border bg-card px-1.5 py-1 shadow-sm sm:gap-1 sm:px-2">
|
|
41
|
-
{
|
|
41
|
+
{REACTION_EMOJIS.map((emoji) => (
|
|
42
42
|
<Tooltip key={emoji}>
|
|
43
43
|
<TooltipTrigger asChild>
|
|
44
44
|
<button
|
|
@@ -156,6 +156,8 @@ export interface VideoPlayerHandle {
|
|
|
156
156
|
export interface VideoPlayerProps {
|
|
157
157
|
recordingId: string;
|
|
158
158
|
videoUrl: string | null | undefined;
|
|
159
|
+
/** Version of the stored media bytes, used when a stable URL is replaced. */
|
|
160
|
+
mediaVersion?: string | number | null;
|
|
159
161
|
/**
|
|
160
162
|
* Container format of `videoUrl`, when known. Used only to pick an accurate
|
|
161
163
|
* `canPlayType` MIME check (e.g. Safari cannot play `video/webm`) — Clips
|
|
@@ -228,6 +230,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
228
230
|
const t = useT();
|
|
229
231
|
const {
|
|
230
232
|
videoUrl,
|
|
233
|
+
mediaVersion,
|
|
231
234
|
videoFormat,
|
|
232
235
|
embedProvider,
|
|
233
236
|
durationMs,
|
|
@@ -261,10 +264,21 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
261
264
|
onCommentClick,
|
|
262
265
|
} = props;
|
|
263
266
|
|
|
264
|
-
const resolvedVideoSrc = useMemo(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
267
|
+
const resolvedVideoSrc = useMemo(() => {
|
|
268
|
+
const localUrl = resolveLocalUrl(videoUrl);
|
|
269
|
+
if (
|
|
270
|
+
!localUrl ||
|
|
271
|
+
mediaVersion == null ||
|
|
272
|
+
embedProvider === "loom" ||
|
|
273
|
+
isLoomEmbedUrl(localUrl)
|
|
274
|
+
) {
|
|
275
|
+
return localUrl;
|
|
276
|
+
}
|
|
277
|
+
// Some storage providers keep the public URL stable while replacing the
|
|
278
|
+
// object behind it. Keep the player source identity aligned with the
|
|
279
|
+
// recording row so a repaired asset cannot stay cached in the player.
|
|
280
|
+
return setUrlSearchParam(localUrl, "media", String(mediaVersion));
|
|
281
|
+
}, [embedProvider, mediaVersion, videoUrl]);
|
|
268
282
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
269
283
|
const [playbackVideoEl, setPlaybackVideoEl] =
|
|
270
284
|
useState<HTMLVideoElement | null>(null);
|
|
@@ -569,11 +583,11 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
569
583
|
return;
|
|
570
584
|
}
|
|
571
585
|
|
|
572
|
-
if (recoveringFromErrorRef.current) return;
|
|
573
|
-
|
|
574
586
|
const v = videoRef.current;
|
|
575
587
|
const sameResource =
|
|
576
588
|
activeVideoSourceIdentity === incomingVideoSourceIdentity;
|
|
589
|
+
if (recoveringFromErrorRef.current && sameResource) return;
|
|
590
|
+
|
|
577
591
|
const playbackActive =
|
|
578
592
|
playAttemptPendingRef.current ||
|
|
579
593
|
isPlayPending ||
|
|
@@ -20,6 +20,8 @@ export function meta() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const STORAGE_KEY_PREFIX = "clips-share-pw-";
|
|
23
|
+
const READY_MEDIA_SETTLE_POLL_MS = 20 * 1000;
|
|
24
|
+
const READY_MEDIA_SETTLE_POLL_INTERVAL_MS = 1000;
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Parse `t` URL param into ms (supports plain seconds or `1m20s` / `1h2m3s`).
|
|
@@ -78,6 +80,7 @@ export default function EmbedRoute() {
|
|
|
78
80
|
}
|
|
79
81
|
});
|
|
80
82
|
const [pwError, setPwError] = useState<string | null>(null);
|
|
83
|
+
const readyMediaPollRef = useRef<{ key: string; until: number } | null>(null);
|
|
81
84
|
|
|
82
85
|
useEffect(() => {
|
|
83
86
|
if (typeof document === "undefined") return;
|
|
@@ -121,6 +124,32 @@ export default function EmbedRoute() {
|
|
|
121
124
|
return { ok: res.ok, status: res.status, data };
|
|
122
125
|
},
|
|
123
126
|
enabled: !!shareId,
|
|
127
|
+
refetchInterval: (q) => {
|
|
128
|
+
const payload = (q.state.data as { data?: any } | undefined)?.data;
|
|
129
|
+
const rec = payload?.recording;
|
|
130
|
+
if (!rec) return false;
|
|
131
|
+
if (rec.status !== "ready" || !rec.videoUrl) {
|
|
132
|
+
readyMediaPollRef.current = null;
|
|
133
|
+
return 2000;
|
|
134
|
+
}
|
|
135
|
+
const mediaKey = [
|
|
136
|
+
rec.id,
|
|
137
|
+
rec.durationMs ?? "",
|
|
138
|
+
rec.videoSizeBytes ?? "",
|
|
139
|
+
rec.videoFormat ?? "",
|
|
140
|
+
].join(":");
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
if (readyMediaPollRef.current?.key !== mediaKey) {
|
|
143
|
+
readyMediaPollRef.current = {
|
|
144
|
+
key: mediaKey,
|
|
145
|
+
until: now + READY_MEDIA_SETTLE_POLL_MS,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return now < readyMediaPollRef.current.until
|
|
149
|
+
? READY_MEDIA_SETTLE_POLL_INTERVAL_MS
|
|
150
|
+
: false;
|
|
151
|
+
},
|
|
152
|
+
refetchIntervalInBackground: false,
|
|
124
153
|
});
|
|
125
154
|
|
|
126
155
|
const recording = dataQ.data?.data?.recording;
|
|
@@ -196,6 +225,10 @@ export default function EmbedRoute() {
|
|
|
196
225
|
onVideoElementChange={setTrackedVideoEl}
|
|
197
226
|
recordingId={recording.id}
|
|
198
227
|
videoUrl={recording.videoUrl}
|
|
228
|
+
mediaVersion={[
|
|
229
|
+
recording.videoSizeBytes ?? "",
|
|
230
|
+
recording.updatedAt ?? "",
|
|
231
|
+
].join(":")}
|
|
199
232
|
videoFormat={recording.videoFormat}
|
|
200
233
|
embedProvider={isLoomEmbedBacked ? "loom" : null}
|
|
201
234
|
durationMs={recording.durationMs}
|
|
@@ -348,9 +348,9 @@ export default function RecordingPage() {
|
|
|
348
348
|
// without requiring a manual refresh.
|
|
349
349
|
const mediaKey = [
|
|
350
350
|
rec.id,
|
|
351
|
-
rec.videoUrl,
|
|
352
351
|
rec.durationMs ?? "",
|
|
353
352
|
rec.videoSizeBytes ?? "",
|
|
353
|
+
rec.videoFormat ?? "",
|
|
354
354
|
].join(":");
|
|
355
355
|
const now = Date.now();
|
|
356
356
|
if (readyMediaPollRef.current?.key !== mediaKey) {
|
|
@@ -1601,6 +1601,10 @@ export default function RecordingPage() {
|
|
|
1601
1601
|
onVideoElementChange={setTrackedVideoEl}
|
|
1602
1602
|
recordingId={recording.id}
|
|
1603
1603
|
videoUrl={recording.videoUrl}
|
|
1604
|
+
mediaVersion={[
|
|
1605
|
+
recording.videoSizeBytes ?? "",
|
|
1606
|
+
recording.updatedAt ?? "",
|
|
1607
|
+
].join(":")}
|
|
1604
1608
|
videoFormat={recording.videoFormat}
|
|
1605
1609
|
embedProvider={isLoomEmbedBacked ? "loom" : null}
|
|
1606
1610
|
durationMs={recording.durationMs}
|