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
12 changes: 10 additions & 2 deletions apps/lfx-one/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,19 @@ export const routes: Routes = [
path: 'projects',
loadComponent: () => import('./modules/pages/home/home.component').then((m) => m.HomeComponent),
},
{
path: 'meetings',
loadChildren: () => import('./modules/meetings/meetings.routes').then((m) => m.MEETING_ROUTES),
},
],
},
{
path: 'meetings',
loadChildren: () => import('./modules/meeting/meeting.routes').then((m) => m.MEETING_ROUTES),
path: 'meetings/not-found',
loadComponent: () => import('./modules/meetings/meeting-not-found/meeting-not-found.component').then((m) => m.MeetingNotFoundComponent),
},
{
path: 'meetings/:id',
loadComponent: () => import('./modules/meetings/meeting-join/meeting-join.component').then((m) => m.MeetingJoinComponent),
},
{
path: 'project/:slug',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@ export class MainLayoutComponent {
routerLink: '/',
},
{
label: 'My Meetings',
label: 'Meetings',
icon: 'fa-light fa-video',
routerLink: '/my-meetings',
disabled: true,
routerLink: '/meetings',
},
{
label: 'Project Health',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ <h2 class="font-display font-semibold text-gray-900">My Meetings</h2>
label="View All"
icon="fa-light fa-chevron-right"
iconPos="right"
(onClick)="handleViewAll()"
[routerLink]="['/meetings']"
styleClass="!text-sm !font-normal"
[text]="true"
size="small"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,4 @@ export class MyMeetingsComponent {
// Sort by earliest time first and limit to 5
return meetings.sort((a, b) => a.sortTime - b.sortTime).slice(0, 5);
});

public handleViewAll(): void {
this.router.navigate(['/meetings']);
}
}
15 changes: 0 additions & 15 deletions apps/lfx-one/src/app/modules/meeting/meeting.routes.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,16 @@ import { ExpandableTextComponent } from '@components/expandable-text/expandable-
import { InputTextComponent } from '@components/input-text/input-text.component';
import { MessageComponent } from '@components/message/message.component';
import { environment } from '@environments/environment';
import { extractUrlsWithDomains, getCurrentOrNextOccurrence, Meeting, MeetingAttachment, MeetingOccurrence, Project, User } from '@lfx-one/shared';
import {
canJoinMeeting,
extractUrlsWithDomains,
getCurrentOrNextOccurrence,
Meeting,
MeetingAttachment,
MeetingOccurrence,
Project,
User,
} from '@lfx-one/shared';
import { FileSizePipe } from '@pipes/file-size.pipe';
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
import { MeetingService } from '@services/meeting.service';
Expand Down Expand Up @@ -122,8 +131,9 @@ export class MeetingJoinComponent {
if (typeof window !== 'undefined' && url) {
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');

// Check if popup was blocked
if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') {
// With noopener, window.open() may return null even on success
// Only check if window.closed is explicitly true to detect popup blocking
if (newWindow !== null && newWindow.closed) {
// Popup was blocked
this.messageService.add({
severity: 'warn',
Expand All @@ -132,11 +142,11 @@ export class MeetingJoinComponent {
life: 5000,
});
} else {
// Popup opened successfully
// Window opened (or likely opened with noopener returning null)
this.messageService.add({
severity: 'success',
summary: 'Meeting Opened',
detail: 'The meeting has been opened in a new tab.',
detail: "The meeting has been opened in a new tab. If you don't see it, check if popups are blocked.",
life: 3000,
});
}
Expand Down Expand Up @@ -257,32 +267,7 @@ export class MeetingJoinComponent {

private initializeCanJoinMeeting(): Signal<boolean> {
return computed(() => {
const meeting = this.meeting();
const currentOccurrence = this.currentOccurrence();

// If we have an occurrence, use its timing
if (currentOccurrence) {
const now = new Date();
const startTime = new Date(currentOccurrence.start_time);
const earlyJoinMinutes = meeting.early_join_time_minutes || 10;
const earliestJoinTime = new Date(startTime.getTime() - earlyJoinMinutes * 60000);
const latestJoinTime = new Date(startTime.getTime() + currentOccurrence.duration * 60000 + 40 * 60000); // 40 minutes after end

return now >= earliestJoinTime && now <= latestJoinTime;
}

// Fallback to original meeting logic if no occurrences
if (!meeting?.start_time) {
return false;
}

const now = new Date();
const startTime = new Date(meeting.start_time);
const earlyJoinMinutes = meeting.early_join_time_minutes || 10; // Default to 10 minutes
const earliestJoinTime = new Date(startTime.getTime() - earlyJoinMinutes * 60000);
const latestJoinTime = new Date(startTime.getTime() + meeting.duration * 60000 + 40 * 60000); // 40 minutes after end

return now >= earliestJoinTime && now <= latestJoinTime;
return canJoinMeeting(this.meeting(), this.currentOccurrence());
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<div class="flex flex-wrap items-center gap-4">
<div class="flex-1 min-w-[300px]">
<div class="bg-slate-50 relative rounded-[12px]">
<div class="flex flex-row items-center overflow-clip rounded-[inherit] size-full">
<div class="box-border content-stretch flex gap-[8px] items-center px-[12px] py-[8px] relative w-full">
<i class="fa-light fa-magnifying-glass w-4 h-4 text-[#90a1b9] shrink-0"></i>
<input
type="text"
[value]="searchQuery()"
(input)="onSearchInput($event)"
placeholder="Search meetings..."
class="font-sans font-normal leading-[normal] not-italic flex-1 bg-transparent border-0 outline-none text-[#90a1b9] text-[14px] placeholder:text-[#90a1b9]" />
</div>
</div>
<div aria-hidden="true" class="absolute border-[#cad5e2] border-[0.5px] border-solid inset-0 pointer-events-none rounded-[12px]"></div>
</div>
</div>

<div [ngClass]="{ 'opacity-50': isCalendarView }" class="inline-flex items-center bg-slate-100 rounded-lg p-0.5 gap-0.5">
<button
(click)="onTimeFilterClick('upcoming')"
[disabled]="isCalendarView"
[ngClass]="{
'bg-white text-slate-900 shadow-sm': timeFilter() === 'upcoming',
'text-slate-600 hover:text-slate-900': timeFilter() !== 'upcoming',
'cursor-not-allowed': isCalendarView,
}"
class="px-3 py-1.5 rounded-md text-[12.25px] transition-all duration-200">
Upcoming ({{ upcomingCount() }})
</button>
<button
(click)="onTimeFilterClick('past')"
[disabled]="isCalendarView"
[ngClass]="{
'bg-white text-slate-900 shadow-sm': timeFilter() === 'past',
'text-slate-600 hover:text-slate-900': timeFilter() !== 'past',
'cursor-not-allowed': isCalendarView,
}"
class="px-3 py-1.5 rounded-md text-[12.25px] transition-all duration-200">
Past ({{ pastCount() }})
</button>
</div>

<!-- TODO: Readd when in scope to filter by visibility -->
<!--
<div class="inline-flex items-center bg-slate-100 rounded-lg p-0.5 gap-0.5">
<button
(click)="onVisibilityFilterClick('mine')"
[ngClass]="{
'bg-white text-slate-900 shadow-sm': visibilityFilter() === 'mine',
'text-slate-600 hover:text-slate-900': visibilityFilter() !== 'mine',
}"
class="px-3 py-1.5 rounded-md text-[12.25px] transition-all duration-200">
Mine ({{ mineCount() }})
</button>
<button
(click)="onVisibilityFilterClick('public')"
[ngClass]="{
'bg-white text-slate-900 shadow-sm': visibilityFilter() === 'public',
'text-slate-600 hover:text-slate-900': visibilityFilter() !== 'public',
}"
class="px-3 py-1.5 rounded-md text-[12.25px] transition-all duration-200">
Public ({{ publicCount() }})
</button>
</div>
-->
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { Component, computed, input, model, Signal } from '@angular/core';
import { Meeting } from '@lfx-one/shared/interfaces';

@Component({
selector: 'lfx-meetings-top-bar',
standalone: true,
imports: [CommonModule],
templateUrl: './meetings-top-bar.component.html',
})
export class MeetingsTopBarComponent {
public searchQuery = model.required<string>();
public timeFilter = model.required<'upcoming' | 'past'>();
public visibilityFilter = model.required<'mine' | 'public'>();
public meetings = input.required<Meeting[]>();
public viewMode = input<'list' | 'calendar'>('list');

public upcomingCount: Signal<number>;
public pastCount: Signal<number>;
public mineCount: Signal<number>;
public publicCount: Signal<number>;

public constructor() {
this.upcomingCount = this.initializeUpcomingCount();
this.pastCount = this.initializePastCount();
this.mineCount = this.initializeMineCount();
this.publicCount = this.initializePublicCount();
}

public onSearchInput(event: Event): void {
const value = (event.target as HTMLInputElement).value;
this.searchQuery.set(value);
}

public onTimeFilterClick(value: 'upcoming' | 'past'): void {
if (this.viewMode() !== 'calendar') {
this.timeFilter.set(value);
}
}

public onVisibilityFilterClick(value: 'mine' | 'public'): void {
this.visibilityFilter.set(value);
}

public get isCalendarView(): boolean {
return this.viewMode() === 'calendar';
}

private initializeUpcomingCount(): Signal<number> {
return computed(() => {
const now = new Date();
return this.meetings().filter((meeting) => {
const meetingEndTime = new Date(meeting.start_time);
meetingEndTime.setMinutes(meetingEndTime.getMinutes() + meeting.duration + 40);
return meetingEndTime >= now;
}).length;
});
}

private initializePastCount(): Signal<number> {
return computed(() => {
const now = new Date();
return this.meetings().filter((meeting) => {
const meetingEndTime = new Date(meeting.start_time);
meetingEndTime.setMinutes(meetingEndTime.getMinutes() + meeting.duration + 40);
return meetingEndTime < now;
}).length;
});
}

private initializeMineCount(): Signal<number> {
return computed(() => {
return this.meetings().filter((meeting) => meeting.visibility?.toLowerCase() === 'private').length;
});
}

private initializePublicCount(): Signal<number> {
return computed(() => {
return this.meetings().filter((meeting) => meeting.visibility?.toLowerCase() === 'public').length;
});
}
}
Loading