// Version: 1.1.0 | Created: 2026-04-05 | Updated: 2026-04-10 // Web implementation: uses JitsiMeetExternalAPI via dart:js_interop. // This file is web-only — guarded by the Flutter web build. // The external_api.js script is lazy-loaded on first use — not in index.html. // This prevents privacy browsers (Brave, Mullvad) from blocking the app // at startup due to a cross-origin script from conf.m8chat.au. import 'dart:async'; import 'dart:js_interop'; import 'dart:ui_web' as ui_web; import 'package:web/web.dart' as web; import '../../../core/config/app_config.dart'; /// Manages a single Jitsi iframe session via the JitsiMeetExternalAPI. class JitsiWebService { JitsiWebService._(); static final instance = JitsiWebService._(); JSObject? _api; bool _viewRegistered = false; bool _scriptLoaded = false; bool _scriptLoading = false; /// Unique view type for HtmlElementView. static const String viewType = 'jitsi-meet-container'; /// Lazy-loads the JitsiMeetExternalAPI script from conf.m8chat.au. /// Returns true if the script loaded successfully, false otherwise. Future _ensureScriptLoaded() async { if (_scriptLoaded) return true; if (_scriptLoading) { // Wait for in-flight load to finish. for (var i = 0; i < 50; i++) { await Future.delayed(const Duration(milliseconds: 100)); if (_scriptLoaded) return true; } return false; } _scriptLoading = true; final completer = Completer(); final script = web.document.createElement('script') as web.HTMLScriptElement; script.src = 'https://${AppConfig.jitsiDomain}/external_api.js'; script.async = true; script.onload = (web.Event event) { _scriptLoaded = true; _scriptLoading = false; completer.complete(true); }.toJS; script.onerror = (web.Event event) { _scriptLoading = false; completer.complete(false); }.toJS; web.document.head?.append(script); return completer.future; } /// Registers the platform view factory (once). void ensureViewRegistered() { if (_viewRegistered) return; ui_web.platformViewRegistry.registerViewFactory( viewType, (int viewId, {Object? params}) { final div = web.document.createElement('div') as web.HTMLDivElement; div.id = 'jitsi-container-$viewId'; div.style ..width = '100%' ..height = '100%'; return div; }, ); _viewRegistered = true; } /// Starts a Jitsi meeting inside the container div created by the platform view. /// Call this AFTER the HtmlElementView has been mounted (e.g. in a post-frame callback). /// Lazy-loads the Jitsi script on first call. Future joinMeeting({ required String roomName, String? jwt, String? displayName, String? avatarUrl, void Function()? onReadyToClose, }) async { dispose(); // tear down any previous meeting // Lazy-load the external_api.js script if not already present. final loaded = await _ensureScriptLoaded(); if (!loaded) return; // Find the container div — there should be exactly one with our prefix. final containers = web.document.querySelectorAll('[id^="jitsi-container-"]'); if (containers.length == 0) return; final parentNode = containers.item(containers.length - 1); if (parentNode == null) return; final configOverwrite = { 'startAudioMuted': 0, 'startVideoMuted': 0, 'disableDeepLinking': true, 'prejoinPageEnabled': true, }.jsify(); final interfaceConfigOverwrite = { 'SHOW_CHROME_EXTENSION_BANNER': false, }.jsify(); final options = { 'roomName': roomName, 'parentNode': parentNode, 'width': '100%', 'height': '100%', 'configOverwrite': configOverwrite, 'interfaceConfigOverwrite': interfaceConfigOverwrite, }; if (jwt != null && jwt.isNotEmpty) { options['jwt'] = jwt; } if (displayName != null && displayName.isNotEmpty) { options['userInfo'] = { 'displayName': displayName, if (avatarUrl != null) 'avatarUrl': avatarUrl, }.jsify(); } final jsOptions = options.jsify(); _api = _createJitsiApi(AppConfig.jitsiDomain.toJS, jsOptions as JSObject); if (onReadyToClose != null && _api != null) { _addEventListener(_api!, 'readyToClose'.toJS, () { onReadyToClose(); }.toJS); } } /// Cleans up the Jitsi iframe. void dispose() { if (_api != null) { _disposeApi(_api!); _api = null; } } } /// Calls `new JitsiMeetExternalAPI(domain, options)`. @JS('JitsiMeetExternalAPI') external JSObject _createJitsiApi(JSString domain, JSObject options); /// Calls `api.addEventListener(event, callback)`. @JS() extension type _JitsiApi(JSObject _) implements JSObject { external void addEventListener(JSString event, JSFunction callback); external void dispose(); } void _addEventListener(JSObject api, JSString event, JSFunction callback) { (api as _JitsiApi).addEventListener(event, callback); } void _disposeApi(JSObject api) { (api as _JitsiApi).dispose(); }