Merge pull request #2741 from krille-chan/krille/implement-jitsi

feat: Implement experimental jitsi group calls behind a feature flag
This commit is contained in:
Krille-chan 2026-03-21 11:38:20 +01:00 committed by GitHub
commit c94b4ebe47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 202 additions and 12 deletions

View file

@ -40,6 +40,8 @@ enum AppSettings<T> {
showPresences<bool>('chat.fluffy.show_presences', true),
displayNavigationRail<bool>('chat.fluffy.display_navigation_rail', false),
experimentalVoip<bool>('chat.fluffy.experimental_voip', false),
jitsiFeature<bool>('chat.fluffy.enable_jitsi', false),
jitsiDomain<String>('chat.fluffy.jitsi_domain', 'meet.jit.si'),
shareKeysWith<String>('chat.fluffy.share_keys_with_2', 'all'),
noEncryptionWarningShown<bool>(
'chat.fluffy.no_encryption_warning_shown',

View file

@ -2807,4 +2807,4 @@
"skipSupportingFluffyChat": "FluffyChat unterstützen überspringen",
"iDoNotWantToSupport": "Ich möchte nicht unterstützen",
"iAlreadySupportFluffyChat": "I unterstütze FluffyChat bereits"
}
}

View file

@ -2789,5 +2789,13 @@
"iDoNotWantToSupport": "I do not want to support",
"iAlreadySupportFluffyChat": "I already support FluffyChat",
"setLowPriority": "Set low priority",
"unsetLowPriority": "Unset low priority"
}
"unsetLowPriority": "Unset low priority",
"removeCallFromChat": "Remove call from chat",
"removeCallFromChatDescription": "Do you want to remove the call from the chat for all members?",
"removeCallForEveryone": "Remove call for everyone",
"startVoiceCall": "Start voice call",
"startVideoCall": "Start video call",
"joinVoiceCall": "Join voice call",
"joinVideoCall": "Join video call",
"live": "Live"
}

View file

@ -2801,4 +2801,4 @@
"iDoNotWantToSupport": "Ma ei soovi toetada",
"setLowPriority": "Märgi vähetähtsaks",
"unsetLowPriority": "Eemalda märkimine vähetähtsaks"
}
}

View file

@ -2807,4 +2807,4 @@
"iAlreadySupportFluffyChat": "Tacaím le FluffyChat cheana féin",
"setLowPriority": "Socraigh tosaíocht íseal",
"unsetLowPriority": "Díshuiteáil tosaíocht íseal"
}
}

View file

@ -2801,4 +2801,4 @@
"iAlreadySupportFluffyChat": "Xa apoiei a FluffyChat",
"setLowPriority": "Establecer prioridade baixa",
"unsetLowPriority": "Non establecer prioridade baixa"
}
}

View file

@ -2808,4 +2808,4 @@
"iAlreadySupportFluffyChat": "Jeg støtter allerede FluffyChat",
"setLowPriority": "Sett lav prioritet",
"unsetLowPriority": "Fjern lav prioritet"
}
}

View file

@ -2800,4 +2800,4 @@
"support": "Steunen",
"setLowPriority": "Lage prioriteit instellen",
"unsetLowPriority": "Lage prioriteit uitschakelen"
}
}

View file

@ -2801,4 +2801,4 @@
"iAlreadySupportFluffyChat": "我已支持 FluffyChat",
"setLowPriority": "设置低优先级",
"unsetLowPriority": "取消设置低优先级"
}
}

View file

@ -14,6 +14,7 @@ import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
import 'package:fluffychat/pages/chat/chat_event_list.dart';
import 'package:fluffychat/pages/chat/encryption_button.dart';
import 'package:fluffychat/pages/chat/jitsi_popup_button.dart';
import 'package:fluffychat/pages/chat/pinned_events.dart';
import 'package:fluffychat/pages/chat/reply_display.dart';
import 'package:fluffychat/utils/account_config.dart';
@ -224,14 +225,16 @@ class ChatView extends StatelessWidget {
],
),
] else if (!controller.room.isArchived) ...[
if (AppSettings.experimentalVoip.value &&
if ((AppSettings.experimentalVoip.value &&
Matrix.of(context).voipPlugin != null &&
controller.room.isDirectChat)
controller.room.isDirectChat))
IconButton(
onPressed: controller.onPhoneButtonTap,
icon: const Icon(Icons.call_outlined),
tooltip: L10n.of(context).placeCall,
),
)
else if (AppSettings.jitsiFeature.value)
JitsiPopupButton(controller.room),
EncryptionButton(controller.room),
ChatSettingsPopupMenu(controller.room, true),
],

