Skip to content

Commit bd23c56

Browse files
Merge pull request #63 from Saksham-Sirohi/adding-db-and-fetaure-gating
fix notes exporters and type checking
2 parents ce8e69a + 4738605 commit bd23c56

15 files changed

Lines changed: 351 additions & 389 deletions

File tree

‎server/package-lock.json‎

Lines changed: 74 additions & 371 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Meeting" ADD COLUMN "description" TEXT;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Please do not edit this file manually
22
# It should be added in your version-control system (e.g., Git)
3-
provider = "postgresql"
3+
provider = "postgresql"

‎server/prisma/schema.prisma‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ model Meeting {
2929
startTime DateTime?
3030
endTime DateTime?
3131
recurrence String? // NONE, DAILY, WEEKLY, MONTHLY, YEARLY
32+
description String?
3233
guestEmails String[] @default([])
3334
moderatorEmails String[] @default([])
3435
googleEventId String?

‎server/src/controllers/meetingController.ts‎

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,45 @@ async function getGoogleAccessToken(userId: string, req?: Request): Promise<stri
5454
}
5555
}
5656

57+
// Helper to convert simple Markdown to HTML for Google Calendar
58+
function markdownToHtml(md: string): string {
59+
if (!md) return '';
60+
return md
61+
.replace(/&/g, '&amp;')
62+
.replace(/</g, '&lt;')
63+
.replace(/>/g, '&gt;')
64+
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
65+
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
66+
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
67+
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
68+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
69+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
70+
.replace(/__(.*?)__/g, '<strong>$1</strong>')
71+
.replace(/_(.*?)_/g, '<em>$1</em>')
72+
.replace(/`(.*?)`/g, '<code>$1</code>')
73+
.replace(/^\> (.*$)/gim, '<blockquote>$1</blockquote>')
74+
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>')
75+
.replace(/\n/g, '<br />');
76+
}
77+
5778
// Helper: Sync Event to Google Calendar (Create or Update)
5879
async function syncGoogleCalendarEvent(
5980
userId: string,
60-
meeting: { title: string; roomName: string; startTime: Date; endTime: Date; recurrence?: string | null; guestEmails?: string[]; moderatorEmails?: string[] },
81+
meeting: { title: string; description?: string | null; roomName: string; startTime: Date; endTime: Date; recurrence?: string | null; guestEmails?: string[]; moderatorEmails?: string[] },
6182
existingEventId?: string | null,
6283
req?: Request
6384
): Promise<string | null> {
6485
const host = req?.headers.host || 'localhost:8780';
6586
const accessToken = await getGoogleAccessToken(userId, req);
6687
if (!accessToken) return null;
6788

89+
const joinLinkHtml = `<p><strong>Join LoungeMesh Spatial Meet:</strong> <a href="http://${host}/session/${meeting.roomName}">http://${host}/session/${meeting.roomName}</a></p>`;
90+
const descHtml = meeting.description ? markdownToHtml(meeting.description) : '';
91+
const finalDescription = descHtml ? `${descHtml}<br /><br />${joinLinkHtml}` : joinLinkHtml;
92+
6893
const eventPayload: any = {
6994
summary: meeting.title,
70-
description: `Join LoungeMesh Spatial Meet: http://${host}/session/${meeting.roomName}`,
95+
description: finalDescription,
7196
start: {
7297
dateTime: meeting.startTime.toISOString(),
7398
timeZone: 'UTC',
@@ -156,7 +181,8 @@ export async function createMeeting(req: Request, res: Response) {
156181
lobbyEnabled,
157182
stagePromotionEnabled,
158183
allowParticipantRecording,
159-
roomDefaults
184+
roomDefaults,
185+
description
160186
} = req.body;
161187

162188
if (!title || !roomName) {
@@ -185,6 +211,7 @@ export async function createMeeting(req: Request, res: Response) {
185211
startTime: start,
186212
endTime: end,
187213
recurrence: recurrence || 'NONE',
214+
description: description || null,
188215
guestEmails: guests,
189216
moderatorEmails: moderators,
190217
hostId,
@@ -206,6 +233,7 @@ export async function createMeeting(req: Request, res: Response) {
206233
if (isScheduled && start && end && syncGoogleCal) {
207234
const eventId = await syncGoogleCalendarEvent(hostId, {
208235
title,
236+
description: description || null,
209237
roomName,
210238
startTime: start,
211239
endTime: end,
@@ -304,7 +332,8 @@ export async function updateMeeting(req: Request, res: Response) {
304332
stagePromotionEnabled,
305333
allowParticipantRecording,
306334
roomDefaults,
307-
syncGoogleCal
335+
syncGoogleCal,
336+
description
308337
} = req.body;
309338

310339
try {
@@ -346,6 +375,7 @@ export async function updateMeeting(req: Request, res: Response) {
346375
startTime: start,
347376
endTime: end,
348377
recurrence: rec,
378+
description: description !== undefined ? description : meeting.description,
349379
guestEmails: guests,
350380
moderatorEmails: moderators,
351381
configs: {
@@ -383,6 +413,7 @@ export async function updateMeeting(req: Request, res: Response) {
383413
meeting.hostId,
384414
{
385415
title: updated.title,
416+
description: updated.description,
386417
roomName: updated.roomName,
387418
startTime: start,
388419
endTime: end,

‎src-vue/components/home/ScheduleModal.vue‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { ref, onMounted } from 'vue';
33
import { useAuthStore } from '@/stores/authStore';
4+
import NotesEditor from '@/components/session/NotesEditor.vue';
45
56
export interface ScheduledMeeting {
67
id: string;
@@ -15,6 +16,7 @@ export interface ScheduledMeeting {
1516
configs?: {
1617
userGrants?: string | null;
1718
} | null;
19+
description?: string | null;
1820
}
1921
2022
const props = defineProps<{
@@ -29,6 +31,7 @@ const emit = defineEmits<{
2931
const auth = useAuthStore();
3032
3133
const title = ref('');
34+
const description = ref('');
3235
const date = ref('');
3336
const time = ref('');
3437
const duration = ref(30); // default 30 mins
@@ -146,6 +149,7 @@ function handleModEmailKeydown(e: KeyboardEvent) {
146149
onMounted(() => {
147150
if (props.meeting) {
148151
title.value = props.meeting.title;
152+
description.value = props.meeting.description || '';
149153
recurrence.value = props.meeting.recurrence || 'NONE';
150154
syncGoogleCal.value = !!props.meeting.googleEventId;
151155
guestEmails.value = (props.meeting as any).guestEmails ? [...(props.meeting as any).guestEmails] : [];
@@ -238,6 +242,7 @@ async function handleSubmit() {
238242
239243
const payload = {
240244
title: title.value.trim(),
245+
description: description.value.trim(),
241246
roomName,
242247
isScheduled: true,
243248
startTime: startDateTime.toISOString(),
@@ -318,6 +323,15 @@ async function handleSubmit() {
318323
</Transition>
319324
</div>
320325

326+
<div class="inputGroup">
327+
<label>Description</label>
328+
<NotesEditor
329+
v-model="description"
330+
placeholder="Agenda, objectives, links..."
331+
class="descriptionEditor"
332+
/>
333+
</div>
334+
321335
<div class="row">
322336
<div class="inputGroup half">
323337
<label for="meetDate">
@@ -464,7 +478,7 @@ async function handleSubmit() {
464478
border-radius: 20px;
465479
box-shadow: 0 30px 60px -15px rgba(30, 34, 64, 0.15), 0 10px 20px -5px rgba(30, 34, 64, 0.05);
466480
width: 100%;
467-
max-width: 420px;
481+
max-width: 480px;
468482
padding: 24px;
469483
position: relative;
470484
display: flex;
@@ -521,6 +535,14 @@ async function handleSubmit() {
521535
display: flex;
522536
flex-direction: column;
523537
gap: 16px;
538+
max-height: 65vh;
539+
overflow-y: auto;
540+
padding-right: 6px;
541+
}
542+
543+
.descriptionEditor {
544+
height: 140px;
545+
max-height: 140px;
524546
}
525547
526548
.error {

‎src-vue/components/session/LeaveDialog.test.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ describe('LeaveDialog', () => {
2323
it('shows export options and emits each action for hosts', async () => {
2424
const wrapper = mountDialog({ hasRecording: true });
2525
const buttons = wrapper.findAll('.export');
26-
expect(buttons).toHaveLength(3);
26+
expect(buttons).toHaveLength(4);
2727
await buttons[0].trigger('click');
2828
await buttons[1].trigger('click');
2929
await buttons[2].trigger('click');
30+
await buttons[3].trigger('click');
3031
await wrapper.find('.btn.leave').trigger('click');
3132
await wrapper.find('.btn.cancel').trigger('click');
3233
expect(wrapper.emitted('export-notes')).toHaveLength(1);
34+
expect(wrapper.emitted('export-notes-rtf')).toHaveLength(1);
3335
expect(wrapper.emitted('export-whiteboard')).toHaveLength(1);
3436
expect(wrapper.emitted('export-recording')).toHaveLength(1);
3537
expect(wrapper.emitted('leave')).toHaveLength(1);
@@ -38,13 +40,13 @@ describe('LeaveDialog', () => {
3840

3941
it('disables the recording download until a recording exists', () => {
4042
const wrapper = mountDialog({ hasRecording: false });
41-
const recordingBtn = wrapper.findAll('.export')[2];
43+
const recordingBtn = wrapper.findAll('.export')[3];
4244
expect((recordingBtn.element as HTMLButtonElement).disabled).toBe(true);
4345
});
4446

4547
it('hides the recording export when recording is unsupported', () => {
4648
const wrapper = mountDialog({ recordingSupported: false });
47-
expect(wrapper.findAll('.export')).toHaveLength(2);
49+
expect(wrapper.findAll('.export')).toHaveLength(3);
4850
});
4951

5052
it('shows LIVE pill on the recording export button while recording is active', () => {

‎src-vue/components/session/LeaveDialog.vue‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const emit = defineEmits<{
1313
(e: 'cancel'): void;
1414
(e: 'leave'): void;
1515
(e: 'export-notes'): void;
16+
(e: 'export-notes-rtf'): void;
1617
(e: 'export-whiteboard'): void;
1718
(e: 'export-recording'): void;
1819
(e: 'toggle-recording'): void;
@@ -32,6 +33,7 @@ function onLeave() {
3233
function onExport(type: string) {
3334
playUiSound('tap');
3435
if (type === 'notes') emit('export-notes');
36+
else if (type === 'notes-rtf') emit('export-notes-rtf');
3537
else if (type === 'whiteboard') emit('export-whiteboard');
3638
else if (type === 'recording') emit('export-recording');
3739
}
@@ -71,11 +73,24 @@ function onExport(type: string) {
7173
<line x1="16" y1="13" x2="8" y2="13"/>
7274
<line x1="16" y1="17" x2="8" y2="17"/>
7375
</svg>
74-
<span>Notes</span>
76+
<span>Public Notes</span>
7577
</div>
7678
<span class="ext">.md</span>
7779
</button>
7880

81+
<button type="button" class="export" @click="onExport('notes-rtf')">
82+
<div class="exportLeft">
83+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="exportIcon">
84+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
85+
<polyline points="14 2 14 8 20 8"/>
86+
<line x1="16" y1="13" x2="8" y2="13"/>
87+
<line x1="16" y1="17" x2="8" y2="17"/>
88+
</svg>
89+
<span>Public Notes (RTF)</span>
90+
</div>
91+
<span class="ext">.rtf</span>
92+
</button>
93+
7994
<button type="button" class="export" @click="onExport('whiteboard')">
8095
<div class="exportLeft">
8196
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="exportIcon">

‎src-vue/composables/useSessionExport.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ vi.mock('@/utils/sessionExport', () => ({
1212
renderWhiteboardToCanvas: vi.fn(() => ({}) as HTMLCanvasElement),
1313
canvasToPngBlob,
1414
downloadBlob,
15+
markdownToRtf: vi.fn(() => 'RTF'),
16+
safeSessionSlug: vi.fn(() => 'slug'),
1517
}));
1618

1719
import { useSessionExport } from './useSessionExport';
@@ -29,6 +31,11 @@ describe('useSessionExport', () => {
2931
expect(downloadBlob).toHaveBeenCalledWith(expect.any(Blob), 'file.ext');
3032
});
3133

34+
it('exports notes as an RTF download', () => {
35+
useSessionExport(() => 'room').exportNotesRtf();
36+
expect(downloadBlob).toHaveBeenCalledWith(expect.any(Blob), 'file.ext');
37+
});
38+
3239
it('exports the whiteboard when a PNG blob is produced', async () => {
3340
canvasToPngBlob.mockResolvedValue(new Blob(['p']));
3441
await useSessionExport(() => 'room').exportWhiteboard();

‎src-vue/composables/useSessionExport.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
exportFileName,
66
notesToMarkdown,
77
renderWhiteboardToCanvas,
8+
markdownToRtf,
9+
safeSessionSlug,
810
} from '@/utils/sessionExport';
911

1012
/** Download helpers for the host's end-of-session exports (notes, whiteboard, recording). */
@@ -16,6 +18,12 @@ export function useSessionExport(getSessionId: () => string) {
1618
downloadBlob(new Blob([markdown], { type: 'text/markdown' }), exportFileName('notes', getSessionId()));
1719
}
1820

21+
function exportNotesRtf(): void {
22+
const titleText = `LoungeMesh public notes — ${safeSessionSlug(getSessionId())}`;
23+
const rtfContent = markdownToRtf(features.sharedNotes, titleText);
24+
downloadBlob(new Blob([rtfContent], { type: 'application/rtf' }), exportFileName('notes-rtf', getSessionId()));
25+
}
26+
1927
async function exportWhiteboard(): Promise<void> {
2028
const canvas = renderWhiteboardToCanvas(features.whiteboardStrokes);
2129
const blob = await canvasToPngBlob(canvas);
@@ -26,7 +34,7 @@ export function useSessionExport(getSessionId: () => string) {
2634
const snapBlob = await canvasToPngBlob(snapCanvas);
2735
/* v8 ignore next 3 */
2836
if (snapBlob) {
29-
downloadBlob(snapBlob, exportFileName(`whiteboard-snapshot-${i + 1}`, getSessionId()));
37+
downloadBlob(snapBlob, exportFileName(`whiteboard-snapshot-${i + 1}` as any, getSessionId()));
3038
}
3139
}
3240
}
@@ -35,5 +43,5 @@ export function useSessionExport(getSessionId: () => string) {
3543
downloadBlob(blob, exportFileName('recording', getSessionId(), new Date(), 'mp4'));
3644
}
3745

38-
return { exportNotes, exportWhiteboard, exportRecording };
46+
return { exportNotes, exportNotesRtf, exportWhiteboard, exportRecording };
3947
}

0 commit comments

Comments
 (0)