33
44import { CommonModule } from '@angular/common' ;
55import { HttpParams } from '@angular/common/http' ;
6- import { Component , computed , inject , signal , Signal , WritableSignal } from '@angular/core' ;
6+ import { Component , computed , effect , inject , OnDestroy , signal , Signal , WritableSignal } from '@angular/core' ;
77import { toObservable , toSignal } from '@angular/core/rxjs-interop' ;
88import { FormControl , FormGroup , ReactiveFormsModule , Validators } from '@angular/forms' ;
99import { ActivatedRoute , Router } from '@angular/router' ;
@@ -46,7 +46,7 @@ import { catchError, combineLatest, finalize, map, of, switchMap, tap } from 'rx
4646 providers : [ ] ,
4747 templateUrl : './meeting-join.component.html' ,
4848} )
49- export class MeetingJoinComponent {
49+ export class MeetingJoinComponent implements OnDestroy {
5050 // Injected services
5151 private readonly messageService = inject ( MessageService ) ;
5252 private readonly activatedRoute = inject ( ActivatedRoute ) ;
@@ -69,6 +69,10 @@ export class MeetingJoinComponent {
6969 public canJoinMeeting : Signal < boolean > ;
7070 public joinUrlWithParams : Signal < string | undefined > ;
7171 public attachments : Signal < MeetingAttachment [ ] > ;
72+ public hasAutoJoined : WritableSignal < boolean > = signal < boolean > ( false ) ;
73+ private autoJoinTimeout : ReturnType < typeof setTimeout > | null = null ;
74+ public messageSeverity : Signal < 'success' | 'info' | 'warn' > ;
75+ public messageIcon : Signal < string > ;
7276
7377 // Form value signals for reactivity
7478 private formValues : Signal < { name : string ; email : string ; organization : string } > ;
@@ -87,6 +91,42 @@ export class MeetingJoinComponent {
8791 this . canJoinMeeting = this . initializeCanJoinMeeting ( ) ;
8892 this . joinUrlWithParams = this . initializeJoinUrlWithParams ( ) ;
8993 this . attachments = this . initializeAttachments ( ) ;
94+ this . messageSeverity = this . initializeMessageSeverity ( ) ;
95+ this . messageIcon = this . initializeMessageIcon ( ) ;
96+
97+ // Auto-join effect for signed-in users - use allowSignalWrites for state updates
98+ effect (
99+ ( ) => {
100+ const authenticated = this . authenticated ( ) ;
101+ const user = this . user ( ) ;
102+ const canJoinMeeting = this . canJoinMeeting ( ) ;
103+ const hasAutoJoined = this . hasAutoJoined ( ) ;
104+ const meeting = this . meeting ( ) ;
105+
106+ // Clear any existing timeout
107+ if ( this . autoJoinTimeout ) {
108+ clearTimeout ( this . autoJoinTimeout ) ;
109+ this . autoJoinTimeout = null ;
110+ }
111+
112+ // Schedule auto-join only if conditions are met
113+ if ( authenticated && user && user . email && canJoinMeeting && ! hasAutoJoined && meeting && meeting . uid && ! this . isJoining ( ) ) {
114+ // Set a timeout to prevent rapid-fire execution
115+ this . autoJoinTimeout = setTimeout ( ( ) => {
116+ this . performAutoJoin ( ) ;
117+ } , 500 ) ; // Small delay to let all signals settle
118+ }
119+ } ,
120+ { allowSignalWrites : true }
121+ ) ;
122+ }
123+
124+ public ngOnDestroy ( ) : void {
125+ // Cleanup timeout on component destroy
126+ if ( this . autoJoinTimeout ) {
127+ clearTimeout ( this . autoJoinTimeout ) ;
128+ this . autoJoinTimeout = null ;
129+ }
90130 }
91131
92132 public onJoinMeeting ( ) : void {
@@ -110,14 +150,73 @@ export class MeetingJoinComponent {
110150 next : ( res ) => {
111151 this . meeting ( ) . join_url = res . join_url ;
112152 const joinUrlWithParams = this . buildJoinUrlWithParams ( res . join_url ) ;
113- window . open ( joinUrlWithParams , '_blank' ) ;
153+ this . openMeetingSecurely ( joinUrlWithParams ) ;
114154 } ,
115155 error : ( { error } ) => {
116156 this . messageService . add ( { severity : 'error' , summary : 'Error' , detail : error . error } ) ;
117157 } ,
118158 } ) ;
119159 }
120160
161+ private performAutoJoin ( ) : void {
162+ // Double-check conditions before performing auto-join
163+ const authenticated = this . authenticated ( ) ;
164+ const user = this . user ( ) ;
165+ const canJoinMeeting = this . canJoinMeeting ( ) ;
166+ const hasAutoJoined = this . hasAutoJoined ( ) ;
167+ const meeting = this . meeting ( ) ;
168+
169+ if ( ! authenticated || ! user || ! user . email || ! canJoinMeeting || hasAutoJoined || ! meeting || ! meeting . uid || this . isJoining ( ) ) {
170+ return ; // Conditions no longer met, abort
171+ }
172+
173+ // Auto-joining meeting for authenticated user
174+
175+ // Mark as auto-joined immediately to prevent multiple attempts
176+ this . hasAutoJoined . set ( true ) ;
177+
178+ // Show a notification that we're auto-joining
179+ this . messageService . add ( {
180+ severity : 'info' ,
181+ summary : 'Auto-joining Meeting' ,
182+ detail : 'Automatically opening the meeting for you...' ,
183+ life : 3000 ,
184+ } ) ;
185+
186+ // If meeting has a direct join URL, use it
187+ if ( meeting . join_url ) {
188+ const joinUrlWithParams = this . buildJoinUrlWithParams ( meeting . join_url ) ;
189+ this . openMeetingSecurely ( joinUrlWithParams ) ;
190+ } else {
191+ // Otherwise, fetch the join URL first
192+ this . meetingService
193+ . getPublicMeetingJoinUrl ( meeting . uid , meeting . password , {
194+ email : user . email ,
195+ } )
196+ . subscribe ( {
197+ next : ( res ) => {
198+ if ( res . join_url ) {
199+ meeting . join_url = res . join_url ;
200+ const joinUrlWithParams = this . buildJoinUrlWithParams ( res . join_url ) ;
201+ this . openMeetingSecurely ( joinUrlWithParams ) ;
202+ } else {
203+ throw new Error ( 'No join URL received' ) ;
204+ }
205+ } ,
206+ error : ( ) => {
207+ this . messageService . add ( {
208+ severity : 'error' ,
209+ summary : 'Auto-join Failed' ,
210+ detail : 'Could not automatically join the meeting. Please use the Join Meeting button.' ,
211+ life : 5000 ,
212+ } ) ;
213+ // Reset auto-join flag so user can try manually
214+ this . hasAutoJoined . set ( false ) ;
215+ } ,
216+ } ) ;
217+ }
218+ }
219+
121220 private initializeMeeting ( ) {
122221 return toSignal < Meeting & { project : Project } > (
123222 combineLatest ( [ this . activatedRoute . paramMap , this . activatedRoute . queryParamMap ] ) . pipe (
@@ -305,6 +404,58 @@ export class MeetingJoinComponent {
305404 return `${ joinUrl } ?${ queryString } ` ;
306405 }
307406
407+ private initializeMessageSeverity ( ) : Signal < 'success' | 'info' | 'warn' > {
408+ return computed ( ( ) => {
409+ const hasAutoJoined = this . hasAutoJoined ( ) ;
410+ const canJoinMeeting = this . canJoinMeeting ( ) ;
411+
412+ if ( hasAutoJoined ) {
413+ return 'success' ;
414+ }
415+ if ( canJoinMeeting ) {
416+ return 'info' ;
417+ }
418+ return 'warn' ;
419+ } ) ;
420+ }
421+
422+ private initializeMessageIcon ( ) : Signal < string > {
423+ return computed ( ( ) => {
424+ const hasAutoJoined = this . hasAutoJoined ( ) ;
425+ const canJoinMeeting = this . canJoinMeeting ( ) ;
426+
427+ if ( hasAutoJoined ) {
428+ return 'fa-light fa-external-link' ;
429+ }
430+ if ( canJoinMeeting ) {
431+ return 'fa-light fa-check-circle' ;
432+ }
433+ return 'fa-light fa-clock' ;
434+ } ) ;
435+ }
436+
437+ private openMeetingSecurely ( url : string ) : void {
438+ // Try to open the meeting URL securely
439+ const newWindow = window . open ( url , '_blank' , 'noopener,noreferrer' ) ;
440+
441+ // Handle popup blocker scenarios
442+ if ( ! newWindow || newWindow . closed || typeof newWindow . closed === 'undefined' ) {
443+ // Popup was blocked, show user message with manual link
444+ this . messageService . add ( {
445+ severity : 'warn' ,
446+ summary : 'Popup Blocked' ,
447+ detail : 'Your browser blocked the meeting popup. Please allow popups for this site and try again, or click the Join Meeting button.' ,
448+ life : 8000 ,
449+ } ) ;
450+
451+ // Reset auto-join flag so user can try manually
452+ this . hasAutoJoined . set ( false ) ;
453+ } else {
454+ // Clear opener reference for security (prevent tabnabbing)
455+ newWindow . opener = null ;
456+ }
457+ }
458+
308459 private initializeAttachments ( ) : Signal < MeetingAttachment [ ] > {
309460 // Convert meeting signal to observable to react to changes
310461 return toSignal (
0 commit comments