View file

@ -0,0 +1,177 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
class JitsiPopupButton extends StatelessWidget {
final Room room;
const JitsiPopupButton(this.room, {super.key});
Future<void> _startCall(BuildContext context, bool isAudioOnly) async {
final l10n = L10n.of(context);
final urlResult = await showFutureLoadingDialog(
context: context,
future: () async {
final conferenceId = room.client.generateUniqueTransactionId();
final domain = AppSettings.jitsiDomain.value;
final uri = Uri(
scheme: 'https',
host: domain,
path: conferenceId,
fragment: isAudioOnly ? 'config.startWithVideoMuted=true' : null,
);
await room.addWidget(
MatrixWidget(
room: room,
name: 'Jitsi Meet',
type: 'jitsi',
url: uri.toString(),
data: {
'domain': domain,
'isAudioOnly': isAudioOnly,
'conferenceId': conferenceId,
'roomName': room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
),
},
),
);
return uri;
},
);
final url = urlResult.result;
if (url == null) return;
await launchUrl(url);
if (!context.mounted) return;
final consent = await showOkCancelAlertDialog(
context: context,
title: l10n.removeCallFromChat,
message: l10n.removeCallFromChatDescription,
okLabel: l10n.remove,
);
if (consent != OkCancelResult.ok) return;
if (!context.mounted) return;
await _endAllCalls(context);
}
Future<void> _endAllCalls(BuildContext context) => showFutureLoadingDialog(
context: context,
future: () async {
final activeJitsiCalls = room.states['im.vector.modular.widgets']?.values
.where((state) => state.content['type'] == 'jitsi');
if (activeJitsiCalls == null) return;
for (final call in activeJitsiCalls) {
await room.deleteWidget(call.stateKey!);
}
},
);
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
final activeJitsiCalls = room.widgets
.where((widget) => widget.type == 'jitsi')
.map((widget) {
final isAudioOnly = widget.data?.tryGet<bool>('isAudioOnly') ?? false;
final domain = widget.data?.tryGet<String>('domain');
final conferenceId = widget.data?.tryGet<String>('conferenceId');
return (
isAudioOnly: isAudioOnly,
domain: domain,
conferenceId: conferenceId,
);
})
.toList();
final canEditCalls = room.canChangeStateEvent('im.vector.modular.widgets');
if (activeJitsiCalls.isEmpty && !canEditCalls) {
return const SizedBox.shrink();
}
return PopupMenuButton(
itemBuilder: (context) => [
...activeJitsiCalls.map(
(call) => PopupMenuItem(
onTap: () => launchUrl(
Uri(
scheme: 'https',
host: call.domain,
path: call.conferenceId,
fragment: call.isAudioOnly
? 'config.startWithVideoMuted=true'
: null,
),
),
child: Row(
mainAxisSize: .min,
children: [
Icon(call.isAudioOnly ? Icons.add_call : Icons.video_call),
const SizedBox(width: 12),
Text(
call.isAudioOnly ? l10n.joinVoiceCall : l10n.joinVideoCall,
),
],
),
),
),
if (canEditCalls) ...[
if (activeJitsiCalls.isEmpty) ...[
PopupMenuItem(
onTap: () => _startCall(context, true),
child: Row(
mainAxisSize: .min,
children: [
const Icon(Icons.add_call),
const SizedBox(width: 12),
Text(l10n.startVoiceCall),
],
),
),
PopupMenuItem(
onTap: () => _startCall(context, false),
child: Row(
mainAxisSize: .min,
children: [
const Icon(Icons.video_call),
const SizedBox(width: 12),
Text(l10n.startVideoCall),
],
),
),
] else
PopupMenuItem(
onTap: () => _endAllCalls(context),
child: Row(
mainAxisSize: .min,
children: [
Icon(
Icons.call_end_outlined,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 12),
Text(
l10n.removeCallForEveryone,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
],
),
),
],
],
icon: Badge(
label: Text(l10n.live),
isLabelVisible: activeJitsiCalls.isNotEmpty,
child: Icon(Icons.video_call_outlined),
),
);
}
}