@@ -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}
0 commit comments