Skip to content

Commit 4832f5c

Browse files
authored
Merge pull request #122 from linuxfoundation/jme/LFXV2-646
feat(meeting-join): implement automatic meeting join for authenticate…
2 parents 6011e9a + a4b8369 commit 4832f5c

File tree

2 files changed

+167
-9
lines changed

2 files changed

+167
-9
lines changed

apps/lfx-one/src/app/modules/meeting/meeting-join/meeting-join.component.html

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,16 @@ <h4 class="font-medium text-gray-900 font-sans">{{ user.name }}</h4>
211211
</div>
212212

213213
<lfx-message
214-
[severity]="canJoinMeeting() ? 'info' : 'warn'"
215-
[icon]="canJoinMeeting() ? 'fa-light fa-check-circle' : 'fa-light fa-clock'"
216-
styleClass="flex items-center justify-between w-full">
214+
[severity]="messageSeverity()"
215+
[icon]="messageIcon()"
216+
styleClass="flex items-center justify-between w-full"
217+
[attr.data-testid]="'join-status-message'">
217218
<ng-template #content>
218219
<div class="flex items-center justify-between w-full">
219220
<div class="flex items-center gap-3 text-sm">
220-
@if (canJoinMeeting()) {
221+
@if (hasAutoJoined()) {
222+
<span class="font-sans">Meeting opened in a new tab. If it didn't open, use the button to join manually.</span>
223+
} @else if (canJoinMeeting()) {
221224
<span class="font-sans">Ready to join as {{ user.name }}</span>
222225
} @else {
223226
<div class="flex flex-col">
@@ -236,14 +239,16 @@ <h4 class="font-medium text-gray-900 font-sans">{{ user.name }}</h4>
236239
[disabled]="!canJoinMeeting()"
237240
severity="primary"
238241
label="Join Meeting"
239-
icon="fa-light fa-sign-in"></lfx-button>
242+
icon="fa-light fa-sign-in"
243+
[attr.data-testid]="'join-meeting-button-authenticated'"></lfx-button>
240244
} @else {
241245
<lfx-button
242246
size="small"
243247
severity="primary"
244248
label="Join Meeting"
245249
[disabled]="!canJoinMeeting()"
246250
icon="fa-light fa-sign-in"
251+
[attr.data-testid]="'join-meeting-button-authenticated'"
247252
(click)="onJoinMeeting()"></lfx-button>
248253
}
249254
</div>
@@ -345,14 +350,16 @@ <h4 class="font-medium text-gray-900 font-sans">Enter your information</h4>
345350
severity="primary"
346351
label="Join Meeting"
347352
icon="fa-light fa-sign-in"
348-
[disabled]="joinForm.invalid || !canJoinMeeting()"></lfx-button>
353+
[disabled]="joinForm.invalid || !canJoinMeeting()"
354+
[attr.data-testid]="'join-meeting-button-form'"></lfx-button>
349355
} @else {
350356
<lfx-button
351357
size="small"
352358
severity="primary"
353359
label="Join Meeting"
354360
[disabled]="joinForm.invalid || !canJoinMeeting()"
355361
icon="fa-light fa-sign-in"
362+
[attr.data-testid]="'join-meeting-button-form'"
356363
(click)="onJoinMeeting()"></lfx-button>
357364
}
358365
</div>

apps/lfx-one/src/app/modules/meeting/meeting-join/meeting-join.component.ts

Lines changed: 154 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { CommonModule } from '@angular/common';
55
import { 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';
77
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
88
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
99
import { 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

Comments
 (0)