Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,16 @@ <h4 class="font-medium text-gray-900 font-sans">{{ user.name }}</h4>
</div>

<lfx-message
[severity]="canJoinMeeting() ? 'info' : 'warn'"
[icon]="canJoinMeeting() ? 'fa-light fa-check-circle' : 'fa-light fa-clock'"
styleClass="flex items-center justify-between w-full">
[severity]="messageSeverity()"
[icon]="messageIcon()"
styleClass="flex items-center justify-between w-full"
[attr.data-testid]="'join-status-message'">
<ng-template #content>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-3 text-sm">
@if (canJoinMeeting()) {
@if (hasAutoJoined()) {
<span class="font-sans">Meeting opened in a new tab. If it didn't open, use the button to join manually.</span>
} @else if (canJoinMeeting()) {
<span class="font-sans">Ready to join as {{ user.name }}</span>
} @else {
<div class="flex flex-col">
Expand All @@ -236,14 +239,16 @@ <h4 class="font-medium text-gray-900 font-sans">{{ user.name }}</h4>
[disabled]="!canJoinMeeting()"
severity="primary"
label="Join Meeting"
icon="fa-light fa-sign-in"></lfx-button>
icon="fa-light fa-sign-in"
[attr.data-testid]="'join-meeting-button-authenticated'"></lfx-button>
} @else {
<lfx-button
size="small"
severity="primary"
label="Join Meeting"
[disabled]="!canJoinMeeting()"
icon="fa-light fa-sign-in"
[attr.data-testid]="'join-meeting-button-authenticated'"
(click)="onJoinMeeting()"></lfx-button>
}
</div>
Expand Down Expand Up @@ -345,14 +350,16 @@ <h4 class="font-medium text-gray-900 font-sans">Enter your information</h4>
severity="primary"
label="Join Meeting"
icon="fa-light fa-sign-in"
[disabled]="joinForm.invalid || !canJoinMeeting()"></lfx-button>
[disabled]="joinForm.invalid || !canJoinMeeting()"
[attr.data-testid]="'join-meeting-button-form'"></lfx-button>
} @else {
<lfx-button
size="small"
severity="primary"
label="Join Meeting"
[disabled]="joinForm.invalid || !canJoinMeeting()"
icon="fa-light fa-sign-in"
[attr.data-testid]="'join-meeting-button-form'"
(click)="onJoinMeeting()"></lfx-button>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { CommonModule } from '@angular/common';
import { HttpParams } from '@angular/common/http';
import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core';
import { Component, computed, effect, inject, OnDestroy, signal, Signal, WritableSignal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
Expand Down Expand Up @@ -46,7 +46,7 @@ import { catchError, combineLatest, finalize, map, of, switchMap, tap } from 'rx
providers: [],
templateUrl: './meeting-join.component.html',
})
export class MeetingJoinComponent {
export class MeetingJoinComponent implements OnDestroy {
// Injected services
private readonly messageService = inject(MessageService);
private readonly activatedRoute = inject(ActivatedRoute);
Expand All @@ -69,6 +69,10 @@ export class MeetingJoinComponent {
public canJoinMeeting: Signal<boolean>;
public joinUrlWithParams: Signal<string | undefined>;
public attachments: Signal<MeetingAttachment[]>;
public hasAutoJoined: WritableSignal<boolean> = signal<boolean>(false);
private autoJoinTimeout: ReturnType<typeof setTimeout> | null = null;
public messageSeverity: Signal<'success' | 'info' | 'warn'>;
public messageIcon: Signal<string>;

// Form value signals for reactivity
private formValues: Signal<{ name: string; email: string; organization: string }>;
Expand All @@ -87,6 +91,42 @@ export class MeetingJoinComponent {
this.canJoinMeeting = this.initializeCanJoinMeeting();
this.joinUrlWithParams = this.initializeJoinUrlWithParams();
this.attachments = this.initializeAttachments();
this.messageSeverity = this.initializeMessageSeverity();
this.messageIcon = this.initializeMessageIcon();

// Auto-join effect for signed-in users - use allowSignalWrites for state updates
effect(
() => {
const authenticated = this.authenticated();
const user = this.user();
const canJoinMeeting = this.canJoinMeeting();
const hasAutoJoined = this.hasAutoJoined();
const meeting = this.meeting();

// Clear any existing timeout
if (this.autoJoinTimeout) {
clearTimeout(this.autoJoinTimeout);
this.autoJoinTimeout = null;
}

// Schedule auto-join only if conditions are met
if (authenticated && user && user.email && canJoinMeeting && !hasAutoJoined && meeting && meeting.uid && !this.isJoining()) {
// Set a timeout to prevent rapid-fire execution
this.autoJoinTimeout = setTimeout(() => {
this.performAutoJoin();
}, 500); // Small delay to let all signals settle
}
},
{ allowSignalWrites: true }
);
}

public ngOnDestroy(): void {
// Cleanup timeout on component destroy
if (this.autoJoinTimeout) {
clearTimeout(this.autoJoinTimeout);
this.autoJoinTimeout = null;
}
}

public onJoinMeeting(): void {
Expand All @@ -110,14 +150,73 @@ export class MeetingJoinComponent {
next: (res) => {
this.meeting().join_url = res.join_url;
const joinUrlWithParams = this.buildJoinUrlWithParams(res.join_url);
window.open(joinUrlWithParams, '_blank');
this.openMeetingSecurely(joinUrlWithParams);
},
error: ({ error }) => {
this.messageService.add({ severity: 'error', summary: 'Error', detail: error.error });
},
});
}

private performAutoJoin(): void {
// Double-check conditions before performing auto-join
const authenticated = this.authenticated();
const user = this.user();
const canJoinMeeting = this.canJoinMeeting();
const hasAutoJoined = this.hasAutoJoined();
const meeting = this.meeting();

if (!authenticated || !user || !user.email || !canJoinMeeting || hasAutoJoined || !meeting || !meeting.uid || this.isJoining()) {
return; // Conditions no longer met, abort
}

// Auto-joining meeting for authenticated user

// Mark as auto-joined immediately to prevent multiple attempts
this.hasAutoJoined.set(true);

// Show a notification that we're auto-joining
this.messageService.add({
severity: 'info',
summary: 'Auto-joining Meeting',
detail: 'Automatically opening the meeting for you...',
life: 3000,
});

// If meeting has a direct join URL, use it
if (meeting.join_url) {
const joinUrlWithParams = this.buildJoinUrlWithParams(meeting.join_url);
this.openMeetingSecurely(joinUrlWithParams);
} else {
// Otherwise, fetch the join URL first
this.meetingService
.getPublicMeetingJoinUrl(meeting.uid, meeting.password, {
email: user.email,
})
.subscribe({
next: (res) => {
if (res.join_url) {
meeting.join_url = res.join_url;
const joinUrlWithParams = this.buildJoinUrlWithParams(res.join_url);
this.openMeetingSecurely(joinUrlWithParams);
} else {
throw new Error('No join URL received');
}
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Auto-join Failed',
detail: 'Could not automatically join the meeting. Please use the Join Meeting button.',
life: 5000,
});
// Reset auto-join flag so user can try manually
this.hasAutoJoined.set(false);
},
});
}
}

