Skip to content

Commit 85298c3

Browse files
authored
Merge pull request #203 from linuxfoundation/fix/LFXV2-891
fix(meetings): use consistent meeting identifier for v1/v2 compatibility
2 parents 5f980ca + 29436fb commit 85298c3

File tree

7 files changed

+248
-175
lines changed

7 files changed

+248
-175
lines changed

apps/lfx-one/src/app/modules/dashboards/components/dashboard-meeting-card/dashboard-meeting-card.component.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
<div class="flex items-center gap-1 flex-shrink-0 mt-0.5">
8181
@for (attachment of attachments(); track attachment.uid; let index = $index) {
8282
<a
83-
[href]="attachment.type === 'link' ? attachment.link : '/api/meetings/' + meeting().uid + '/attachments/' + attachment.uid"
83+
[href]="attachment.type === 'link' ? attachment.link : '/api/meetings/' + meetingIdentifier() + '/attachments/' + attachment.uid"
8484
target="_blank"
8585
rel="noopener noreferrer"
8686
[download]="attachment.type === 'file' ? attachment.name : null"
@@ -101,8 +101,7 @@
101101
<lfx-button
102102
class="w-full"
103103
label="See Meeting Details"
104-
[routerLink]="meetingDetailRouterLink()"
105-
[queryParams]="meetingDetailQueryParams()"
104+
[href]="meetingDetailUrl()"
106105
target="_blank"
107106
type="button"
108107
rel="noopener noreferrer"

apps/lfx-one/src/app/modules/dashboards/components/dashboard-meeting-card/dashboard-meeting-card.component.ts

Lines changed: 195 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -41,159 +41,33 @@ export class DashboardMeetingCardComponent {
4141
public readonly joinUrl: Signal<string | null>;
4242

4343
// Computed values
44-
public readonly meetingTypeInfo: Signal<MeetingTypeBadge> = computed(() => {
45-
const type = this.meeting().meeting_type?.toLowerCase();
46-
const config = type ? (MEETING_TYPE_CONFIGS[type] ?? DEFAULT_MEETING_TYPE_CONFIG) : DEFAULT_MEETING_TYPE_CONFIG;
47-
48-
// Map text color to severity
49-
let severity: ComponentSeverity = 'secondary';
50-
if (config.textColor.includes('red')) severity = 'danger';
51-
else if (config.textColor.includes('blue')) severity = 'info';
52-
else if (config.textColor.includes('green')) severity = 'success';
53-
else if (config.textColor.includes('purple')) severity = 'primary';
54-
else if (config.textColor.includes('amber')) severity = 'warn';
55-
56-
return {
57-
label: config.label,
58-
className: `${config.bgColor} ${config.textColor}`,
59-
severity,
60-
icon: `${config.icon} mr-2`,
61-
};
62-
});
63-
64-
public readonly meetingStartTime: Signal<string> = computed(() => {
65-
const occurrence = this.occurrence();
66-
const meeting = this.meeting();
67-
68-
// Use occurrence start time if available, otherwise use meeting start time
69-
return occurrence?.start_time || meeting.start_time;
70-
});
71-
72-
public readonly formattedTime: Signal<string> = computed(() => {
73-
const startTime = this.meetingStartTime();
74-
75-
try {
76-
const meetingDate = new Date(startTime);
77-
78-
if (isNaN(meetingDate.getTime())) {
79-
return startTime;
80-
}
81-
82-
const today = new Date();
83-
const tomorrow = new Date(today);
84-
tomorrow.setDate(tomorrow.getDate() + 1);
85-
86-
const isToday = meetingDate.toDateString() === today.toDateString();
87-
const isTomorrow = meetingDate.toDateString() === tomorrow.toDateString();
88-
89-
const timeStr = meetingDate.toLocaleTimeString('en-US', {
90-
hour: 'numeric',
91-
minute: '2-digit',
92-
hour12: true,
93-
});
94-
95-
if (isToday) {
96-
return `Today, ${timeStr}`;
97-
} else if (isTomorrow) {
98-
return `Tomorrow, ${timeStr}`;
99-
}
100-
const dateStr = meetingDate.toLocaleDateString('en-US', {
101-
month: 'short',
102-
day: 'numeric',
103-
});
104-
return `${dateStr} at ${timeStr}`;
105-
} catch {
106-
return startTime;
107-
}
108-
});
109-
110-
public readonly isTodayMeeting: Signal<boolean> = computed(() => {
111-
const startTime = this.meetingStartTime();
112-
113-
try {
114-
const meetingDate = new Date(startTime);
115-
116-
if (isNaN(meetingDate.getTime())) {
117-
return false;
118-
}
119-
120-
const today = new Date();
121-
return meetingDate.toDateString() === today.toDateString();
122-
} catch {
123-
return false;
124-
}
125-
});
126-
127-
public readonly isPrivate: Signal<boolean> = computed(() => {
128-
return this.meeting().visibility === 'private';
129-
});
130-
131-
public readonly hasYoutubeUploads: Signal<boolean> = computed(() => {
132-
return this.meeting().youtube_upload_enabled === true;
133-
});
134-
135-
public readonly hasRecording: Signal<boolean> = computed(() => {
136-
return this.meeting().recording_enabled === true;
137-
});
138-
139-
public readonly hasTranscripts: Signal<boolean> = computed(() => {
140-
return this.meeting().transcript_enabled === true;
141-
});
142-
143-
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
144-
public readonly hasAiSummary: Signal<boolean> = computed(() => {
145-
const meeting = this.meeting();
146-
// V2: zoom_config.ai_companion_enabled, V1: zoom_ai_enabled
147-
return meeting.zoom_config?.ai_companion_enabled === true || meeting.zoom_ai_enabled === true;
148-
});
44+
public readonly meetingTypeInfo: Signal<MeetingTypeBadge> = this.initMeetingTypeInfo();
45+
public readonly meetingStartTime: Signal<string> = this.initMeetingStartTime();
46+
public readonly formattedTime: Signal<string> = this.initFormattedTime();
47+
public readonly isTodayMeeting: Signal<boolean> = this.initIsTodayMeeting();
48+
public readonly isPrivate: Signal<boolean> = this.initIsPrivate();
49+
public readonly hasYoutubeUploads: Signal<boolean> = this.initHasYoutubeUploads();
50+
public readonly hasRecording: Signal<boolean> = this.initHasRecording();
51+
public readonly hasTranscripts: Signal<boolean> = this.initHasTranscripts();
52+
public readonly canJoinMeeting: Signal<boolean> = this.initCanJoinMeeting();
14953

15054
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
151-
public readonly meetingTitle: Signal<string> = computed(() => {
152-
const occurrence = this.occurrence();
153-
const meeting = this.meeting();
154-
155-
// Priority: occurrence title > meeting title > meeting topic (v1)
156-
return occurrence?.title || meeting.title || meeting.topic || '';
157-
});
158-
159-
public readonly canJoinMeeting: Signal<boolean> = computed(() => {
160-
return canJoinMeeting(this.meeting(), this.occurrence());
161-
});
55+
public readonly hasAiSummary: Signal<boolean> = this.initHasAiSummary();
56+
public readonly meetingTitle: Signal<string> = this.initMeetingTitle();
16257

16358
// TODO(v1-migration): Remove once all meetings are migrated to V2
164-
public readonly isLegacyMeeting: Signal<boolean> = computed(() => {
165-
return this.meeting().version === 'v1';
166-
});
167-
168-
// TODO(v1-migration): Simplify to use V2 uid only once all meetings are migrated to V2
169-
public readonly meetingDetailRouterLink: Signal<string[]> = computed(() => {
170-
const meeting = this.meeting();
171-
const identifier = this.isLegacyMeeting() && meeting.id ? meeting.id : meeting.uid;
172-
return ['/meetings', identifier];
173-
});
174-
175-
// TODO(v1-migration): Remove V1 parameter handling once all meetings are migrated to V2
176-
public readonly meetingDetailQueryParams: Signal<Record<string, string>> = computed(() => {
177-
const meeting = this.meeting();
178-
const params: Record<string, string> = {};
179-
180-
if (meeting.password) {
181-
params['password'] = meeting.password;
182-
}
183-
if (this.isLegacyMeeting()) {
184-
params['v1'] = 'true';
185-
}
186-
187-
return params;
188-
});
59+
public readonly isLegacyMeeting: Signal<boolean> = this.initIsLegacyMeeting();
60+
public readonly meetingIdentifier: Signal<string> = this.initMeetingIdentifier();
61+
public readonly meetingDetailUrl: Signal<string> = this.initMeetingDetailUrl();
18962

19063
public constructor() {
19164
// Convert meeting input signal to observable and create reactive attachment stream
19265
const meeting$ = toObservable(this.meeting);
193-
const attachments$ = meeting$.pipe(
194-
switchMap((meeting) => {
195-
if (meeting.uid) {
196-
return this.meetingService.getMeetingAttachments(meeting.uid).pipe(catchError(() => of([])));
66+
const meetingIdentifier$ = toObservable(this.meetingIdentifier);
67+
const attachments$ = meetingIdentifier$.pipe(
68+
switchMap((identifier) => {
69+
if (identifier) {
70+
return this.meetingService.getMeetingAttachments(identifier).pipe(catchError(() => of([])));
19771
}
19872
return of([]);
19973
})
@@ -227,4 +101,180 @@ export class DashboardMeetingCardComponent {
227101

228102
this.joinUrl = toSignal(joinUrl$, { initialValue: null });
229103
}
104+
105+
private initMeetingTypeInfo(): Signal<MeetingTypeBadge> {
106+
return computed(() => {
107+
const type = this.meeting().meeting_type?.toLowerCase();
108+
const config = type ? (MEETING_TYPE_CONFIGS[type] ?? DEFAULT_MEETING_TYPE_CONFIG) : DEFAULT_MEETING_TYPE_CONFIG;
109+
110+
// Map text color to severity
111+
let severity: ComponentSeverity = 'secondary';
112+
if (config.textColor.includes('red')) severity = 'danger';
113+
else if (config.textColor.includes('blue')) severity = 'info';
114+
else if (config.textColor.includes('green')) severity = 'success';
115+
else if (config.textColor.includes('purple')) severity = 'primary';
116+
else if (config.textColor.includes('amber')) severity = 'warn';
117+
118+
return {
119+
label: config.label,
120+
className: `${config.bgColor} ${config.textColor}`,
121+
severity,
122+
icon: `${config.icon} mr-2`,
123+
};
124+
});
125+
}
126+
127+
private initMeetingStartTime(): Signal<string> {
128+
return computed(() => {
129+
const occurrence = this.occurrence();
130+
const meeting = this.meeting();
131+
132+
// Use occurrence start time if available, otherwise use meeting start time
133+
return occurrence?.start_time || meeting.start_time;
134+
});
135+
}
136+
137+
private initFormattedTime(): Signal<string> {
138+
return computed(() => {
139+
const startTime = this.meetingStartTime();
140+
141+
try {
142+
const meetingDate = new Date(startTime);
143+
144+
if (isNaN(meetingDate.getTime())) {
145+
return startTime;
146+
}
147+
148+
const today = new Date();
149+
const tomorrow = new Date(today);
150+
tomorrow.setDate(tomorrow.getDate() + 1);
151+
152+
const isToday = meetingDate.toDateString() === today.toDateString();
153+
const isTomorrow = meetingDate.toDateString() === tomorrow.toDateString();
154+
155+
const timeStr = meetingDate.toLocaleTimeString('en-US', {
156+
hour: 'numeric',
157+
minute: '2-digit',
158+
hour12: true,
159+
});
160+
161+
if (isToday) {
162+
return `Today, ${timeStr}`;
163+
} else if (isTomorrow) {
164+
return `Tomorrow, ${timeStr}`;
165+
}
166+
const dateStr = meetingDate.toLocaleDateString('en-US', {
167+
month: 'short',
168+
day: 'numeric',
169+
});
170+
return `${dateStr} at ${timeStr}`;
171+
} catch {
172+
return startTime;
173+
}
174+
});
175+
}
176+
177+
private initIsTodayMeeting(): Signal<boolean> {
178+
return computed(() => {
179+
const startTime = this.meetingStartTime();
180+
181+
try {
182+
const meetingDate = new Date(startTime);
183+
184+
if (isNaN(meetingDate.getTime())) {
185+
return false;
186+
}
187+
188+
const today = new Date();
189+
return meetingDate.toDateString() === today.toDateString();
190+
} catch {
191+
return false;
192+
}
193+
});
194+
}
195+
196+
private initIsPrivate(): Signal<boolean> {
197+
return computed(() => {
198+
return this.meeting().visibility === 'private';
199+
});
200+
}
201+
202+
private initHasYoutubeUploads(): Signal<boolean> {
203+
return computed(() => {
204+
return this.meeting().youtube_upload_enabled === true;
205+
});
206+
}
207+
208+
private initHasRecording(): Signal<boolean> {
209+
return computed(() => {
210+
return this.meeting().recording_enabled === true;
211+
});
212+
}
213+
214+
private initHasTranscripts(): Signal<boolean> {
215+
return computed(() => {
216+
return this.meeting().transcript_enabled === true;
217+
});
218+
}
219+
220+
private initCanJoinMeeting(): Signal<boolean> {
221+
return computed(() => {
222+
return canJoinMeeting(this.meeting(), this.occurrence());
223+
});
224+
}
225+
226+
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
227+
private initHasAiSummary(): Signal<boolean> {
228+
return computed(() => {
229+
const meeting = this.meeting();
230+
// V2: zoom_config.ai_companion_enabled, V1: zoom_ai_enabled
231+
return meeting.zoom_config?.ai_companion_enabled === true || meeting.zoom_ai_enabled === true;
232+
});
233+
}
234+
235+
// TODO(v1-migration): Simplify to use V2 fields only once all meetings are migrated to V2
236+
private initMeetingTitle(): Signal<string> {
237+
return computed(() => {
238+
const occurrence = this.occurrence();
239+
const meeting = this.meeting();
240+
241+
// Priority: occurrence title > meeting title > meeting topic (v1)
242+
return occurrence?.title || meeting.title || meeting.topic || '';
243+
});
244+
}
245+
246+
// TODO(v1-migration): Remove once all meetings are migrated to V2
247+
private initIsLegacyMeeting(): Signal<boolean> {
248+
return computed(() => {
249+
return this.meeting().version === 'v1';
250+
});
251+
}
252+
253+
// TODO(v1-migration): Simplify to use V2 uid only once all meetings are migrated to V2
254+
private initMeetingIdentifier(): Signal<string> {
255+
return computed(() => {
256+
const meeting = this.meeting();
257+
return this.isLegacyMeeting() && meeting.id ? (meeting.id as string) : meeting.uid;
258+
});
259+
}
260+
261+
// TODO(v1-migration): Remove V1 parameter handling once all meetings are migrated to V2
262+
private initMeetingDetailUrl(): Signal<string> {
263+
return computed(() => {
264+
const meeting = this.meeting();
265+
const identifier = this.meetingIdentifier();
266+
const params = new URLSearchParams();
267+
268+
if (meeting.password) {
269+
params.set('password', meeting.password);
270+
}
271+
272+
if (this.isLegacyMeeting()) {
273+
params.set('v1', 'true');
274+
}
275+
276+
const queryString = params.toString();
277+
return queryString ? `/meetings/${identifier}?${queryString}` : `/meetings/${identifier}`;
278+
});
279+
}
230280
}

apps/lfx-one/src/app/modules/meetings/components/meeting-card/meeting-card.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
177177
<div class="grid grid-cols-2 gap-1">
178178
@for (attachment of attachments(); track attachment.uid) {
179179
<a
180-
[href]="attachment.type === 'link' ? attachment.link : '/api/meetings/' + meeting().uid + '/attachments/' + attachment.uid"
180+
[href]="attachment.type === 'link' ? attachment.link : '/api/meetings/' + meetingIdentifier() + '/attachments/' + attachment.uid"
181181
target="_blank"
182182
rel="noopener noreferrer"
183183
class="inline-flex !text-black font-semibold items-center gap-1.5 px-2 py-1.5 bg-gray-200/80 hover:bg-gray-300/80 rounded text-[12px] tracking-wide transition-colors h-[26px]"
@@ -217,8 +217,8 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
217217
size="small"
218218
class="w-full"
219219
label="Join Meeting"
220-
[routerLink]="['/meetings', meeting().uid]"
221-
[queryParams]="{ password: meeting().password || '' }"
220+
[routerLink]="['/meetings', meetingIdentifier()]"
221+
[queryParams]="joinQueryParams()"
222222
rel="noopener noreferrer"
223223
icon="fa-light fa-video"
224224
styleClass="w-full !bg-emerald-500 hover:!bg-emerald-600 !text-white !font-semibold"

0 commit comments

Comments
 (0)