Automatic and Manual Event Ending#417
Conversation
Only conflict in client/lib/features/admin/presentation/views/data_tab.dart
- Verify meetingEndedAt is written and email sent - Verify idempotency (second call is a no-op) - Verify non-mod/admin/owner is rejected
- Auto-leave when meetingEndedAt is set on LiveMeeting doc - Add endMeetingForAll to CloudFunctionsEventService - Add End Meeting menu item in admin panel for mods/admins/owners - Show confirmation dialog before ending - Add l10n strings in all 4 ARB files (en, es, zh, zh_Hant_TW)
- Add ScheduledEndMeeting OnRequestMethod CF with idempotency guard - Add isFirstJoin flag to MeetingJoinResult - Schedule end task on first join when event has scheduledTime + duration - Register ScheduledEndMeeting in main.dart
- Verify meetingEndedAt is written and email sent - Verify idempotency (second call is a no-op)
# Conflicts: # client/lib/features/admin/presentation/views/data_tab.dart
- Add optional endTime parameter to VerticalTimeAndDateIndicator - Show time range (e.g. '2:30p - 3:30p') when endTime is provided - Pass calculated endTime (scheduledTime + durationInMinutes) at all 5 call sites: event_card, carousel_tabs, home_page_event_card, event_button, event_info
- Verify start-only display - Verify start-end range display
data_models/build.sh + firebase/functions/build.sh -> build-all.sh Expanded script to cover all three packages (client, data_models, firebase/functions) and use the FVM-pinned Flutter/Dart SDK instead of global binaries, fixing kernel binary format mismatch errors when the global SDK version differs from the repo pin.
Resolves kernel binary format mismatch errors caused by global Flutter/Dart differing from the repo-pinned version. All flutter/dart invocations now prefer client/.fvm/flutter_sdk/bin, with fallback to global. Also clears build stamps when the active SDK fingerprint changes between runs.
- Add meetingEndedAt (DateTime?) and kFieldMeetingEndedAt to LiveMeeting - Add EndMeetingForAllRequest to data_models requests - Break out shared recording-stop logic into stop_all_event_recordings.dart - Refactor EventEnded CF to use shared helper - Add EndMeetingForAll callable CF with mod/admin/owner auth check, idempotency guard, recording stop, and post-event email - Register EndMeetingForAll in main.dart
|
Visit the preview URL for this PR (updated for commit f696265): https://gen-hls-bkc-7627--pr417-mw-feat-event-end-y4w13zyo.web.app (expires Thu, 21 May 2026 00:07:37 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: eed668cca81618d491d024574a8f8a6003deaa8d |
There was a problem hiding this comment.
Pull request overview
This PR introduces a host-initiated and scheduled mechanism to end live meetings, including persisting an “ended” marker on the LiveMeeting document, stopping active recordings, and notifying participants. It also updates client UI to display event end times and exposes “End Meeting” controls, plus adds dev/build script improvements.
Changes:
- Adds Cloud Functions to end a meeting for all participants (manual, host/mod/admin/owner) and to end it automatically at a scheduled time (Cloud Tasks).
- Persists
meetingEndedAtinLiveMeeting, blocks new joins after end, and auto-leaves clients when the meeting is ended. - Updates UI to show time ranges (start–end), adds event settings for auto-end + grace period, and adds build/dev scripting improvements.
Reviewed changes
Copilot reviewed 40 out of 40 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| run-dev.sh | Uses FVM Flutter/Dart when available; clears build stamps when SDK fingerprint changes. |
| package.json | Adds build script entrypoint. |
| build-all.sh | New build script to run pub get/build steps across client/data_models/functions. |
| firebase/functions/node/main.dart | Registers new EndMeetingForAll (onCall) and ScheduledEndMeeting (onRequest) functions. |
| firebase/functions/lib/events/live_meetings/end_meeting_for_all.dart | Implements callable “end meeting for all” flow (writes meetingEndedAt, stops recordings, emails participants). |
| firebase/functions/lib/events/live_meetings/scheduled_end_meeting.dart | Implements scheduled HTTP function to end meeting at a given time. |
| firebase/functions/lib/events/live_meetings/stop_all_event_recordings.dart | Extracts shared logic to stop main + breakout recordings. |
| firebase/functions/lib/events/live_meetings/get_meeting_join_info.dart | Rejects joining ended meetings; schedules auto-end task on first join. |
| firebase/functions/lib/events/live_meetings/live_meeting_utils.dart | Returns isFirstJoin flag to support first-join scheduling. |
| firebase/functions/lib/events/event_ended.dart | Reuses shared “stop all recordings” helper. |
| firebase/functions/test/events/live_meetings/end_meeting_for_all_test.dart | Adds tests for manual end meeting behavior, idempotency, and authorization. |
| firebase/functions/test/events/live_meetings/scheduled_end_meeting_test.dart | Adds tests for scheduled meeting end behavior and idempotency. |
| firebase/functions/test/events/live_meetings/get_meeting_join_info_test.dart | Adds test that join is rejected if meetingEndedAt is set. |
| data_models/lib/events/live_meetings/live_meeting.dart | Adds meetingEndedAt field + Firestore key constant. |
| data_models/lib/events/live_meetings/live_meeting.g.dart | Generated JSON serialization updates for meetingEndedAt. |
| data_models/lib/events/live_meetings/live_meeting.freezed.dart | Generated Freezed updates for meetingEndedAt. |
| data_models/lib/events/event.dart | Adds autoEndMeeting + autoEndGracePeriodMinutes settings keys/defaults. |
| data_models/lib/events/event.g.dart | Generated JSON serialization updates for new EventSettings fields. |
| data_models/lib/events/event.freezed.dart | Generated Freezed updates for new EventSettings fields. |
| data_models/lib/cloud_functions/requests.dart | Adds EndMeetingForAllRequest. |
| data_models/lib/cloud_functions/requests.g.dart | Generated JSON serialization for EndMeetingForAllRequest. |
| data_models/lib/cloud_functions/requests.freezed.dart | Generated Freezed code for EndMeetingForAllRequest. |
| client/lib/features/events/data/services/cloud_functions_event_service.dart | Adds client call for endMeetingForAll. |
| client/lib/features/events/features/live_meeting/features/admin_panel/presentation/widgets/admin_panel.dart | Adds “End Meeting” menu item + confirmation dialog. |
| client/lib/features/events/features/live_meetings/data/providers/live_meeting_provider.dart | Auto-leaves when meeting ended; prompts host/admin to optionally end for all when leaving. |
| client/lib/features/events/features/event_page/presentation/views/event_settings_drawer.dart | Adds UI for auto-end meeting toggle + grace period input. |
| client/lib/features/events/features/event_page/presentation/event_settings_presenter.dart | Adds updateSettingValue to support non-bool settings updates. |
| client/lib/features/community/presentation/widgets/carousel/time_indicator.dart | Displays time range when endTime is provided. |
| client/test/lib/features/community/presentation/widgets/carousel/time_indicator_test.dart | Adds widget tests for time-range display behavior. |
| client/lib/features/community/presentation/widgets/carousel/carousel_tabs.dart | Supplies endTime to time indicator in carousel. |
| client/lib/features/community/presentation/widgets/event_card.dart | Supplies endTime to time indicator. |
| client/lib/features/home/presentation/views/home_page_event_card.dart | Supplies endTime to time indicator. |
| client/lib/features/events/presentation/widgets/event_button.dart | Supplies endTime to time indicator. |
| client/lib/features/events/features/event_page/presentation/widgets/event_info.dart | Supplies endTime to time indicator. |
| client/lib/l10n/app_en.arb | Adds endMeeting and endMeetingConfirmation strings. |
| client/lib/l10n/app_es.arb | Adds endMeeting and endMeetingConfirmation strings. |
| client/lib/l10n/app_zh.arb | Adds endMeeting and endMeetingConfirmation strings. |
| client/lib/l10n/app_zh_Hant_TW.arb | Adds endMeeting and endMeetingConfirmation strings. |
| firebase/functions/build.sh | Removes old functions build script. |
| data_models/build.sh | Removes old data_models build script. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| test('sets meetingEndedAt and sends email', () async { | ||
| final event = await createTestEvent(); | ||
| registerFallbackValue(event); | ||
| registerFallbackValue(EventEmailType.ended); | ||
|
|
…om the bool check in Settings display
e44572e to
cb49d91
Compare
- Move date into own line below event title - Enlarge event picture and justify left - Add missing label for message participants button - Locate that button with the other event control buttons (cancel and add to calendar), next to the share buttons because it's semantically related - Emphasize that button mainly for aesthetics but also because it's the most common action (cal add is probably only done once, canceling the event is not generally expected, want to encourage messaging through Frankly platform)
Specifically, mini refactor to use it for the calendar_menu_button.
dariusk
left a comment
There was a problem hiding this comment.
This generally looks great! Love to see the debounces and clamps and all that bread and butter logic flow stuff.
I have more specific comments inline, particularly with some instructions on putting in Matomo hooks for analytics.
🫡 Been a pleasure serving with you!!
| type: ActionButtonType.outline, | ||
| color: context.theme.colorScheme.surfaceContainerLowest, | ||
| icon: Icon( | ||
| CupertinoIcons.calendar_badge_plus, |
There was a problem hiding this comment.
I thought we were moving away from CupertinoIcons? This was my understanding from working on #223
There was a problem hiding this comment.
Yes, this is true. Please use Icons (Material Icons) for consistency!
| type: ActionButtonType.outline, | ||
| color: context.theme.colorScheme.surfaceContainerLowest, | ||
| icon: Icon( | ||
| CupertinoIcons.paperplane, |
There was a problem hiding this comment.
See above on CupertinoIcons
| size: 24, | ||
| color: context.theme.colorScheme.onSurfaceVariant, | ||
| ), | ||
| text: 'Message participants', |
There was a problem hiding this comment.
Should be a localized string
| final endDateTime = scheduledTime.add( | ||
| Duration(minutes: eventProvider.event.durationInMinutes), | ||
| ); | ||
| final endFmt = DateFormat('h:mma').format(endDateTime); |
There was a problem hiding this comment.
Could we possibly reuse some of the time formatting code you add in time_indicator.dart here?
| ], | ||
| ], | ||
| ), | ||
| if (canCancelParticipation || _canEditEvent) ...[ SizedBox(height: 4), |
There was a problem hiding this comment.
Strange whitespace needs fixing!
| } | ||
| transaction.update( | ||
| liveMeetingRef, | ||
| UpdateData.fromMap({ |
There was a problem hiding this comment.
I think right around here we need some hooks for the Matomo API. Basically, wherever we have the call to set the meeting end time (or if it's in two places put it in both places), we should put code close to or identical to what's here:
and remove the linked code from meeting_agenda_provider.dart. The linked code is from an attempt to capture a "Complete Event" event for Matomo analytics. It logs 1) that the Frankly event ended, and 2) how many seconds long the event was. However, the linked code was written back before we really had a concept of "meeting ended" so it's really scattershot and only triggers sometimes. Deleting that code and dropping it in here (with a little math to calculate duration) would be ideal.
If we get this working right we'll be able to see in Matomo a list of anonymized meeting IDs alongside duration of the meeting, like in the following screenshot but with more than just the top entry working correctly
There was a problem hiding this comment.
Oh yeah, probably also the hook needs to go in firebase/functions/lib/events/live_meetings/scheduled_end_meeting.dart
|
|
||
| // On first join, schedule automatic meeting end if the event has | ||
| // autoEndMeeting enabled, a scheduled time, and a duration. | ||
| if (result.isFirstJoin) { |
There was a problem hiding this comment.
This whole conditional branch pleases me to look at
Automatic and Manual Event Ending
What is in this PR?
Establish and test a consistent mechanism for ending an event manually and at a particular time. Enable the host to close all meeting rooms manually with a button or automatically at end time (with an optional grace period before closing all rooms). Add and test the UI for this functionality, and limit the grace period to an integer number of minutes between 0 (default) and 120.
Changes in the codebase
General Approach
The core concept is a
meetingEndedAttimestamp on theLiveMeetingdocument. Setting this field triggers two things: (1) the client detects the change via its live meeting stream and auto-leaves, and (2) server-side cleanup runs (stop recordings, send "event ended" email).Two server paths can set
meetingEndedAt:endMeetingForAllCloud Function. Authorization checks confirm the caller is the event creator or a community mod/admin/owner.GetMeetingJoinInfoschedules a Cloud Task forscheduledTime + durationInMinutes + gracePeriod. When the task fires,ScheduledEndMeetingsetsmeetingEndedAtvia the same shared core logic.Both paths use a Firestore transaction for the idempotency guard: if
meetingEndedAtis already set, the operation is a no-op. This prevents duplicate emails and recording stops even if the manual and scheduled paths race.On the client, the event settings drawer exposes the
autoEndMeetingtoggle and a grace period input field. The grace period is validated client-side (0-120 integer, with debounced auto-correction) and clamped server-side as a defense-in-depth measure.Non-obvious choices
Grace period input uses a managed
TextEditingControllerrather thanTextFormFieldwithinitialValue. The naive approach (using aValueKeykeyed to the model value) causes Flutter to destroy and recreate theTextFormFieldwidget on every keystroke, becauseonChanged->updateSettingValue->setStatechanges the key. This also unfocuses the field. The managed controller approach avoids this by keeping the controller alive across rebuilds, with a_gracePeriodEditingflag to preventupdateViewfrom overwriting the text while the user is typing.Auto-end task scheduling is best-effort (try/catch). A Cloud Tasks failure during
GetMeetingJoinInfowould otherwise block the user from joining the meeting. The task is non-critical -- the host can always end manually.ScheduledEndMeetingvalidates theX-CloudTasks-TaskNameheader. Cloud Functions strips this header from external HTTP requests, so its presence confirms the call originated from a Cloud Tasks queue. This prevents unauthorized callers from ending meetings via the raw HTTP endpoint.Recording stops and email delivery run in parallel. These are independent operations. Running them sequentially risked approaching the Cloud Function timeout for events with many breakout rooms and gives inconsistent user experience (not that it really matters in this case but polish is nice).
Dialog dismiss is treated as cancel in the host leave prompt. On my first pass, dismissing the "End for all?" dialog (tapping outside) forced the result to
falseand silently left the meeting. Now it cancels the leave action entirely, requiring an explicit choice.Changes by Commit
7a3b4310-- AddmeetingEndedAtfield toLiveMeeting, createendMeetingForAllCF with auth checks, extract recording stop logic intostop_all_event_recordings.dart34cc52d1-- Add unit tests forEndMeetingForAllCFb190f08a-- Client reacts tomeetingEndedAtchanges (auto-leave), wire upendMeetingForAllcall, add "End Meeting" button in admin panel547212af-- AddScheduledEndMeetingCF (Cloud Tasks HTTP handler), schedule auto-end on first join inGetMeetingJoinInfoc4c34713-- Add unit tests forScheduledEndMeetingCF29107097-- Comment_postEventEmailThresholdInMinutesexplaining its purpose887a9e1c-- Display end time alongside start time in event UI (carousel, event cards, event info)4fc78c4d-- Add widget tests forVerticalTimeAndDateIndicatorend time display5992828e-- Reject join attempts on ended meetings (server-side check inGetMeetingJoinInfo)aa1fde93-- Add test for join rejection on ended meetingsb2ef50a8-- Add host leave prompt ("End for all?" dialog before leaving)62bf5e14-- AddautoEndMeetingandautoEndGracePeriodMinutesfields toEventSettings, add toggle + grace period input to event settings drawer, wire into task schedulingb42c2bd5-- Fix settings type error by filtering non-bool values from the dev settings bool display4e27021f-- Wrap themeetingEndedAtread-then-write in a Firestore transaction for atomic idempotency (bothEndMeetingForAllandScheduledEndMeeting)9a3b0c7b-- Remove deaddisplayNameresolution code and unused imports fromGetMeetingJoinInfo2a3df808-- Pass event from the transaction result viaMeetingJoinResultinstead of re-reading it from Firestore97ee210c-- Wrap auto-end task scheduling in try/catch so join is not blocked by Cloud Tasks failures09e4eb52-- Localize hard-coded strings in leave prompt and event settings (ARB files)0ed29a9b-- ValidateX-CloudTasks-TaskNameheader inScheduledEndMeetingto prevent unauthorized external callsb42c8d3f-- Add email verification to scheduled end meeting test295e7db5-- Clamp grace period to 0-120 minutes server-side inGetMeetingJoinInfob56e9a01-- Add grace period input validation and auto-correction (managed controller, debounce, red error styling)e428a3fe-- Extract shared end meeting core logic (transaction, recording stop, email) intoend_meeting_core.dart, called by both CFs5f89aa07-- Treat dialog dismiss as cancel in host leave promptaafef468-- Parallelize breakout room recording stops withFuture.wait72f7f3e4-- Run recording stops in parallel with email delivery in end meeting core0938e944-- UsepermissionDeniedinstead offailedPreconditionfor auth errors (better error categorization in logs)a978fdcb-- Short-circuit auth check for event creator inendMeetingForAll(skip membership lookup when the caller is the host)d3b0dcd9-- Guard against double-tap on leave with_leavingInProgressflag3b6f104d-- Add widget tests for grace period input validation and auto-correctioncb49d913-- Add comments explaining the validation schemeChanges outside the codebase
No infrastructure or environment changes. Cloud Tasks is already enabled in the project. The
ScheduledEndMeetingfunction is registered inmain.dartand deployed alongside other Cloud Functions.Testing this PR
Automated tests:
EndMeetingForAllCF unit tests (end_meeting_for_all_test.dart) -- covers auth, idempotency, recording stop, email sendScheduledEndMeetingCF unit tests (scheduled_end_meeting_test.dart) -- covers execution, idempotency, email verifyGetMeetingJoinInfotest for join rejection on ended meetingsVerticalTimeAndDateIndicatorRun CF tests:
Run client tests:
Manual testing checklist:
Additional information
The
meetingEndedAtfield is write-once. Once set, there is no mechanism to "restart" a meeting. This is intentional -- ending is a final action confirmed by a dialog.The grace period field is clamped to 0-120 on both client and server. The server-side clamp in
GetMeetingJoinInfois an extra defense (and good hygeine in case of unvalidated input attacks + is coupled with authentication); the client-side validation is the primary UX.The
ScheduledEndMeetingCloud Function is an HTTP handler (not a callable) because Cloud Tasks dispatches via HTTP POST. TheX-CloudTasks-TaskNameheader check is the auth mechanism -- Cloud Functions infrastructure strips this header from external requests.