private initializeMeeting() {
return toSignal<Meeting & { project: Project }>(
combineLatest([this.activatedRoute.paramMap, this.activatedRoute.queryParamMap]).pipe(
Expand Down Expand Up @@ -305,6 +404,58 @@ export class MeetingJoinComponent {
return `${joinUrl}?${queryString}`;
}

private initializeMessageSeverity(): Signal<'success' | 'info' | 'warn'> {
return computed(() => {
const hasAutoJoined = this.hasAutoJoined();
const canJoinMeeting = this.canJoinMeeting();

if (hasAutoJoined) {
return 'success';
}
if (canJoinMeeting) {
return 'info';
}
return 'warn';
});
}

private initializeMessageIcon(): Signal<string> {
return computed(() => {
const hasAutoJoined = this.hasAutoJoined();
const canJoinMeeting = this.canJoinMeeting();

if (hasAutoJoined) {
return 'fa-light fa-external-link';
}
if (canJoinMeeting) {
return 'fa-light fa-check-circle';
}
return 'fa-light fa-clock';
});
}

private openMeetingSecurely(url: string): void {
// Try to open the meeting URL securely
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');

// Handle popup blocker scenarios
if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') {
// Popup was blocked, show user message with manual link
this.messageService.add({
severity: 'warn',
summary: 'Popup Blocked',
detail: 'Your browser blocked the meeting popup. Please allow popups for this site and try again, or click the Join Meeting button.',
life: 8000,
});

// Reset auto-join flag so user can try manually
this.hasAutoJoined.set(false);
} else {
// Clear opener reference for security (prevent tabnabbing)
newWindow.opener = null;
}
}

private initializeAttachments(): Signal<MeetingAttachment[]> {
// Convert meeting signal to observable to react to changes
return toSignal(
Expand Down