From 77880666e7d42a394cd1745e1df646196e547bcc Mon Sep 17 00:00:00 2001 From: Jayesh Nirve Date: Tue, 28 Jun 2022 16:20:06 +0530 Subject: [PATCH] feat: group calls stash --- android/app/src/main/AndroidManifest.xml | 12 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 1 - assets/l10n/intl_ca.arb | 57 +- assets/l10n/intl_gl.arb | 4 +- lib/config/isrg_x1.dart | 32 -- lib/main.dart | 1 - lib/pages/bootstrap/bootstrap_dialog.dart | 7 +- lib/pages/chat/chat.dart | 68 ++- lib/pages/chat/chat_app_bar_title.dart | 5 +- lib/pages/chat/chat_input_row.dart | 4 +- lib/pages/chat/chat_view.dart | 7 +- lib/pages/chat/event_info_dialog.dart | 8 +- lib/pages/chat/events/message.dart | 41 +- lib/pages/chat/events/message_content.dart | 97 ++-- lib/pages/chat/events/message_reactions.dart | 2 +- lib/pages/chat/events/reply_content.dart | 33 +- lib/pages/chat/events/state_message.dart | 28 +- lib/pages/chat/pinned_events.dart | 63 +-- lib/pages/chat/reply_display.dart | 35 +- .../chat_encryption_settings_view.dart | 9 +- lib/pages/chat_list/chat_list.dart | 47 +- lib/pages/chat_list/chat_list_item.dart | 67 +-- .../chat_list/client_chooser_button.dart | 4 +- lib/pages/dialer/dialer.dart | 528 +++++++++--------- lib/pages/dialer/group_call_view.dart | 166 ++++++ .../invitation_selection.dart | 2 +- .../key_verification_dialog.dart | 2 +- lib/pages/settings/settings_view.dart | 1 - lib/pages/story/story_page.dart | 7 +- lib/utils/background_push.dart | 31 +- lib/utils/client_manager.dart | 57 +- lib/utils/custom_http_client.dart | 30 - lib/utils/fluffy_share.dart | 6 +- .../client_stories_extension.dart | 3 +- .../fluffybox_database.dart | 3 - ...dart => flutter_matrix_hive_database.dart} | 92 +-- .../matrix_file_extension.dart | 6 +- lib/utils/push_helper.dart | 40 +- lib/utils/voip/call_session_state.dart | 191 +++++++ lib/utils/voip/call_state_proxy.dart | 45 ++ lib/utils/voip/callkeep_manager.dart | 312 ----------- lib/utils/voip/group_call_state.dart | 123 ++++ lib/utils/voip_plugin.dart | 86 ++- .../local_notifications_extension.dart | 9 +- lib/widgets/matrix.dart | 3 +- pubspec.lock | 43 +- pubspec.yaml | 16 +- test/utils/test_client.dart | 4 +- 48 files changed, 1270 insertions(+), 1168 deletions(-) delete mode 100644 lib/config/isrg_x1.dart create mode 100644 lib/pages/dialer/group_call_view.dart delete mode 100644 lib/utils/custom_http_client.dart rename lib/utils/matrix_sdk_extensions.dart/{flutter_hive_collections_database.dart => flutter_matrix_hive_database.dart} (50%) create mode 100644 lib/utils/voip/call_session_state.dart create mode 100644 lib/utils/voip/call_state_proxy.dart delete mode 100644 lib/utils/voip/callkeep_manager.dart create mode 100644 lib/utils/voip/group_call_state.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index db30b324..fd2d19f4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -23,9 +23,10 @@ + + tools:overrideLibrary="io.wazo.callkeep, net.touchcapture.qr.flutterqr, com.cloudwebrtc.webrtc, org.webrtc, com.it_nomads.fluttersecurestorage, com.pichillilorenzo.flutter_inappwebview, com.example.video_compress, com.otaliastudios.transcoder, com.otaliastudios.opengl, com.kineapps.flutter_file_dialog, com.pravera.flutter_foreground_task"/> @@ -102,6 +105,8 @@ + + @@ -129,4 +134,7 @@ android:name="flutterEmbedding" android:value="2" /> + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index ef49c991..7353dbd1 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,5 +2,4 @@ - \ No newline at end of file diff --git a/assets/l10n/intl_ca.arb b/assets/l10n/intl_ca.arb index 03b924fc..541db3b9 100644 --- a/assets/l10n/intl_ca.arb +++ b/assets/l10n/intl_ca.arb @@ -185,7 +185,7 @@ "username": {} } }, - "changedTheChatDescriptionTo": "{username} ha canviat la descripció del xat a: '{description}'", + "changedTheChatDescriptionTo": "{username} ha canviat la descripció del xat a: «{description}»", "@changedTheChatDescriptionTo": { "type": "text", "placeholders": { @@ -193,7 +193,7 @@ "description": {} } }, - "changedTheChatNameTo": "{username} ha canviat el nom del xat a: '{chatname}'", + "changedTheChatNameTo": "{username} ha canviat el nom del xat a: «{chatname}»", "@changedTheChatNameTo": { "type": "text", "placeholders": { @@ -404,7 +404,7 @@ "type": "text", "placeholders": {} }, - "containsDisplayName": "Conté l'àlies", + "containsDisplayName": "Conté el nom visible", "@containsDisplayName": { "type": "text", "placeholders": {} @@ -441,7 +441,7 @@ "type": "text", "placeholders": {} }, - "couldNotSetDisplayname": "No s’ha pogut definir l'àlies", + "couldNotSetDisplayname": "No s’ha pogut definir el nom visible", "@couldNotSetDisplayname": { "type": "text", "placeholders": {} @@ -503,7 +503,7 @@ "timeOfDay": {} } }, - "dateWithoutYear": "{day}-{month}", + "dateWithoutYear": "{day}/{month}", "@dateWithoutYear": { "type": "text", "placeholders": { @@ -511,7 +511,7 @@ "day": {} } }, - "dateWithYear": "{day}-{month}-{year}", + "dateWithYear": "{day}/{month}/{year}", "@dateWithYear": { "type": "text", "placeholders": { @@ -570,7 +570,7 @@ "type": "text", "placeholders": {} }, - "displaynameHasBeenChanged": "Ha canviat l'àlies", + "displaynameHasBeenChanged": "S’ha canviat el nom visible", "@displaynameHasBeenChanged": { "type": "text", "placeholders": {} @@ -590,7 +590,7 @@ "type": "text", "placeholders": {} }, - "editDisplayname": "Edita l'àlies", + "editDisplayname": "Edita el nom visible", "@editDisplayname": { "type": "text", "placeholders": {} @@ -1725,7 +1725,7 @@ "type": "text", "placeholders": {} }, - "unknownEvent": "Esdeveniment desconegut '{type}'", + "unknownEvent": "L’esdeveniment «{type}» és desconegut", "@unknownEvent": { "type": "text", "placeholders": { @@ -1829,7 +1829,7 @@ "type": "text", "placeholders": {} }, - "verifySuccess": "T'has verificat correctament!", + "verifySuccess": "Us heu verificat correctament.", "@verifySuccess": { "type": "text", "placeholders": {} @@ -1889,7 +1889,7 @@ "type": "text", "placeholders": {} }, - "warningEncryptionInBeta": "El xifrat d'extrem a extrem es troba actualment en proves (Beta. Utilitza'l sota la teva responsabilitat!", + "warningEncryptionInBeta": "El xifratge d’extrem a extrem es troba actualment en proves. Utilitzeu-lo sota la vostra responsabilitat.", "@warningEncryptionInBeta": { "type": "text", "placeholders": {} @@ -2417,7 +2417,7 @@ "type": "text", "placeholders": {} }, - "changedTheDisplaynameTo": "{username} ha canviat el seu àlies a: '{displayname}'", + "changedTheDisplaynameTo": "{username} ha canviat el propi nom visible a: «{displayname}»", "@changedTheDisplaynameTo": { "type": "text", "placeholders": { @@ -2492,38 +2492,5 @@ "@bubbleSize": { "type": "text", "placeholders": {} - }, - "commandHint_myroomnick": "Estableix el teu àlies per a aquesta sala", - "@commandHint_myroomnick": { - "type": "text", - "description": "Usage hint for the command /myroomnick" - }, - "editBlockedServers": "Edita els servidors bloquejats", - "@editBlockedServers": { - "type": "text", - "placeholders": {} - }, - "badServerLoginTypesException": "El servidor admet els inicis de sessió:\n{serverVersions}\nPerò l'aplicació només admet:\n{supportedVersions}", - "@badServerLoginTypesException": { - "type": "text", - "placeholders": { - "serverVersions": {}, - "supportedVersions": {} - } - }, - "discoverGroups": "Descobreix grups", - "@discoverGroups": { - "type": "text", - "placeholders": {} - }, - "discover": "Descobreix", - "@discover": { - "type": "text", - "placeholders": {} - }, - "editChatPermissions": "Edita els permisos del xat", - "@editChatPermissions": { - "type": "text", - "placeholders": {} } } diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index 83c11d9b..08662560 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -2858,7 +2858,5 @@ } }, "showSpaces": "Mostrar lista de espazos", - "@showSpaces": {}, - "noEmailWarning": "Escribe un enderezo de email válido. Doutro xeito non poderás restablecer o contrasinal. Se non queres, toca outra vez no botón para continuar.", - "@noEmailWarning": {} + "@showSpaces": {} } diff --git a/lib/config/isrg_x1.dart b/lib/config/isrg_x1.dart deleted file mode 100644 index 6a6d0ac2..00000000 --- a/lib/config/isrg_x1.dart +++ /dev/null @@ -1,32 +0,0 @@ -// ignore: constant_identifier_names -const String ISRG_X1 = """-----BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE-----"""; diff --git a/lib/main.dart b/lib/main.dart index 7a0d60f9..6d061856 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,6 @@ import 'config/themes.dart'; import 'utils/background_push.dart'; import 'utils/custom_scroll_behaviour.dart'; import 'utils/localized_exception_extension.dart'; -import 'utils/platform_infos.dart'; import 'widgets/lock_screen.dart'; import 'widgets/matrix.dart'; diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index 6e35e62e..bace36c9 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -121,12 +121,7 @@ class _BootstrapDialogState extends State { icon: const Icon(Icons.save_alt_outlined), label: Text(L10n.of(context)!.saveTheSecurityKeyNow), onPressed: () { - final box = context.findRenderObject() as RenderBox; - Share.share( - key!, - sharePositionOrigin: - box.localToGlobal(Offset.zero) & box.size, - ); + Share.share(key!); setState(() => _recoveryKeyCopied = true); }, ), diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index c58272cd..f7410969 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -25,7 +25,6 @@ import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart import 'package:fluffychat/utils/matrix_sdk_extensions.dart/ios_badge_client_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/voip/callkeep_manager.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; @@ -35,6 +34,8 @@ import 'send_file_dialog.dart'; import 'send_location_dialog.dart'; import 'sticker_picker_dialog.dart'; +enum VoipType { kVoice, kVideo, kGroup } + class Chat extends StatefulWidget { final Widget? sideView; @@ -168,14 +169,6 @@ class ChatController extends State { void initState() { scrollController.addListener(_updateScrollController); inputFocus.addListener(_inputFocusListener); - final voipPlugin = Matrix.of(context).voipPlugin; - - if (voipPlugin != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - CallKeepManager().setVoipPlugin(voipPlugin); - CallKeepManager().initialize().catchError((_) => true); - }); - } super.initState(); } @@ -461,11 +454,11 @@ class ChatController extends State { if (selectedEvents.length == 1) { return selectedEvents.first .getDisplayEvent(timeline!) - .calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!)); + .getLocalizedBody(MatrixLocals(L10n.of(context)!)); } for (final event in selectedEvents) { if (copyString.isNotEmpty) copyString += '\n\n'; - copyString += event.getDisplayEvent(timeline!).calcLocalizedBodyFallback( + copyString += event.getDisplayEvent(timeline!).getLocalizedBody( MatrixLocals(L10n.of(context)!), withSenderNamePrefix: true); } @@ -773,7 +766,7 @@ class ChatController extends State { editEvent = selectedEvents.first; inputText = sendController.text = editEvent! .getDisplayEvent(timeline!) - .calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!), + .getLocalizedBody(MatrixLocals(L10n.of(context)!), withSenderNamePrefix: false, hideReply: true); selectedEvents.clear(); }); @@ -961,22 +954,30 @@ class ChatController extends State { } }); } - final callType = await showModalActionSheet( + final callType = await showModalActionSheet( context: context, title: L10n.of(context)!.warning, message: L10n.of(context)!.videoCallsBetaWarning, cancelLabel: L10n.of(context)!.cancel, actions: [ - SheetAction( - label: L10n.of(context)!.voiceCall, - icon: Icons.phone_outlined, - key: CallType.kVoice, - ), - SheetAction( - label: L10n.of(context)!.videoCall, - icon: Icons.video_call_outlined, - key: CallType.kVideo, - ), + if (room!.isDirectChat) + SheetAction( + label: L10n.of(context)!.voiceCall, + icon: Icons.phone_outlined, + key: VoipType.kVoice, + ), + if (room!.isDirectChat) + SheetAction( + label: L10n.of(context)!.videoCall, + icon: Icons.video_call_outlined, + key: VoipType.kVideo, + ), + if (!room!.isDirectChat) + const SheetAction( + label: 'Join group call', + icon: Icons.phone_outlined, + key: VoipType.kGroup, + ), ], ); if (callType == null) return; @@ -987,11 +988,22 @@ class ChatController extends State { Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials()); if (success.result != null) { final voipPlugin = Matrix.of(context).voipPlugin; - await voipPlugin!.voip.inviteToCall(room!.id, callType).catchError((e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text((e as Object).toLocalizedString(context))), - ); - }); + if ({VoipType.kVideo, VoipType.kVoice}.contains(callType)) { + await voipPlugin!.voip + .inviteToCall(room!.id, + callType == VoipType.kVoice ? CallType.kVoice : CallType.kVideo) + .catchError((e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text((e as Object).toLocalizedString(context))), + ); + }); + } else { + var groupCall = voipPlugin!.voip.getGroupCallForRoom(room!.id); + groupCall ??= await voipPlugin.voip.newGroupCall( + room!.id, GroupCallType.Video, GroupCallIntent.Prompt); + groupCall?.enter(); + Logs().e('Group call should be enter now'); + } } else { await showOkAlertDialog( context: context, diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index f4a369d7..a112ff8c 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -29,11 +29,10 @@ class ChatAppBarTitle extends StatelessWidget { ? () => showModalBottomSheet( context: context, builder: (c) => UserBottomSheet( - user: room - .unsafeGetUserFromMemoryOrFallback(directChatMatrixID), + user: room.getUserByMXIDSync(directChatMatrixID), outerContext: context, onMention: () => controller.sendController.text += - '${room.unsafeGetUserFromMemoryOrFallback(directChatMatrixID).mention} ', + '${room.getUserByMXIDSync(directChatMatrixID).mention} ', ), ) : () => VRouter.of(context).toSegments(['rooms', room.id, 'details']), diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 62f1db76..19599f78 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -293,14 +293,14 @@ class _ChatAccountPicker extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: FutureBuilder( - future: controller.sendingClient!.fetchOwnProfile(), + future: controller.sendingClient!.ownProfile, builder: (context, snapshot) => PopupMenuButton( onSelected: _popupMenuButtonSelected, itemBuilder: (BuildContext context) => clients .map((client) => PopupMenuItem( value: client!.userID, child: FutureBuilder( - future: client.fetchOwnProfile(), + future: client.ownProfile, builder: (context, snapshot) => ListTile( leading: Avatar( mxContent: snapshot.data?.avatarUrl, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index d628737b..c53615a7 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -113,8 +113,7 @@ class ChatView extends StatelessWidget { ]; } else { return [ - if (Matrix.of(context).voipPlugin != null && - controller.room!.isDirectChat) + if (Matrix.of(context).voipPlugin != null) IconButton( onPressed: controller.onPhoneButtonTap, icon: const Icon(Icons.call_outlined), @@ -350,12 +349,12 @@ class ChatView extends StatelessWidget { builder: (c) => UserBottomSheet( user: event - .senderFromMemoryOrFallback, + .sender, outerContext: context, onMention: () => controller .sendController - .text += '${event.senderFromMemoryOrFallback.mention} ', + .text += '${event.sender.mention} ', ), ), unfold: controller diff --git a/lib/pages/chat/event_info_dialog.dart b/lib/pages/chat/event_info_dialog.dart index 16862bf0..ce9be4d8 100644 --- a/lib/pages/chat/event_info_dialog.dart +++ b/lib/pages/chat/event_info_dialog.dart @@ -48,12 +48,12 @@ class EventInfoDialog extends StatelessWidget { children: [ ListTile( leading: Avatar( - mxContent: event.senderFromMemoryOrFallback.avatarUrl, - name: event.senderFromMemoryOrFallback.calcDisplayname(), + mxContent: event.sender.avatarUrl, + name: event.sender.calcDisplayname(), ), title: Text(L10n.of(context)!.sender), - subtitle: Text( - '${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]'), + subtitle: + Text('${event.sender.calcDisplayname()} [${event.senderId}]'), ), ListTile( title: Text(L10n.of(context)!.time), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 32f0873a..37f5dd94 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -75,7 +75,7 @@ class Message extends StatelessWidget { EventTypes.Sticker, EventTypes.Encrypted, ].contains(nextEvent!.type) - ? nextEvent!.senderId == event.senderId && !displayTime + ? nextEvent!.sender.id == event.sender.id && !displayTime : false; final textColor = ownMessage ? Theme.of(context).colorScheme.onPrimary @@ -125,16 +125,11 @@ class Message extends StatelessWidget { ), ), )) - : FutureBuilder( - future: event.fetchSenderUser(), - builder: (context, snapshot) { - final user = snapshot.data ?? event.senderFromMemoryOrFallback; - return Avatar( - mxContent: user.avatarUrl, - name: user.calcDisplayname(), - onTap: () => onAvatarTab!(event), - ); - }), + : Avatar( + mxContent: event.sender.avatarUrl, + name: event.sender.calcDisplayname(), + onTap: () => onAvatarTab!(event), + ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -145,22 +140,14 @@ class Message extends StatelessWidget { padding: const EdgeInsets.only(left: 8.0, bottom: 4), child: ownMessage || event.room.isDirectChat ? const SizedBox(height: 12) - : FutureBuilder( - future: event.fetchSenderUser(), - builder: (context, snapshot) { - final displayname = - snapshot.data?.calcDisplayname() ?? - event.senderFromMemoryOrFallback - .calcDisplayname(); - return Text( - displayname, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: displayname.color, - ), - ); - }), + : Text( + event.sender.calcDisplayname(), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: event.sender.calcDisplayname().color, + ), + ), ), Container( alignment: alignment, diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 59e4d649..2c6597ba 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -34,7 +34,7 @@ class MessageContent extends StatelessWidget { content: Text( event.type == EventTypes.Encrypted ? L10n.of(context)!.needPantalaimonWarning - : event.calcLocalizedBodyFallback( + : event.getLocalizedBody( MatrixLocals(L10n.of(context)!), ), ))); @@ -172,73 +172,48 @@ class MessageContent extends StatelessWidget { textmessage: default: if (event.redacted) { - return FutureBuilder( - future: event.fetchSenderUser(), - builder: (context, snapshot) { - return _ButtonContent( - label: L10n.of(context)!.redactedAnEvent(snapshot.data - ?.calcDisplayname() ?? - event.senderFromMemoryOrFallback.calcDisplayname()), - icon: const Icon(Icons.delete_outlined), - textColor: buttonTextColor, - onPressed: () => onInfoTab!(event), - ); - }); + return _ButtonContent( + label: L10n.of(context)! + .redactedAnEvent(event.sender.calcDisplayname()), + icon: const Icon(Icons.delete_outlined), + textColor: buttonTextColor, + onPressed: () => onInfoTab!(event), + ); } final bigEmotes = event.onlyEmotes && event.numberEmotes > 0 && event.numberEmotes <= 10; - return FutureBuilder( - future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!), - hideReply: true), - builder: (context, snapshot) { - return LinkText( - text: snapshot.data ?? - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true), - textStyle: TextStyle( - color: textColor, - fontSize: bigEmotes ? fontSize * 3 : fontSize, - decoration: - event.redacted ? TextDecoration.lineThrough : null, - ), - linkStyle: TextStyle( - color: textColor.withAlpha(150), - fontSize: bigEmotes ? fontSize * 3 : fontSize, - decoration: TextDecoration.underline, - ), - onLinkTap: (url) => UrlLauncher(context, url).launchUrl(), - ); - }); + return LinkText( + text: event.getLocalizedBody(MatrixLocals(L10n.of(context)!), + hideReply: true), + textStyle: TextStyle( + color: textColor, + fontSize: bigEmotes ? fontSize * 3 : fontSize, + decoration: event.redacted ? TextDecoration.lineThrough : null, + ), + linkStyle: TextStyle( + color: textColor.withAlpha(150), + fontSize: bigEmotes ? fontSize * 3 : fontSize, + decoration: TextDecoration.underline, + ), + onLinkTap: (url) => UrlLauncher(context, url).launchUrl(), + ); } case EventTypes.CallInvite: - return FutureBuilder( - future: event.fetchSenderUser(), - builder: (context, snapshot) { - return _ButtonContent( - label: L10n.of(context)!.startedACall( - snapshot.data?.calcDisplayname() ?? - event.senderFromMemoryOrFallback.calcDisplayname()), - icon: const Icon(Icons.phone_outlined), - textColor: buttonTextColor, - onPressed: () => onInfoTab!(event), - ); - }); + return _ButtonContent( + label: L10n.of(context)!.startedACall(event.sender.calcDisplayname()), + icon: const Icon(Icons.phone_outlined), + textColor: buttonTextColor, + onPressed: () => onInfoTab!(event), + ); default: - return FutureBuilder( - future: event.fetchSenderUser(), - builder: (context, snapshot) { - return _ButtonContent( - label: L10n.of(context)!.userSentUnknownEvent( - snapshot.data?.calcDisplayname() ?? - event.senderFromMemoryOrFallback.calcDisplayname(), - event.type), - icon: const Icon(Icons.info_outlined), - textColor: buttonTextColor, - onPressed: () => onInfoTab!(event), - ); - }); + return _ButtonContent( + label: L10n.of(context)! + .userSentUnknownEvent(event.sender.calcDisplayname(), event.type), + icon: const Icon(Icons.info_outlined), + textColor: buttonTextColor, + onPressed: () => onInfoTab!(event), + ); } } } diff --git a/lib/pages/chat/events/message_reactions.dart b/lib/pages/chat/events/message_reactions.dart index 023f757d..8e4b784c 100644 --- a/lib/pages/chat/events/message_reactions.dart +++ b/lib/pages/chat/events/message_reactions.dart @@ -39,7 +39,7 @@ class MessageReactions extends StatelessWidget { ); } reactionMap[key]!.count++; - reactionMap[key]!.reactors!.add(e.senderFromMemoryOrFallback); + reactionMap[key]!.reactors!.add(e.sender); reactionMap[key]!.reacted |= e.senderId == e.room.client.userID; } } diff --git a/lib/pages/chat/events/reply_content.dart b/lib/pages/chat/events/reply_content.dart index cde75eb1..65cf3e97 100644 --- a/lib/pages/chat/events/reply_content.dart +++ b/lib/pages/chat/events/reply_content.dart @@ -52,7 +52,7 @@ class ReplyContent extends StatelessWidget { ); } else { replyBody = Text( - displayEvent.calcLocalizedBodyFallback( + displayEvent.getLocalizedBody( MatrixLocals(L10n.of(context)!), withSenderNamePrefix: false, hideReply: true, @@ -83,25 +83,18 @@ class ReplyContent extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - FutureBuilder( - future: displayEvent.fetchSenderUser(), - builder: (context, snapshot) { - return Text( - (snapshot.data?.calcDisplayname() ?? - displayEvent.senderFromMemoryOrFallback - .calcDisplayname()) + - ':', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.bold, - color: ownMessage - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.onBackground, - fontSize: fontSize, - ), - ); - }), + Text( + displayEvent.sender.calcDisplayname() + ':', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + color: ownMessage + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onBackground, + fontSize: fontSize, + ), + ), replyBody, ], ), diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index 6e270ae6..c10537ad 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -39,24 +39,16 @@ class StateMessage extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - FutureBuilder( - future: event - .calcLocalizedBody(MatrixLocals(L10n.of(context)!)), - builder: (context, snapshot) { - return Text( - snapshot.data ?? - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!)), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14 * AppConfig.fontSizeFactor, - color: Theme.of(context).textTheme.bodyText2!.color, - decoration: event.redacted - ? TextDecoration.lineThrough - : null, - ), - ); - }), + Text( + event.getLocalizedBody(MatrixLocals(L10n.of(context)!)), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14 * AppConfig.fontSizeFactor, + color: Theme.of(context).textTheme.bodyText2!.color, + decoration: + event.redacted ? TextDecoration.lineThrough : null, + ), + ), if (counter != 0) Text( L10n.of(context)!.moreEvents(counter), diff --git a/lib/pages/chat/pinned_events.dart b/lib/pages/chat/pinned_events.dart index 856d95ac..c72ff1cc 100644 --- a/lib/pages/chat/pinned_events.dart +++ b/lib/pages/chat/pinned_events.dart @@ -26,7 +26,7 @@ class PinnedEvents extends StatelessWidget { actions: events .map((event) => SheetAction( key: event?.eventId ?? '', - label: event?.calcLocalizedBodyFallback( + label: event?.getLocalizedBody( MatrixLocals(L10n.of(context)!), withSenderNamePrefix: true, hideReply: true, @@ -90,41 +90,32 @@ class PinnedEvents extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: FutureBuilder( - future: event.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), - withSenderNamePrefix: true, - hideReply: true, - ), - builder: (context, snapshot) { - return LinkText( - text: snapshot.data ?? - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - withSenderNamePrefix: true, - hideReply: true, - ), - maxLines: 2, - textStyle: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: fontSize, - decoration: event.redacted - ? TextDecoration.lineThrough - : null, - ), - linkStyle: TextStyle( - color: Theme.of(context) - .textTheme - .bodyText1 - ?.color - ?.withAlpha(150), - fontSize: fontSize, - decoration: TextDecoration.underline, - ), - onLinkTap: (url) => - UrlLauncher(context, url).launchUrl(), - ); - }), + child: LinkText( + text: event.getLocalizedBody( + MatrixLocals(L10n.of(context)!), + withSenderNamePrefix: true, + hideReply: true, + ), + maxLines: 2, + textStyle: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: fontSize, + decoration: event.redacted + ? TextDecoration.lineThrough + : null, + ), + linkStyle: TextStyle( + color: Theme.of(context) + .textTheme + .bodyText1 + ?.color + ?.withAlpha(150), + fontSize: fontSize, + decoration: TextDecoration.underline, + ), + onLinkTap: (url) => + UrlLauncher(context, url).launchUrl(), + ), ), ), ], diff --git a/lib/pages/chat/reply_display.dart b/lib/pages/chat/reply_display.dart index ccb6e6a7..5185d1db 100644 --- a/lib/pages/chat/reply_display.dart +++ b/lib/pages/chat/reply_display.dart @@ -50,7 +50,6 @@ class _EditContent extends StatelessWidget { @override Widget build(BuildContext context) { - final event = this.event; if (event == null) { return Container(); } @@ -61,27 +60,19 @@ class _EditContent extends StatelessWidget { color: Theme.of(context).primaryColor, ), Container(width: 15.0), - FutureBuilder( - future: event.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), - withSenderNamePrefix: false, - hideReply: true, - ), - builder: (context, snapshot) { - return Text( - snapshot.data ?? - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - withSenderNamePrefix: false, - hideReply: true, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: TextStyle( - color: Theme.of(context).textTheme.bodyText2!.color, - ), - ); - }), + Text( + event?.getLocalizedBody( + MatrixLocals(L10n.of(context)!), + withSenderNamePrefix: false, + hideReply: true, + ) ?? + '', + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + color: Theme.of(context).textTheme.bodyText2!.color, + ), + ), ], ); } diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart index 581bff85..8da5698a 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart @@ -94,18 +94,15 @@ class ChatEncryptionSettingsView extends StatelessWidget { child: ListTile( leading: Avatar( mxContent: room - .unsafeGetUserFromMemoryOrFallback( - deviceKeys[i].userId) + .getUserByMXIDSync(deviceKeys[i].userId) .avatarUrl, name: room - .unsafeGetUserFromMemoryOrFallback( - deviceKeys[i].userId) + .getUserByMXIDSync(deviceKeys[i].userId) .calcDisplayname(), ), title: Text( room - .unsafeGetUserFromMemoryOrFallback( - deviceKeys[i].userId) + .getUserByMXIDSync(deviceKeys[i].userId) .calcDisplayname(), ), subtitle: Text( diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index bbe06ce2..504de3ff 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -1,10 +1,12 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; @@ -17,6 +19,7 @@ import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pages/chat_list/spaces_bottom_bar.dart'; import 'package:fluffychat/pages/chat_list/spaces_entry.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions.dart/client_stories_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import '../../../utils/account_bundles.dart'; import '../../main.dart'; @@ -205,13 +208,54 @@ class ChatListController extends State with TickerProviderStateMixin { } } + final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + Future goToRoom(String? roomId) async { + try { + final client = Matrix.of(context).client; + Logs().v('[Push] Attempting to go to room $roomId...'); + if (roomId == null) { + return; + } + await client.roomsLoading; + await client.accountDataLoading; + final isStory = client + .getRoomById(roomId) + ?.getState(EventTypes.RoomCreate) + ?.content + .tryGet('type') == + ClientStoriesExtension.storiesRoomType; + VRouter.of(context).toSegments([isStory ? 'stories' : 'rooms', roomId]); + } catch (e, s) { + Logs().e('[Push] Failed to open room', e, s); + } + } + + void doStuffIfOpenedFromNotification() async { + bool _wentToRoomOnStartup = false; + final notificationAppLaunchDetails = await _flutterLocalNotificationsPlugin + .getNotificationAppLaunchDetails(); + if (notificationAppLaunchDetails == null || + notificationAppLaunchDetails.payload == null || + !notificationAppLaunchDetails.didNotificationLaunchApp || + _wentToRoomOnStartup) { + return; + } + final payload = jsonDecode(notificationAppLaunchDetails.payload!); + final roomId = payload['roomId']; + final eventType = payload['eventType']; + Logs().i(eventType); + _wentToRoomOnStartup = true; + goToRoom(roomId); + } + @override void initState() { _initReceiveSharingIntent(); - scrollController.addListener(_onScroll); _waitForFirstSync(); _hackyWebRTCFixForWeb(); + doStuffIfOpenedFromNotification(); super.initState(); } @@ -591,6 +635,7 @@ class ChatListController extends State with TickerProviderStateMixin { void _hackyWebRTCFixForWeb() { Matrix.of(context).voipPlugin?.context = context; + Matrix.of(context).voipPlugin?.start(); } void snapBackSpacesSheet() { diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 9affa6dc..ebf6eb89 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -262,47 +262,32 @@ class ChatListItem extends StatelessWidget { ), softWrap: false, ) - : FutureBuilder( - future: room.lastEvent?.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: !room.isDirectChat || - room.directChatMatrixID != - room.lastEvent?.senderId, - ) ?? - Future.value(L10n.of(context)!.emptyChat), - builder: (context, snapshot) { - return Text( - room.membership == Membership.invite - ? L10n.of(context)!.youAreInvitedToThisChat - : snapshot.data ?? - room.lastEvent?.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: !room.isDirectChat || - room.directChatMatrixID != - room.lastEvent?.senderId, - ) ?? - L10n.of(context)!.emptyChat, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: unread - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).textTheme.bodyText2!.color, - decoration: room.lastEvent?.redacted == true - ? TextDecoration.lineThrough - : null, - ), - ); - }), + : Text( + room.membership == Membership.invite + ? L10n.of(context)!.youAreInvitedToThisChat + : room.lastEvent?.getLocalizedBody( + MatrixLocals(L10n.of(context)!), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: !room.isDirectChat || + room.directChatMatrixID != + room.lastEvent?.senderId, + ) ?? + L10n.of(context)!.emptyChat, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: unread + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).textTheme.bodyText2!.color, + decoration: room.lastEvent?.redacted == true + ? TextDecoration.lineThrough + : null, + ), + ), ), const SizedBox(width: 8), AnimatedContainer( diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 3c9a4b2f..cdd844a6 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -48,7 +48,7 @@ class ClientChooserButton extends StatelessWidget { (client) => PopupMenuItem( value: client, child: FutureBuilder( - future: client!.fetchOwnProfile(), + future: client!.ownProfile, builder: (context, snapshot) => Row( children: [ Avatar( @@ -90,7 +90,7 @@ class ClientChooserButton extends StatelessWidget { matrix.accountBundles.forEach((key, value) => clientCount += value.length); return Center( child: FutureBuilder( - future: matrix.client.fetchOwnProfile(), + future: matrix.client.ownProfile, builder: (context, snapshot) => Stack( alignment: Alignment.center, children: [ diff --git a/lib/pages/dialer/dialer.dart b/lib/pages/dialer/dialer.dart index b8470903..934b84f9 100644 --- a/lib/pages/dialer/dialer.dart +++ b/lib/pages/dialer/dialer.dart @@ -19,6 +19,11 @@ import 'dart:async'; import 'dart:math'; +import 'package:fluffychat/pages/dialer/group_call_view.dart'; +import 'package:fluffychat/utils/voip/call_session_state.dart'; +import 'package:fluffychat/utils/voip/call_state_proxy.dart'; +import 'package:fluffychat/utils/voip/group_call_state.dart'; +import 'package:fluffychat/utils/voip_plugin.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -64,60 +69,52 @@ class _StreamView extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - decoration: const BoxDecoration( - color: Colors.black54, - ), - child: Stack( - alignment: Alignment.center, - children: [ - if (videoMuted) - Container( - color: Colors.transparent, - ), - if (!videoMuted) - RTCVideoView( - // yes, it must explicitly be casted even though I do not feel - // comfortable with it... - wrappedStream.renderer as RTCVideoRenderer, - mirror: mirrored, - objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, - ), - if (videoMuted) - Positioned( - child: Avatar( + decoration: const BoxDecoration( + color: Colors.black54, + ), + child: Stack( + alignment: Alignment.center, + children: [ + if (videoMuted) + Container( + color: Colors.transparent, + ), + if (!videoMuted) + RTCVideoView( + // yes, it must explicitly be casted even though I do not feel + // comfortable with it... + wrappedStream.renderer as RTCVideoRenderer, + mirror: mirrored, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, + ), + if (videoMuted) + Positioned( + child: Avatar( mxContent: avatarUrl, name: displayName, size: mainView ? 96 : 48, client: matrixClient, // textSize: mainView ? 36 : 24, // matrixClient: matrixClient, - )), - if (!isScreenSharing) - Positioned( - left: 4.0, - bottom: 4.0, - child: Icon(audioMuted ? Icons.mic_off : Icons.mic, - color: Colors.white, size: 18.0), - ) - ], - )); + ), + ), + if (!isScreenSharing) + Positioned( + left: 4.0, + bottom: 4.0, + child: Icon(audioMuted ? Icons.mic_off : Icons.mic, + color: Colors.white, size: 18.0), + ) + ], + ), + ); } } class Calling extends StatefulWidget { - final VoidCallback? onClear; - final BuildContext context; - final String callId; - final CallSession call; - final Client client; - - const Calling( - {required this.context, - required this.call, - required this.client, - required this.callId, - this.onClear, - Key? key}) + final VoipPlugin voipPlugin; + final VoidCallback onClear; + const Calling({required this.voipPlugin, required this.onClear, Key? key}) : super(key: key); @override @@ -125,51 +122,88 @@ class Calling extends StatefulWidget { } class _MyCallingPage extends State { - Room? get room => call?.room; + late CallStateProxy? proxy; + late Room room; - String get displayName => call?.displayName ?? ''; - - String get callId => widget.callId; - - CallSession? get call => widget.call; + String get displayName => proxy?.displayName ?? ''; MediaStream? get localStream { - if (call != null && call!.localUserMediaStream != null) { - return call!.localUserMediaStream!.stream!; + if (proxy != null && proxy!.localUserMediaStream != null) { + return proxy!.localUserMediaStream!.stream!; } return null; } - MediaStream? get remoteStream { - if (call != null && call!.getRemoteStreams.isNotEmpty) { - return call!.getRemoteStreams[0].stream!; + bool get isMicrophoneMuted => proxy?.isMicrophoneMuted ?? false; + bool get isLocalVideoMuted => proxy?.isLocalVideoMuted ?? false; + bool get isScreensharingEnabled => proxy?.isScreensharingEnabled ?? false; + bool get isRemoteOnHold => proxy?.isRemoteOnHold ?? false; + bool get voiceonly => proxy?.voiceonly ?? false; + bool get connecting => proxy?.connecting ?? false; + bool get connected => proxy?.connected ?? false; + bool get ended => proxy?.ended ?? true; + bool get callOnHold => proxy?.callOnHold ?? false; + bool get isGroupCall => (proxy != null && proxy! is GroupCallSessionState); + bool get showMicMuteButton => connected; + bool get showScreenSharingButton => connected; + bool get showHoldButton => connected && !isGroupCall; + WrappedMediaStream? get primaryStream => proxy?.primaryStream; + + bool get showAnswerButton => + (!connected && !connecting && !ended) && + !(proxy?.isOutgoing ?? false) && + !isGroupCall; + bool get showVideoMuteButton => + proxy != null && + (proxy?.localUserMediaStream?.stream?.getVideoTracks().isNotEmpty ?? + false) && + connected; + + List get screenSharingStreams => + (proxy?.screenSharingStreams ?? []); + + List get userMediaStreams { + if (isGroupCall) { + return (proxy?.userMediaStreams ?? []); } - return null; + final streams = [ + ...proxy?.screenSharingStreams ?? [], + ...proxy?.userMediaStreams ?? [] + ]; + streams + .removeWhere((s) => s.stream?.id == proxy?.primaryStream?.stream?.id); + return streams; } - bool get speakerOn => call?.speakerOn ?? false; + String get title { + if (isGroupCall) { + return 'Group call'; + } + return (voiceonly ? 'Voice Call' : 'Video Call') + + ' (' + + (proxy?.callState ?? '') + + ')'; + } - bool get isMicrophoneMuted => call?.isMicrophoneMuted ?? false; + String get heldTitle { + var heldTitle = ''; + if (proxy?.localHold ?? false) { + heldTitle = '${proxy?.displayName ?? ''} held the call.'; + } else if (proxy?.remoteOnHold ?? false) { + heldTitle = 'You held the call.'; + } + return heldTitle; + } - bool get isLocalVideoMuted => call?.isLocalVideoMuted ?? false; + // bool get speakerOn => call?.speakerOn ?? false; - bool get isScreensharingEnabled => call?.screensharingEnabled ?? false; + // bool get mirrored => call?.facingMode == 'user'; - bool get isRemoteOnHold => call?.remoteOnHold ?? false; + VoipPlugin get voipPlugin => widget.voipPlugin; - bool get voiceonly => call == null || call?.type == CallType.kVoice; - - bool get connecting => call?.state == CallState.kConnecting; - - bool get connected => call?.state == CallState.kConnected; - - bool get mirrored => call?.facingMode == 'user'; - - List get streams => call?.streams ?? []; double? _localVideoHeight; double? _localVideoWidth; EdgeInsetsGeometry? _localVideoMargin; - CallState? _state; void _playCallSound() async { const path = 'assets/sounds/call.ogg'; @@ -190,25 +224,18 @@ class _MyCallingPage extends State { } void initialize() async { - final call = this.call; - if (call == null) return; + if (voipPlugin.currentGroupCall != null) { + room = voipPlugin.currentGroupCall!.room; + proxy = GroupCallSessionState(voipPlugin.currentGroupCall!); + } else if (voipPlugin.currentCall != null) { + room = voipPlugin.currentCall!.room; + proxy = CallSessionState(voipPlugin.currentCall!); + } else { + throw Exception('No call or group call found'); + } - call.onCallStateChanged.listen(_handleCallState); - call.onCallEventChanged.listen((event) { - if (event == CallEvent.kFeedsChanged) { - setState(() { - call.tryRemoveStopedStreams(); - }); - } else if (event == CallEvent.kLocalHoldUnhold || - event == CallEvent.kRemoteHoldUnhold) { - setState(() {}); - Logs().i( - 'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}'); - } - }); - _state = call.state; - - if (call.type == CallType.kVideo) { + proxy!.onStateChanged(_handleCallState); + if (!proxy!.voiceonly) { try { // Enable wakelock (keep screen on) unawaited(Wakelock.enable()); @@ -219,97 +246,93 @@ class _MyCallingPage extends State { void cleanUp() { Timer( const Duration(seconds: 2), - () => widget.onClear?.call(), + () => widget.onClear.call(), ); - if (call?.type == CallType.kVideo) { + if (!proxy!.voiceonly) { try { unawaited(Wakelock.disable()); } catch (_) {} } } - @override - void dispose() { - super.dispose(); - call?.cleanUp.call(); - } - void _resizeLocalVideo(Orientation orientation) { final shortSide = min( MediaQuery.of(context).size.width, MediaQuery.of(context).size.height); - _localVideoMargin = remoteStream != null + _localVideoMargin = userMediaStreams.isNotEmpty ? const EdgeInsets.only(top: 20.0, right: 20.0) : EdgeInsets.zero; - _localVideoWidth = remoteStream != null + _localVideoWidth = userMediaStreams.isNotEmpty ? shortSide / 3 : MediaQuery.of(context).size.width; - _localVideoHeight = remoteStream != null + _localVideoHeight = userMediaStreams.isNotEmpty ? shortSide / 4 : MediaQuery.of(context).size.height; } - void _handleCallState(CallState state) { - Logs().v('CallingPage::handleCallState: ${state.toString()}'); + void _handleCallState() { + Logs().v('CallingPage::handleCallState'); if (mounted) { setState(() { - _state = state; - if (_state == CallState.kEnded) cleanUp(); + if (proxy!.callState.toLowerCase() == 'ended') cleanUp(); }); } } - void _answerCall() { - setState(() { - call?.answer(); - }); + void handleAnswerButtonClick() { + if (mounted) { + setState(() { + proxy?.answer(); + }); + } + } + + void handleHangupButtonClick() { + _hangUp(); } void _hangUp() { setState(() { - if (call != null && (call?.isRinging ?? false)) { - call?.reject(); - } else { - call?.hangup(); - } + proxy!.hangup(); }); } - void _muteMic() { + void handleMicMuteButtonClick() { setState(() { - call?.setMicrophoneMuted(!call!.isMicrophoneMuted); + proxy?.setMicrophoneMuted(!isMicrophoneMuted); }); } - void _screenSharing() { + void handleScreenSharingButtonClick() { setState(() { - call?.setScreensharingEnabled(!call!.screensharingEnabled); + proxy?.setScreensharingEnabled(!isScreensharingEnabled); }); } - void _remoteOnHold() { + void handleHoldButtonClick() { setState(() { - call?.setRemoteOnHold(!call!.remoteOnHold); + proxy?.setRemoteOnHold(!isRemoteOnHold); }); } - void _muteCamera() { + void handleVideoMuteButtonClick() { setState(() { - call?.setLocalVideoMuted(!call!.isLocalVideoMuted); + proxy?.setLocalVideoMuted(!isLocalVideoMuted); }); } - void _switchCamera() async { - if (call!.localUserMediaStream != null) { - await Helper.switchCamera( - call!.localUserMediaStream!.stream!.getVideoTracks()[0]); - if (PlatformInfos.isMobile) { - call!.facingMode == 'user' - ? call!.facingMode = 'environment' - : call!.facingMode = 'user'; - } - } - setState(() {}); - } + // Waiting for https://gitlab.com/famedly/company/frontend/famedlysdk/-/issues/301 + // void _switchCamera() async { + // if (proxy!.localUserMediaStream != null) { + // await Helper.switchCamera( + // proxy!.localUserMediaStream!.stream!.getVideoTracks()[0]); + // if (PlatformInfos.isMobile) { + // proxy!.facingMode == 'user' + // ? proxy!.facingMode = 'environment' + // : proxy!.facingMode = 'user'; + // } + // } + // setState(() {}); + // } /* void _switchSpeaker() { @@ -320,16 +343,10 @@ class _MyCallingPage extends State { */ List _buildActionButtons(bool isFloating) { - if (isFloating || call == null) { + if (isFloating || proxy == null) { return []; } - final switchCameraButton = FloatingActionButton( - heroTag: 'switchCamera', - onPressed: _switchCamera, - backgroundColor: Colors.black45, - child: const Icon(Icons.switch_camera), - ); /* var switchSpeakerButton = FloatingActionButton( heroTag: 'switchSpeaker', @@ -339,106 +356,79 @@ class _MyCallingPage extends State { backgroundColor: Theme.of(context).backgroundColor, ); */ - final hangupButton = FloatingActionButton( - heroTag: 'hangup', - onPressed: _hangUp, - tooltip: 'Hangup', - backgroundColor: _state == CallState.kEnded ? Colors.black45 : Colors.red, - child: const Icon(Icons.call_end), - ); - - final answerButton = FloatingActionButton( - heroTag: 'answer', - onPressed: _answerCall, - tooltip: 'Answer', - backgroundColor: Colors.green, - child: const Icon(Icons.phone), - ); - - final muteMicButton = FloatingActionButton( - heroTag: 'muteMic', - onPressed: _muteMic, - foregroundColor: isMicrophoneMuted ? Colors.black26 : Colors.white, - backgroundColor: isMicrophoneMuted ? Colors.white : Colors.black45, - child: Icon(isMicrophoneMuted ? Icons.mic_off : Icons.mic), - ); - - final screenSharingButton = FloatingActionButton( - heroTag: 'screenSharing', - onPressed: _screenSharing, - foregroundColor: isScreensharingEnabled ? Colors.black26 : Colors.white, - backgroundColor: isScreensharingEnabled ? Colors.white : Colors.black45, - child: const Icon(Icons.desktop_mac), - ); - - final holdButton = FloatingActionButton( - heroTag: 'hold', - onPressed: _remoteOnHold, - foregroundColor: isRemoteOnHold ? Colors.black26 : Colors.white, - backgroundColor: isRemoteOnHold ? Colors.white : Colors.black45, - child: const Icon(Icons.pause), - ); - - final muteCameraButton = FloatingActionButton( - heroTag: 'muteCam', - onPressed: _muteCamera, - foregroundColor: isLocalVideoMuted ? Colors.black26 : Colors.white, - backgroundColor: isLocalVideoMuted ? Colors.white : Colors.black45, - child: Icon(isLocalVideoMuted ? Icons.videocam_off : Icons.videocam), - ); - - switch (_state) { - case CallState.kRinging: - case CallState.kInviteSent: - case CallState.kCreateAnswer: - case CallState.kConnecting: - return call!.isOutgoing - ? [hangupButton] - : [answerButton, hangupButton]; - case CallState.kConnected: - return [ - muteMicButton, - //switchSpeakerButton, - if (!voiceonly && !kIsWeb) switchCameraButton, - if (!voiceonly) muteCameraButton, - if (PlatformInfos.isMobile || PlatformInfos.isWeb) - screenSharingButton, - holdButton, - hangupButton, - ]; - case CallState.kEnded: - return [ - hangupButton, - ]; - case CallState.kFledgling: - // TODO: Handle this case. - break; - case CallState.kWaitLocalMedia: - // TODO: Handle this case. - break; - case CallState.kCreateOffer: - // TODO: Handle this case. - break; - case null: - // TODO: Handle this case. - break; - } - return []; + return [ + FloatingActionButton( + heroTag: 'hangup', + onPressed: handleHangupButtonClick, + tooltip: 'Hangup', + backgroundColor: proxy!.callState.toLowerCase() == 'ended' + ? Colors.black45 + : Colors.red, + child: const Icon(Icons.call_end), + ), + if (showAnswerButton) + FloatingActionButton( + heroTag: 'answer', + onPressed: handleAnswerButtonClick, + tooltip: 'Answer', + backgroundColor: Colors.green, + child: const Icon(Icons.phone), + ), + if (showMicMuteButton) + FloatingActionButton( + heroTag: 'muteMic', + onPressed: handleMicMuteButtonClick, + foregroundColor: isMicrophoneMuted ? Colors.black26 : Colors.white, + backgroundColor: isMicrophoneMuted ? Colors.white : Colors.black45, + child: Icon(isMicrophoneMuted ? Icons.mic_off : Icons.mic), + ), + if (showScreenSharingButton) + FloatingActionButton( + heroTag: 'screenSharing', + onPressed: handleScreenSharingButtonClick, + foregroundColor: + isScreensharingEnabled ? Colors.black26 : Colors.white, + backgroundColor: + isScreensharingEnabled ? Colors.white : Colors.black45, + child: const Icon(Icons.desktop_mac), + ), + if (showHoldButton) + FloatingActionButton( + heroTag: 'hold', + onPressed: handleHoldButtonClick, + foregroundColor: isRemoteOnHold ? Colors.black26 : Colors.white, + backgroundColor: isRemoteOnHold ? Colors.white : Colors.black45, + child: const Icon(Icons.pause), + ), + // FloatingActionButton( + // heroTag: 'switchCamera', + // onPressed: _switchCamera, + // backgroundColor: Colors.black45, + // child: const Icon(Icons.switch_camera), + // ), + if (showVideoMuteButton) + FloatingActionButton( + heroTag: 'muteCam', + onPressed: handleVideoMuteButtonClick, + foregroundColor: isLocalVideoMuted ? Colors.black26 : Colors.white, + backgroundColor: isLocalVideoMuted ? Colors.white : Colors.black45, + child: Icon(isLocalVideoMuted ? Icons.videocam_off : Icons.videocam), + ), + ]; } - List _buildContent(Orientation orientation, bool isFloating) { + List _buildP2PView(Orientation orientation, bool isFloating) { final stackWidgets = []; - final call = this.call; - if (call == null || call.callHasEnded) { + if (proxy == null || proxy!.ended) { return stackWidgets; } - if (call.localHold || call.remoteOnHold) { + if (proxy!.localHold || proxy!.remoteOnHold) { var title = ''; - if (call.localHold) { - title = '${call.displayName} held the call.'; - } else if (call.remoteOnHold) { + if (proxy!.localHold) { + title = '${proxy!.displayName} held the call.'; + } else if (proxy!.remoteOnHold) { title = 'You held the call.'; } stackWidgets.add(Center( @@ -460,20 +450,16 @@ class _MyCallingPage extends State { return stackWidgets; } - var primaryStream = call.remoteScreenSharingStream ?? - call.localScreenSharingStream ?? - call.remoteUserMediaStream ?? - call.localUserMediaStream; - - if (!connected) { - primaryStream = call.localUserMediaStream; - } - if (primaryStream != null) { - stackWidgets.add(Center( - child: _StreamView(primaryStream, - mainView: true, matrixClient: widget.client), - )); + stackWidgets.add( + Center( + child: _StreamView( + primaryStream!, + mainView: true, + matrixClient: voipPlugin.client, + ), + ), + ); } if (isFloating || !connected) { @@ -482,39 +468,20 @@ class _MyCallingPage extends State { _resizeLocalVideo(orientation); - if (call.getRemoteStreams.isEmpty) { + if (userMediaStreams.isEmpty) { return stackWidgets; } final secondaryStreamViews = []; - if (call.remoteScreenSharingStream != null) { - final remoteUserMediaStream = call.remoteUserMediaStream; + for (final stream in userMediaStreams) { secondaryStreamViews.add(SizedBox( width: _localVideoWidth, height: _localVideoHeight, - child: _StreamView(remoteUserMediaStream!, matrixClient: widget.client), - )); - secondaryStreamViews.add(const SizedBox(height: 10)); - } - - final localStream = - call.localUserMediaStream ?? call.localScreenSharingStream; - if (localStream != null && !isFloating) { - secondaryStreamViews.add(SizedBox( - width: _localVideoWidth, - height: _localVideoHeight, - child: _StreamView(localStream, matrixClient: widget.client), - )); - secondaryStreamViews.add(const SizedBox(height: 10)); - } - - if (call.localScreenSharingStream != null && !isFloating) { - secondaryStreamViews.add(SizedBox( - width: _localVideoWidth, - height: _localVideoHeight, - child: _StreamView(call.remoteUserMediaStream!, - matrixClient: widget.client), + child: _StreamView( + stream, + matrixClient: voipPlugin.client, + ), )); secondaryStreamViews.add(const SizedBox(height: 10)); } @@ -536,6 +503,8 @@ class _MyCallingPage extends State { return stackWidgets; } + WrappedMediaStream get screenSharing => screenSharingStreams.elementAt(0); + @override Widget build(BuildContext context) { return PIPView(builder: (context, isFloating) { @@ -544,11 +513,13 @@ class _MyCallingPage extends State { floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: SizedBox( - width: 320.0, - height: 150.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: _buildActionButtons(isFloating))), + width: 320.0, + height: 150.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: _buildActionButtons(isFloating), + ), + ), body: OrientationBuilder( builder: (BuildContext context, Orientation orientation) { return Container( @@ -556,13 +527,16 @@ class _MyCallingPage extends State { color: Colors.black87, ), child: Stack(children: [ - ..._buildContent(orientation, isFloating), + if (isGroupCall) + GroupCallView(call: proxy as GroupCallSessionState) + else + ..._buildP2PView(orientation, isFloating), if (!isFloating) Positioned( top: 24.0, left: 24.0, child: IconButton( - color: Colors.black45, + color: Colors.red, icon: const Icon(Icons.arrow_back), onPressed: () { PIPView.of(context)?.setFloating(true); diff --git a/lib/pages/dialer/group_call_view.dart b/lib/pages/dialer/group_call_view.dart new file mode 100644 index 00000000..258f9da9 --- /dev/null +++ b/lib/pages/dialer/group_call_view.dart @@ -0,0 +1,166 @@ +import 'package:fluffychat/utils/voip/group_call_state.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:matrix/matrix.dart'; + +class GroupCallView extends StatefulWidget { + final GroupCallSessionState call; + const GroupCallView({ + Key? key, + required this.call, + }) : super(key: key); + + @override + State createState() => _GroupCallViewState(); +} + +class _GroupCallViewState extends State { + WrappedMediaStream? get primaryStream => widget.call.primaryStream; + + List get screenSharingStreams => + widget.call.screenSharingStreams; + List get userMediaStreams => widget.call.userMediaStreams; + + WrappedMediaStream? get primaryScreenShare => + widget.call.screenSharingStreams.first; + + void updateStreams() { + Logs().i('Group calls, updating streams'); + widget.call.groupCall.onStreamAdd.stream.listen((event) { + if (event.purpose == SDPStreamMetadataPurpose.Usermedia) { + if (userMediaStreams.indexWhere((element) => element == event) == -1) { + setState(() { + userMediaStreams.add(event); + }); + } + } else if (event.purpose == SDPStreamMetadataPurpose.Screenshare) { + if (screenSharingStreams.indexWhere((element) => element == event) == + -1) { + setState(() { + screenSharingStreams.add(event); + }); + } + } + }); + widget.call.groupCall.onStreamRemoved.stream.listen((event) { + if (event.purpose == SDPStreamMetadataPurpose.Usermedia) { + userMediaStreams + .removeWhere((element) => element.stream!.id == event.stream!.id); + setState(() { + userMediaStreams.remove(event); + }); + } else if (event.purpose == SDPStreamMetadataPurpose.Screenshare) { + screenSharingStreams + .removeWhere((element) => element.stream!.id == event.stream!.id); + setState(() { + screenSharingStreams.remove(event); + }); + } + }); + } + + @override + void initState() { + updateStreams(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + Logs().e('Group call state: ' + widget.call.groupCall.state); + Logs().e('Group call state: ' + + widget.call.groupCall.participants.length.toString()); + + if (screenSharingStreams.isNotEmpty) { + return Center( + child: ListView( + children: [ + RTCVideoView( + primaryScreenShare!.renderer as RTCVideoRenderer, + mirror: primaryScreenShare!.isLocal(), + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, + ), + const SizedBox( + height: 100, + ), + CallGrid( + call: widget.call, + screenSharing: true, + userMediaStreams: userMediaStreams, + ), + ], + ), + ); + } else { + // No one is screen sharing, show avatars and user streams here + return Center( + child: CallGrid( + call: widget.call, + screenSharing: false, + userMediaStreams: userMediaStreams, + ), + ); + } + } +} + +class CallGrid extends StatefulWidget { + final GroupCallSessionState call; + final bool screenSharing; + final List userMediaStreams; + const CallGrid( + {Key? key, + required this.call, + required this.screenSharing, + required this.userMediaStreams}) + : super(key: key); + + @override + State createState() => _CallGridState(); +} + +class _CallGridState extends State { + @override + Widget build(BuildContext context) { + final client = Matrix.of(context).client; + return GridView.builder( + itemCount: widget.call.groupCall.participants.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.screenSharing ? 4 : 2), + itemBuilder: (context, index) { + final participant = widget.call.groupCall.participants[index]; + Logs().e('Group calls participants - ' + + participant.displayName.toString()); + if (widget.userMediaStreams + .map((stream) => stream.userId) + .contains(participant.id)) { + return Container( + color: Colors.amber, + child: RTCVideoView( + widget.userMediaStreams + .firstWhere((element) => element.userId == participant.id) + .renderer as RTCVideoRenderer, + mirror: false, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, + ), + ); + } else { + return Center( + child: Container( + height: 200, + width: 200, + color: Colors.red, + child: Avatar( + mxContent: participant.avatarUrl, + name: participant.displayName, + size: 24, + client: client, + ), + ), + ); + } + }); + } +} diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 78972589..3b11c79a 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -38,7 +38,7 @@ class InvitationSelectionController extends State { final participantsIds = participants.map((p) => p.stateKey).toList(); final contacts = client.rooms .where((r) => r.isDirectChat) - .map((r) => r.unsafeGetUserFromMemoryOrFallback(r.directChatMatrixID!)) + .map((r) => r.getUserByMXIDSync(r.directChatMatrixID!)) .toList() ..removeWhere((u) => participantsIds.contains(u.stateKey)); contacts.sort( diff --git a/lib/pages/key_verification/key_verification_dialog.dart b/lib/pages/key_verification/key_verification_dialog.dart index d9213178..53f20b7e 100644 --- a/lib/pages/key_verification/key_verification_dialog.dart +++ b/lib/pages/key_verification/key_verification_dialog.dart @@ -111,7 +111,7 @@ class _KeyVerificationPageState extends State { if (directChatId != null) { user = widget.request.client .getRoomById(directChatId)! - .unsafeGetUserFromMemoryOrFallback(widget.request.userId); + .getUserByMXIDSync(widget.request.userId); } final displayName = user?.calcDisplayname() ?? widget.request.userId.localpart!; diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index c2146f4d..e9316453 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -6,7 +6,6 @@ import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import '../../config/app_config.dart'; import '../../widgets/content_banner.dart'; import 'settings.dart'; diff --git a/lib/pages/story/story_page.dart b/lib/pages/story/story_page.dart index f6d200c3..f784eef5 100644 --- a/lib/pages/story/story_page.dart +++ b/lib/pages/story/story_page.dart @@ -370,7 +370,7 @@ class StoryPageController extends State { .client .getRoomById(roomId) ?.getState(EventTypes.RoomCreate) - ?.senderFromMemoryOrFallback + ?.sender .avatarUrl; String get title => @@ -378,7 +378,7 @@ class StoryPageController extends State { .client .getRoomById(roomId) ?.getState(EventTypes.RoomCreate) - ?.senderFromMemoryOrFallback + ?.sender .calcDisplayname() ?? 'Story not found'; @@ -485,8 +485,7 @@ class StoryPageController extends State { case PopupStoryAction.message: final roomIdResult = await showFutureLoadingDialog( context: context, - future: () => - currentEvent!.senderFromMemoryOrFallback.startDirectChat(), + future: () => currentEvent!.sender.startDirectChat(), ); if (roomIdResult.error != null) return; VRouter.of(context).toSegments(['rooms', roomIdResult.result!]); diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index cd79b0ab..649150bb 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -84,7 +84,13 @@ class BackgroundPush { client: client, l10n: l10n, activeRoomId: router?.currentState?.pathParameters['roomid'], - onSelectNotification: goToRoom, + onSelectNotification: (string) async { + if (string != null) { + final payload = jsonDecode(string); + final roomId = payload['roomId']; + goToRoom(roomId); + } + }, ), onNewToken: _newFcmToken, ); @@ -215,8 +221,6 @@ class BackgroundPush { } } - bool _wentToRoomOnStartup = false; - Future setupPush() async { Logs().d("SetupPush"); if (client.loginState != LoginState.loggedIn || @@ -235,20 +239,6 @@ class BackgroundPush { } else { await setupFirebase(); } - - // ignore: unawaited_futures - _flutterLocalNotificationsPlugin - .getNotificationAppLaunchDetails() - .then((details) { - if (details == null || - !details.didNotificationLaunchApp || - _wentToRoomOnStartup || - router == null) { - return; - } - _wentToRoomOnStartup = true; - goToRoom(details.payload); - }); } Future _noFcmWarning() async { @@ -384,6 +374,13 @@ class BackgroundPush { client: client, l10n: l10n, activeRoomId: router?.currentState?.pathParameters['roomid'], + onSelectNotification: (string) async { + if (string != null) { + final payload = jsonDecode(string); + final roomId = payload['roomId']; + goToRoom(roomId); + } + }, ); } diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index a042e6ee..60a966da 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -7,12 +7,11 @@ import 'package:matrix/encryption/utils/key_verification.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:fluffychat/utils/custom_http_client.dart'; import 'package:fluffychat/utils/custom_image_resizer.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions.dart/flutter_hive_collections_database.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'famedlysdk_store.dart'; import 'matrix_sdk_extensions.dart/fluffybox_database.dart'; +import 'matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart'; abstract class ClientManager { static const String clientNamespace = 'im.fluffychat.store.clients'; @@ -83,33 +82,29 @@ abstract class ClientManager { await Store().setItem(clientNamespace, jsonEncode(clientNamesList)); } - static Client createClient(String clientName) { - final _client = CustomHttpClient.createHTTPClient(); - return Client( - clientName, - httpClient: _client, - verificationMethods: { - KeyVerificationMethod.numbers, - if (kIsWeb || PlatformInfos.isMobile || PlatformInfos.isLinux) - KeyVerificationMethod.emoji, - }, - importantStateEvents: { - // To make room emotes work - 'im.ponies.room_emotes', - // To check which story room we can post in - EventTypes.RoomPowerLevels, - }, - databaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder, - legacyDatabaseBuilder: FlutterFluffyBoxDatabase.databaseBuilder, - supportedLoginTypes: { - AuthenticationTypes.password, - if (PlatformInfos.isMobile || - PlatformInfos.isWeb || - PlatformInfos.isMacOS) - AuthenticationTypes.sso - }, - compute: compute, - customImageResizer: PlatformInfos.isMobile ? customImageResizer : null, - ); - } + static Client createClient(String clientName) => Client( + clientName, + verificationMethods: { + KeyVerificationMethod.numbers, + if (kIsWeb || PlatformInfos.isMobile || PlatformInfos.isLinux) + KeyVerificationMethod.emoji, + }, + importantStateEvents: { + // To make room emotes work + 'im.ponies.room_emotes', + // To check which story room we can post in + EventTypes.RoomPowerLevels, + }, + databaseBuilder: FlutterFluffyBoxDatabase.databaseBuilder, + legacyDatabaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder, + supportedLoginTypes: { + AuthenticationTypes.password, + if (PlatformInfos.isMobile || + PlatformInfos.isWeb || + PlatformInfos.isMacOS) + AuthenticationTypes.sso + }, + compute: compute, + customImageResizer: PlatformInfos.isMobile ? customImageResizer : null, + ); } diff --git a/lib/utils/custom_http_client.dart b/lib/utils/custom_http_client.dart deleted file mode 100644 index 479e5edb..00000000 --- a/lib/utils/custom_http_client.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:http/http.dart' as http; -import 'package:http/io_client.dart'; - -import 'package:fluffychat/config/isrg_x1.dart'; - -class CustomHttpClient { - static HttpClient customHttpClient(String? cert) { - final context = SecurityContext.defaultContext; - - try { - if (cert != null) { - final bytes = utf8.encode(cert); - context.setTrustedCertificatesBytes(bytes); - } - } on TlsException catch (e) { - if (e.osError != null && - e.osError!.message.contains('CERT_ALREADY_IN_HASH_TABLE')) { - } else { - rethrow; - } - } - - return HttpClient(context: context); - } - - static http.Client createHTTPClient() => IOClient(customHttpClient(ISRG_X1)); -} diff --git a/lib/utils/fluffy_share.dart b/lib/utils/fluffy_share.dart index 630803b3..052198cc 100644 --- a/lib/utils/fluffy_share.dart +++ b/lib/utils/fluffy_share.dart @@ -9,11 +9,7 @@ import 'package:fluffychat/utils/platform_infos.dart'; abstract class FluffyShare { static Future share(String text, BuildContext context) async { if (PlatformInfos.isMobile) { - final box = context.findRenderObject() as RenderBox; - return Share.share( - text, - sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size, - ); + return Share.share(text); } await Clipboard.setData( ClipboardData(text: text), diff --git a/lib/utils/matrix_sdk_extensions.dart/client_stories_extension.dart b/lib/utils/matrix_sdk_extensions.dart/client_stories_extension.dart index 269de73c..f2e1627a 100644 --- a/lib/utils/matrix_sdk_extensions.dart/client_stories_extension.dart +++ b/lib/utils/matrix_sdk_extensions.dart/client_stories_extension.dart @@ -12,8 +12,7 @@ extension ClientStoriesExtension on Client { List get contacts => rooms .where((room) => room.isDirectChat) - .map((room) => - room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!)) + .map((room) => room.getUserByMXIDSync(room.directChatMatrixID!)) .toList(); List get storiesRooms => rooms diff --git a/lib/utils/matrix_sdk_extensions.dart/fluffybox_database.dart b/lib/utils/matrix_sdk_extensions.dart/fluffybox_database.dart index c2e938fd..ef7bd00a 100644 --- a/lib/utils/matrix_sdk_extensions.dart/fluffybox_database.dart +++ b/lib/utils/matrix_sdk_extensions.dart/fluffybox_database.dart @@ -14,7 +14,6 @@ import 'package:path_provider/path_provider.dart'; import '../client_manager.dart'; import '../famedlysdk_store.dart'; -// ignore: deprecated_member_use class FlutterFluffyBoxDatabase extends FluffyBoxDatabase { FlutterFluffyBoxDatabase( String name, @@ -28,7 +27,6 @@ class FlutterFluffyBoxDatabase extends FluffyBoxDatabase { static const String _cipherStorageKey = 'database_encryption_key'; - // ignore: deprecated_member_use static Future databaseBuilder(Client client) async { Logs().d('Open FluffyBox...'); fluffybox.HiveAesCipher? hiverCipher; @@ -61,7 +59,6 @@ class FlutterFluffyBoxDatabase extends FluffyBoxDatabase { rethrow; } - // ignore: deprecated_member_use final db = FluffyBoxDatabase( 'fluffybox_${client.clientName.replaceAll(' ', '_').toLowerCase()}', await _findDatabasePath(client), diff --git a/lib/utils/matrix_sdk_extensions.dart/flutter_hive_collections_database.dart b/lib/utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart similarity index 50% rename from lib/utils/matrix_sdk_extensions.dart/flutter_hive_collections_database.dart rename to lib/utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart index 0f775672..9879ff9c 100644 --- a/lib/utils/matrix_sdk_extensions.dart/flutter_hive_collections_database.dart +++ b/lib/utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart @@ -2,108 +2,78 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter/foundation.dart' hide Key; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; -class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { - FlutterHiveCollectionsDatabase( - String name, - String path, { - HiveCipher? key, - }) : super( +import '../platform_infos.dart'; + +class FlutterMatrixHiveStore extends FamedlySdkHiveDatabase { + FlutterMatrixHiveStore(String name, {HiveCipher? encryptionCipher}) + : super( name, - path, - key: key, + encryptionCipher: encryptionCipher, ); - static const String _cipherStorageKey = 'database_encryption_key'; + static bool _hiveInitialized = false; + static const String _hiveCipherStorageKey = 'hive_encryption_key'; - static Future databaseBuilder( + static Future hiveDatabaseBuilder( Client client) async { - Logs().d('Open Hive...'); - HiveAesCipher? hiverCipher; + if (!kIsWeb && !_hiveInitialized) { + _hiveInitialized = true; + } + HiveCipher? hiverCipher; try { // Workaround for secure storage is calling Platform.operatingSystem on web - if (kIsWeb) throw MissingPluginException(); + if (kIsWeb || Platform.isLinux) throw MissingPluginException(); const secureStorage = FlutterSecureStorage(); final containsEncryptionKey = - await secureStorage.containsKey(key: _cipherStorageKey); + await secureStorage.containsKey(key: _hiveCipherStorageKey); if (!containsEncryptionKey) { - // do not try to create a buggy secure storage for new Linux users - if (Platform.isLinux) throw MissingPluginException(); final key = Hive.generateSecureKey(); await secureStorage.write( - key: _cipherStorageKey, + key: _hiveCipherStorageKey, value: base64UrlEncode(key), ); } // workaround for if we just wrote to the key and it still doesn't exist - final rawEncryptionKey = await secureStorage.read(key: _cipherStorageKey); + final rawEncryptionKey = + await secureStorage.read(key: _hiveCipherStorageKey); if (rawEncryptionKey == null) throw MissingPluginException(); - hiverCipher = HiveAesCipher(base64Url.decode(rawEncryptionKey)); + final encryptionKey = base64Url.decode(rawEncryptionKey); + hiverCipher = HiveAesCipher(encryptionKey); } on MissingPluginException catch (_) { Logs().i('Hive encryption is not supported on this platform'); - } catch (_) { - const FlutterSecureStorage().delete(key: _cipherStorageKey); - rethrow; } - - final db = FlutterHiveCollectionsDatabase( - 'hive_collections_${client.clientName.replaceAll(' ', '_').toLowerCase()}', - await _findDatabasePath(client), - key: hiverCipher, + final db = FlutterMatrixHiveStore( + client.clientName, + encryptionCipher: hiverCipher, ); try { await db.open(); - } catch (_) { - Logs().w('Unable to open Hive. Delete database and storage key...'); - const FlutterSecureStorage().delete(key: _cipherStorageKey); + } catch (e, s) { + Logs().e('Unable to open Hive. Delete and try again...', e, s); await db.clear(); - rethrow; + await db.open(); } - Logs().d('Hive is ready'); return db; } - static Future _findDatabasePath(Client client) async { - String path = client.clientName; - if (!kIsWeb) { - Directory directory; - try { - if (Platform.isLinux) { - directory = await getApplicationSupportDirectory(); - } else { - directory = await getApplicationDocumentsDirectory(); - } - } catch (_) { - try { - directory = await getLibraryDirectory(); - } catch (_) { - directory = Directory.current; - } - } - // do not destroy your stable FluffyChat in debug mode - if (kDebugMode) { - directory = Directory(directory.uri.resolve('debug').toFilePath()); - directory.create(recursive: true); - } - path = directory.path; - } - return path; - } - @override int get maxFileSize => supportsFileStoring ? 100 * 1024 * 1024 : 0; @override - bool get supportsFileStoring => !kIsWeb; + bool get supportsFileStoring => (PlatformInfos.isIOS || + PlatformInfos.isAndroid || + PlatformInfos.isDesktop); Future _getFileStoreDirectory() async { try { diff --git a/lib/utils/matrix_sdk_extensions.dart/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions.dart/matrix_file_extension.dart index 17ebd339..bfeefe8a 100644 --- a/lib/utils/matrix_sdk_extensions.dart/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions.dart/matrix_file_extension.dart @@ -25,11 +25,7 @@ extension MatrixFileExtension on MatrixFile { final tmpDirectory = await getTemporaryDirectory(); final path = '${tmpDirectory.path}$fileName'; await File(path).writeAsBytes(bytes); - final box = context.findRenderObject() as RenderBox; - await Share.shareFiles( - [path], - sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size, - ); + await Share.shareFiles([path]); return; } diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index f21544fe..a1360415 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:matrix/matrix.dart'; @@ -10,6 +12,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/utils/famedlysdk_store.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart'; Future pushHelper( @@ -32,7 +35,6 @@ Future pushHelper( Logs().v('Room is in foreground. Stop push helper here.'); return; } - // Initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project final _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); await _flutterLocalNotificationsPlugin.initialize( @@ -56,8 +58,11 @@ Future pushHelper( await store.setString(SettingKeys.notificationCurrentIds, json.encode({})); return; } - Logs().v('Push helper got notification event.'); - + Logs().v('Push helper got notification event ${event.type}'); + if (event.type.startsWith('m.call') && event.type == EventTypes.CallHangup) { + Logs().i('Removing non invite call notificaitons'); + return; + } l10n ??= await L10n.delegate.load(window.locale); final matrixLocals = MatrixLocals(l10n); @@ -81,8 +86,9 @@ Future pushHelper( .toString(); final avatarFile = avatar == null ? null : await DefaultCacheManager().getSingleFile(avatar); - + final bool isCall = event.type != EventTypes.CallHangup; // TODO: handle this properly // Show notification + const insistentFlag = 4; final androidPlatformChannelSpecifics = AndroidNotificationDetails( AppConfig.pushNotificationsChannelId, AppConfig.pushNotificationsChannelName, @@ -106,8 +112,11 @@ Future pushHelper( ), ticker: l10n.unreadChats(notification.counts?.unread ?? 1), importance: Importance.max, - priority: Priority.high, - groupKey: event.room.id, + priority: Priority.max, + category: isCall ? 'call' : 'msg', + fullScreenIntent: isCall ? true : false, + additionalFlags: isCall ? Int32List.fromList([insistentFlag]) : null, + groupKey: event.roomId, ); const iOSPlatformChannelSpecifics = IOSNotificationDetails(); final platformChannelSpecifics = NotificationDetails( @@ -119,8 +128,25 @@ Future pushHelper( event.room.displayname, body, platformChannelSpecifics, - payload: event.roomId, + payload: '{"roomId": "${event.roomId}", "eventType": "${event.type}"}', ); + Logs().d(event.type); + if (isCall) { + Logs().i('VOIP will now try to go to foreground'); + if (!await FlutterForegroundTask.canDrawOverlays) { + FlutterForegroundTask.openSystemAlertWindowSettings(); + } + try { + final wasForeground = await FlutterForegroundTask.isAppOnForeground(); + await Store().setItemBool('wasForeground', wasForeground ?? true); + FlutterForegroundTask.setOnLockScreenVisibility(true); + FlutterForegroundTask.wakeUpScreen(); + FlutterForegroundTask.launchApp(); + } catch (e) { + Logs().e('VOIP foreground failed $e'); + } + } + Logs().v('Push helper has been completed!'); } diff --git a/lib/utils/voip/call_session_state.dart b/lib/utils/voip/call_session_state.dart new file mode 100644 index 00000000..b932ea74 --- /dev/null +++ b/lib/utils/voip/call_session_state.dart @@ -0,0 +1,191 @@ +import 'package:fluffychat/utils/voip/call_state_proxy.dart'; +import 'package:matrix/matrix.dart'; + +class CallSessionState implements CallStateProxy { + final CallSession call; + Function()? callback; + CallSessionState(this.call) { + call.onCallEventChanged.stream.listen((CallEvent event) { + if (event == CallEvent.kState || + event == CallEvent.kFeedsChanged || + event == CallEvent.kLocalHoldUnhold || + event == CallEvent.kRemoteHoldUnhold) { + if (event == CallEvent.kFeedsChanged) { + call.tryRemoveStopedStreams(); + } + callback?.call(); + } + }); + call.onCallStateChanged.stream.listen((state) { + callback?.call(); + }); + } + + @override + bool get voiceonly => call.type == CallType.kVoice; + + @override + bool get connecting => call.state == CallState.kConnecting; + + @override + bool get connected => call.state == CallState.kConnected; + + @override + bool get ended => call.state == CallState.kEnded; + + @override + bool get isOutgoing => call.isOutgoing; + + @override + bool get ringingPlay => call.state == CallState.kInviteSent; + + @override + void answer() => call.answer(); + + @override + void enter() { + // TODO: implement enter + } + + @override + void hangup() { + if (call.isRinging) { + call.reject(); + } else { + call.hangup(); + } + } + + @override + bool get isLocalVideoMuted => call.isLocalVideoMuted; + + @override + bool get isMicrophoneMuted => call.isMicrophoneMuted; + + @override + bool get isRemoteOnHold => call.remoteOnHold; + + @override + bool get localHold => call.localHold; + + @override + bool get remoteOnHold => call.remoteOnHold; + + @override + bool get isScreensharingEnabled => call.screensharingEnabled; + + @override + bool get callOnHold => call.localHold || call.remoteOnHold; + + @override + void setLocalVideoMuted(bool muted) { + call.setLocalVideoMuted(muted); + callback?.call(); + } + + @override + void setMicrophoneMuted(bool muted) { + call.setMicrophoneMuted(muted); + // TODO(Nico): Refactor this to be more granular + callback?.call(); + } + + @override + void setRemoteOnHold(bool onHold) { + call.setRemoteOnHold(onHold); + callback?.call(); + } + + @override + void setScreensharingEnabled(bool enabled) { + call.setScreensharingEnabled(enabled); + callback?.call(); + } + + @override + String get callState { + switch (call.state) { + case CallState.kCreateAnswer: + case CallState.kFledgling: + case CallState.kWaitLocalMedia: + case CallState.kCreateOffer: + break; + case CallState.kRinging: + state = 'Ringing'; + break; + case CallState.kInviteSent: + state = 'Invite Sent'; + break; + case CallState.kConnecting: + state = 'Connecting'; + break; + case CallState.kConnected: + state = 'Connected'; + break; + case CallState.kEnded: + state = 'Ended'; + break; + } + return state; + } + + String state = 'New Call'; + @override + WrappedMediaStream? get localUserMediaStream => call.localUserMediaStream; + @override + WrappedMediaStream? get localScreenSharingStream => + call.localScreenSharingStream; + + @override + List get screenSharingStreams { + final streams = []; + if (connected) { + if (call.remoteScreenSharingStream != null) { + streams.add(call.remoteScreenSharingStream!); + } + if (call.localScreenSharingStream != null) { + streams.add(call.localScreenSharingStream!); + } + } + return streams; + } + + @override + List get userMediaStreams { + final streams = []; + if (connected) { + if (call.remoteUserMediaStream != null) { + streams.add(call.remoteUserMediaStream!); + } + if (call.localUserMediaStream != null) { + streams.add(call.localUserMediaStream!); + } + } + return streams; + } + + @override + WrappedMediaStream? get primaryStream { + if (screenSharingStreams.isNotEmpty) { + return screenSharingStreams.first; + } + + if (userMediaStreams.isNotEmpty) { + return userMediaStreams.first; + } + + if (!connected) { + return call.localUserMediaStream; + } + + return call.localScreenSharingStream ?? call.localUserMediaStream; + } + + @override + String? get displayName => call.displayName; + + @override + void onStateChanged(Function() handler) { + callback = handler; + } +} diff --git a/lib/utils/voip/call_state_proxy.dart b/lib/utils/voip/call_state_proxy.dart new file mode 100644 index 00000000..7b690abf --- /dev/null +++ b/lib/utils/voip/call_state_proxy.dart @@ -0,0 +1,45 @@ +import 'package:matrix/matrix.dart'; + +abstract class CallStateProxy { + String? get displayName; + bool get isMicrophoneMuted; + bool get isLocalVideoMuted; + bool get isScreensharingEnabled; + bool get isRemoteOnHold; + bool get localHold; + bool get remoteOnHold; + bool get voiceonly; + bool get connecting; + bool get connected; + bool get ended; + bool get callOnHold; + bool get isOutgoing; + bool get ringingPlay; + String get callState; + + void answer(); + + void hangup(); + + void enter(); + + void setMicrophoneMuted(bool muted); + + void setLocalVideoMuted(bool muted); + + void setScreensharingEnabled(bool enabled); + + void setRemoteOnHold(bool onHold); + + WrappedMediaStream? get localUserMediaStream; + + WrappedMediaStream? get localScreenSharingStream; + + WrappedMediaStream? get primaryStream; + + List get screenSharingStreams; + + List get userMediaStreams; + + void onStateChanged(Function() callback); +} diff --git a/lib/utils/voip/callkeep_manager.dart b/lib/utils/voip/callkeep_manager.dart deleted file mode 100644 index ca0d4473..00000000 --- a/lib/utils/voip/callkeep_manager.dart +++ /dev/null @@ -1,312 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:callkeep/callkeep.dart'; -import 'package:matrix/matrix.dart'; -import 'package:uuid/uuid.dart'; - -import 'package:fluffychat/utils/voip_plugin.dart'; - -class CallKeeper { - CallKeeper(this.callKeepManager, this.uuid, this.number, this.call) { - call?.onCallStateChanged.listen(_handleCallState); - } - - CallKeepManager callKeepManager; - String number; - String uuid; - bool held = false; - bool muted = false; - bool connected = false; - CallSession? call; - - void _handleCallState(CallState state) { - Logs().v('CallKeepManager::handleCallState: ${state.toString()}'); - switch (state) { - case CallState.kConnecting: - break; - case CallState.kConnected: - if (!connected) { - callKeepManager.answer(uuid); - } else { - callKeepManager.setMutedCall(uuid, false); - callKeepManager.setOnHold(uuid, false); - } - break; - case CallState.kEnded: - callKeepManager.hangup(uuid); - break; - /* TODO: - case CallState.kMuted: - callKeepManager.setMutedCall(uuid, true); - break; - case CallState.kHeld: - callKeepManager.setOnHold(uuid, true); - break; - */ - case CallState.kFledgling: - // TODO: Handle this case. - break; - case CallState.kInviteSent: - // TODO: Handle this case. - break; - case CallState.kWaitLocalMedia: - // TODO: Handle this case. - break; - case CallState.kCreateOffer: - // TODO: Handle this case. - break; - case CallState.kCreateAnswer: - // TODO: Handle this case. - break; - case CallState.kRinging: - // TODO: Handle this case. - break; - } - } -} - -class CallKeepManager { - factory CallKeepManager() { - return _instance; - } - - CallKeepManager._internal() { - _callKeep = FlutterCallkeep(); - } - - static final CallKeepManager _instance = CallKeepManager._internal(); - - late FlutterCallkeep _callKeep; - VoipPlugin? _voipPlugin; - Map calls = {}; - - String newUUID() => const Uuid().v4(); - - String get appName => 'Famedly'; - - Map get alertOptions => { - 'alertTitle': 'Permissions required', - 'alertDescription': '$appName needs to access your phone accounts!', - 'cancelButton': 'Cancel', - 'okButton': 'ok', - // Required to get audio in background when using Android 11 - 'foregroundService': { - 'channelId': 'com.famedly.talk', - 'channelName': 'Foreground service for my app', - 'notificationTitle': '$appName is running on background', - 'notificationIcon': 'mipmap/ic_notification_launcher', - }, - }; - - void setVoipPlugin(VoipPlugin plugin) { - if (kIsWeb) { - throw 'Not support callkeep for flutter web'; - } - _voipPlugin = plugin; - _voipPlugin!.onIncomingCall = (CallSession call) async { - await _callKeep.setup( - null, - { - 'ios': { - 'appName': appName, - }, - 'android': alertOptions, - }, - backgroundMode: true); - - await displayIncomingCall(call); - - call.onCallStateChanged.listen((state) { - if (state == CallState.kEnded) { - _callKeep.endAllCalls(); - } - }); - call.onCallEventChanged.listen((event) { - if (event == CallEvent.kLocalHoldUnhold) { - Logs().i( - 'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}'); - } - }); - }; - } - - void removeCall(String callUUID) { - calls.remove(callUUID); - } - - void addCall(String callUUID, CallKeeper callKeeper) { - calls[callUUID] = callKeeper; - } - - String findCallUUID(String number) { - var uuid = ''; - calls.forEach((String key, CallKeeper item) { - if (item.number == number) { - uuid = key; - return; - } - }); - return uuid; - } - - void setCallHeld(String callUUID, bool held) { - calls[callUUID]!.held = held; - } - - void setCallMuted(String callUUID, bool muted) { - calls[callUUID]!.muted = muted; - } - - void didDisplayIncomingCall(CallKeepDidDisplayIncomingCall event) { - final callUUID = event.callUUID; - final number = event.handle; - Logs().v('[displayIncomingCall] $callUUID number: $number'); - addCall(callUUID!, CallKeeper(this, callUUID, number!, null)); - } - - void onPushKitToken(CallKeepPushKitToken event) { - Logs().v('[onPushKitToken] token => ${event.token}'); - } - - Future initialize() async { - _callKeep.on(CallKeepPerformAnswerCallAction(), answerCall); - _callKeep.on(CallKeepDidPerformDTMFAction(), didPerformDTMFAction); - _callKeep.on( - CallKeepDidReceiveStartCallAction(), didReceiveStartCallAction); - _callKeep.on(CallKeepDidToggleHoldAction(), didToggleHoldCallAction); - _callKeep.on( - CallKeepDidPerformSetMutedCallAction(), didPerformSetMutedCallAction); - _callKeep.on(CallKeepPerformEndCallAction(), endCall); - _callKeep.on(CallKeepPushKitToken(), onPushKitToken); - _callKeep.on(CallKeepDidDisplayIncomingCall(), didDisplayIncomingCall); - } - - Future hangup(String callUUID) async { - await _callKeep.endCall(callUUID); - removeCall(callUUID); - } - - Future reject(String callUUID) async { - await _callKeep.rejectCall(callUUID); - } - - Future answer(String callUUID) async { - final keeper = calls[callUUID]; - if (!keeper!.connected) { - await _callKeep.answerIncomingCall(callUUID); - keeper.connected = true; - } - } - - Future setOnHold(String callUUID, bool held) async { - await _callKeep.setOnHold(callUUID, held); - setCallHeld(callUUID, held); - } - - Future setMutedCall(String callUUID, bool muted) async { - await _callKeep.setMutedCall(callUUID, muted); - setCallMuted(callUUID, muted); - } - - Future updateDisplay(String callUUID) async { - final number = calls[callUUID]!.number; - // Workaround because Android doesn't display well displayName, se we have to switch ... - if (isIOS) { - await _callKeep.updateDisplay(callUUID, - displayName: 'New Name', handle: number); - } else { - await _callKeep.updateDisplay(callUUID, - displayName: number, handle: 'New Name'); - } - } - - Future displayIncomingCall(CallSession call) async { - final callUUID = newUUID(); - final callKeeper = CallKeeper(this, callUUID, call.displayName!, call); - addCall(callUUID, callKeeper); - await _callKeep.displayIncomingCall(callUUID, call.displayName!, - handleType: 'number', hasVideo: call.type == CallType.kVideo); - return callKeeper; - } - - Future checkoutPhoneAccountSetting(BuildContext context) async { - await _callKeep.setup(context, { - 'ios': { - 'appName': appName, - }, - 'android': alertOptions, - }); - final hasPhoneAccount = await _callKeep.hasPhoneAccount(); - if (!hasPhoneAccount) { - await _callKeep.hasDefaultPhoneAccount(context, alertOptions); - } - } - - /// CallActions. - Future answerCall(CallKeepPerformAnswerCallAction event) async { - final callUUID = event.callUUID; - final keeper = calls[event.callUUID]!; - if (!keeper.connected) { - // Answer Call - keeper.call!.answer(); - keeper.connected = true; - } - Timer(const Duration(seconds: 1), () { - _callKeep.setCurrentCallActive(callUUID!); - }); - } - - Future endCall(CallKeepPerformEndCallAction event) async { - final keeper = calls[event.callUUID]; - keeper?.call?.hangup(); - removeCall(event.callUUID!); - } - - Future didPerformDTMFAction(CallKeepDidPerformDTMFAction event) async { - final keeper = calls[event.callUUID]!; - keeper.call?.sendDTMF(event.digits!); - } - - Future didReceiveStartCallAction( - CallKeepDidReceiveStartCallAction event) async { - if (event.handle == null) { - // @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined` - return; - } - final callUUID = event.callUUID ?? newUUID(); - if (event.callUUID == null) { - final call = - await _voipPlugin!.voip.inviteToCall(event.handle!, CallType.kVideo); - addCall(callUUID, CallKeeper(this, callUUID, call.displayName!, call)); - } - await _callKeep.startCall(callUUID, event.handle!, event.handle!); - Timer(const Duration(seconds: 1), () { - _callKeep.setCurrentCallActive(callUUID); - }); - } - - Future didPerformSetMutedCallAction( - CallKeepDidPerformSetMutedCallAction event) async { - final keeper = calls[event.callUUID]!; - if (event.muted ?? false) { - keeper.call?.setMicrophoneMuted(true); - } else { - keeper.call?.setMicrophoneMuted(false); - } - setCallMuted(event.callUUID!, event.muted!); - } - - Future didToggleHoldCallAction( - CallKeepDidToggleHoldAction event) async { - final keeper = calls[event.callUUID]!; - if (event.hold ?? false) { - keeper.call?.setRemoteOnHold(true); - } else { - keeper.call?.setRemoteOnHold(false); - } - setCallHeld(event.callUUID!, event.hold!); - } -} diff --git a/lib/utils/voip/group_call_state.dart b/lib/utils/voip/group_call_state.dart new file mode 100644 index 00000000..a4b44a96 --- /dev/null +++ b/lib/utils/voip/group_call_state.dart @@ -0,0 +1,123 @@ +import 'package:fluffychat/utils/voip/call_state_proxy.dart'; +import 'package:matrix/matrix.dart'; + +class GroupCallSessionState implements CallStateProxy { + Function()? callback; + final GroupCall groupCall; + GroupCallSessionState(this.groupCall) { + groupCall.onGroupCallEvent.stream.listen((event) { + Logs().i("onGroupCallEvent ${event.toString()}"); + callback?.call(); + }); + groupCall.onGroupCallState.stream.listen((event) { + Logs().i("onGroupCallState ${event.toString()}"); + callback?.call(); + }); + } + + @override + void answer() { + // TODO: implement answer + } + + @override + bool get callOnHold => false; + + @override + String get callState => 'New Group Call...'; + + @override + bool get connected => groupCall.state == GroupCallState.Entered; + + @override + bool get connecting => groupCall.state == GroupCallState.Entering; + + @override + String? get displayName => groupCall.displayName; + + @override + bool get ended => + groupCall.state == GroupCallState.Ended || + groupCall.state == GroupCallState.LocalCallFeedUninitialized; + + @override + void enter() async { + await groupCall.initLocalStream(); + groupCall.enter(); + } + + @override + void hangup() async { + groupCall.leave(); + } + + @override + bool get isLocalVideoMuted => groupCall.isLocalVideoMuted; + + @override + bool get isMicrophoneMuted => groupCall.isMicrophoneMuted; + + @override + bool get isOutgoing => false; + + @override + bool get isRemoteOnHold => false; + + @override + bool get isScreensharingEnabled => groupCall.isScreensharing(); + + @override + bool get localHold => false; + + @override + WrappedMediaStream? get localScreenSharingStream => + groupCall.localScreenshareStream; + + @override + WrappedMediaStream? get localUserMediaStream => + groupCall.localUserMediaStream; + + @override + void onStateChanged(Function() handler) { + callback = handler; + } + + @override + WrappedMediaStream? get primaryStream => groupCall.localUserMediaStream; + + @override + bool get remoteOnHold => false; + + @override + bool get ringingPlay => false; + + @override + List get screenSharingStreams => + groupCall.screenshareStreams; + + @override + List get userMediaStreams => groupCall.userMediaStreams; + + @override + void setLocalVideoMuted(bool muted) { + groupCall.setLocalVideoMuted(muted); + } + + @override + void setMicrophoneMuted(bool muted) { + groupCall.setMicrophoneMuted(muted); + } + + @override + void setRemoteOnHold(bool onHold) { + // TODO: implement setRemoteOnHold + } + + @override + void setScreensharingEnabled(bool enabled) { + groupCall.setScreensharingEnabled(enabled, ''); + } + + @override + bool get voiceonly => false; +} diff --git a/lib/utils/voip_plugin.dart b/lib/utils/voip_plugin.dart index 101ccd45..7610410f 100644 --- a/lib/utils/voip_plugin.dart +++ b/lib/utils/voip_plugin.dart @@ -4,11 +4,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as webrtc_impl; import 'package:matrix/matrix.dart'; import 'package:webrtc_interface/webrtc_interface.dart' hide Navigator; import 'package:fluffychat/pages/dialer/dialer.dart'; +import 'package:fluffychat/utils/famedlysdk_store.dart'; import '../../utils/voip/user_media_manager.dart'; class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate { @@ -46,6 +48,43 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate { // // hours wasted: 5 BuildContext context; + CallSession? get currentCall => + voip.currentCID == null ? null : voip.calls[voip.currentCID]; + + GroupCall? get currentGroupCall => voip.currentGroupCID == null + ? null + : voip.groupCalls[voip.currentGroupCID]; + + bool inMeeting = false; + + void start() { + voip.startGroupCalls(); + } + + bool getInMeetingState() { + return currentCall != null || currentGroupCall != null; + } + + String? get currentMeetingRoomId { + return currentCall?.room.id ?? currentGroupCall?.room.id; + } + + CallSession? get call { + if (voip.currentCID != null) { + final call = voip.calls[voip.currentCID]; + if (call != null && call.groupCallId != null) { + return call; + } + } + return null; + } + + GroupCall? get groupCall { + if (voip.currentCID != null) { + return voip.groupCalls[voip.currentGroupCID]; + } + return null; + } void _handleNetworkChanged(ConnectivityResult result) async { /// Got a new connectivity status! @@ -64,8 +103,8 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate { state != AppLifecycleState.paused); } - void addCallingOverlay( - BuildContext context, String callId, CallSession call) { + void addCallingOverlay(BuildContext context) { + Logs().d('[VOIP] addCallingOverlay: adding overlay'); if (overlayEntry != null) { Logs().w('[VOIP] addCallingOverlay: The call session already exists?'); overlayEntry?.remove(); @@ -76,20 +115,14 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate { showDialog( context: context, builder: (context) => Calling( - context: context, - client: client, - callId: callId, - call: call, + voipPlugin: this, onClear: () => Navigator.of(context).pop(), ), ); } else { overlayEntry = OverlayEntry( builder: (_) => Calling( - context: context, - client: client, - callId: callId, - call: call, + voipPlugin: this, onClear: () { overlayEntry?.remove(); overlayEntry = null; @@ -97,6 +130,7 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate { ); Overlay.of(context)!.insert(overlayEntry!); } + Logs().d('[VOIP] addCallingOverlay: adding done'); } @override @@ -139,12 +173,11 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate { @override void handleNewCall(CallSession call) async { + Logs().d('[VOIP] handling new call'); + /// Popup CallingPage for incoming call. - if (!background) { - addCallingOverlay(context, call.callId, call); - } else { - onIncomingCall?.call(call); - } + addCallingOverlay(context); + Logs().d('[VOIP] overlay stuff should be there up there'); } @override @@ -152,6 +185,29 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate { if (overlayEntry != null) { overlayEntry?.remove(); overlayEntry = null; + FlutterForegroundTask.setOnLockScreenVisibility(false); + final wasForeground = await Store().getItemBool('wasForeground'); + !wasForeground ? FlutterForegroundTask.minimizeApp() : null; + } + } + + @override + void handleNewGroupCall(GroupCall groupCall) { + Logs().d('[VOIP] group handling new call'); + + /// Popup CallingPage for incoming call. + addCallingOverlay(context); + Logs().d('[VOIP] group overlay stuff should be there up there'); + } + + @override + void handleGroupCallEnded(GroupCall groupCall) { + if (overlayEntry != null) { + overlayEntry?.remove(); + overlayEntry = null; + // FlutterForegroundTask.setOnLockScreenVisibility(false); + // final wasForeground = await Store().getItemBool('wasForeground'); + // !wasForeground ? FlutterForegroundTask.minimizeApp() : null; } } } diff --git a/lib/widgets/local_notifications_extension.dart b/lib/widgets/local_notifications_extension.dart index 37e8fbcf..81bc5fc4 100644 --- a/lib/widgets/local_notifications_extension.dart +++ b/lib/widgets/local_notifications_extension.dart @@ -31,7 +31,7 @@ extension LocalNotificationsExtension on MatrixState { final event = Event.fromJson(eventUpdate.content, room); final title = room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)!)); - final body = await event.calcLocalizedBody( + final body = event.getLocalizedBody( MatrixLocals(L10n.of(widget.context)!), withSenderNamePrefix: !room.isDirectChat || room.lastEvent?.senderId == client.userID, @@ -40,11 +40,8 @@ extension LocalNotificationsExtension on MatrixState { hideEdit: true, removeMarkdown: true, ); - final icon = event.senderFromMemoryOrFallback.avatarUrl?.getThumbnail( - client, - width: 64, - height: 64, - method: ThumbnailMethod.crop) ?? + final icon = event.sender.avatarUrl?.getThumbnail(client, + width: 64, height: 64, method: ThumbnailMethod.crop) ?? room.avatar?.getThumbnail(client, width: 64, height: 64, method: ThumbnailMethod.crop); if (kIsWeb) { diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 0d208bbb..d63c6e8a 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -33,7 +33,6 @@ import '../pages/key_verification/key_verification_dialog.dart'; import '../utils/account_bundles.dart'; import '../utils/background_push.dart'; import '../utils/famedlysdk_store.dart'; -import '../utils/platform_infos.dart'; import 'local_notifications_extension.dart'; // import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -519,7 +518,7 @@ class MatrixState extends State with WidgetsBindingObserver { onLoginStateChanged.values.map((s) => s.cancel()); onOwnPresence.values.map((s) => s.cancel()); onNotification.values.map((s) => s.cancel()); - client.httpClient.close(); + onFocusSub?.cancel(); onBlurSub?.cancel(); _backgroundPush?.onLogin?.cancel(); diff --git a/pubspec.lock b/pubspec.lock index 53abc989..014b14ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -517,6 +517,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.3.0" + flutter_foreground_task: + dependency: "direct main" + description: + path: "." + ref: "td/lockScreen" + resolved-ref: "4ed1032175acf96d7b8b088dcd738f30c47f6096" + url: "git@github.com:Techno-Disaster/flutter_foreground_task.git" + source: git + version: "3.7.3" flutter_highlight: dependency: transitive description: @@ -717,10 +726,10 @@ packages: flutter_webrtc: dependency: "direct main" description: - name: flutter_webrtc - url: "https://pub.dartlang.org" - source: hosted - version: "0.8.7" + path: "/home/techno_disaster/Projects/Famedly/flutter-webrtc" + relative: false + source: path + version: "0.8.9" frontend_server_client: dependency: transitive description: @@ -938,8 +947,8 @@ packages: dependency: "direct main" description: path: "." - ref: null-safety - resolved-ref: a3d4020911860ff091d90638ab708604b71d2c5a + ref: "2906e65ffaa96afbe6c72e8477d4dfcdfd06c2c3" + resolved-ref: "2906e65ffaa96afbe6c72e8477d4dfcdfd06c2c3" url: "https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git" source: git version: "0.1.4" @@ -1016,10 +1025,10 @@ packages: matrix: dependency: "direct main" description: - name: matrix - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.12" + path: "/home/techno_disaster/Projects/Famedly/famedlysdk" + relative: false + source: path + version: "0.10.0" matrix_api_lite: dependency: transitive description: @@ -1464,7 +1473,7 @@ packages: name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "4.0.8" + version: "4.0.6" share_plus_linux: dependency: transitive description: @@ -1947,7 +1956,7 @@ packages: name: visibility_detector url: "https://pub.dartlang.org" source: hosted - version: "0.3.3" + version: "0.2.2" vm_service: dependency: transitive description: @@ -2026,12 +2035,12 @@ packages: source: hosted version: "1.0.0" webrtc_interface: - dependency: transitive + dependency: "direct overridden" description: - name: webrtc_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" + path: "/home/techno_disaster/Projects/Famedly/webrtc-interface" + relative: false + source: path + version: "1.0.5" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a8cc2d59..9c0a6fea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,7 +57,7 @@ dependencies: keyboard_shortcuts: ^0.1.4 localstorage: ^4.0.0+1 lottie: ^1.2.2 - matrix: ^0.9.12 + matrix: ^0.9.4 matrix_homeserver_recommendations: ^0.2.0 matrix_link_text: ^1.0.2 native_imaging: @@ -75,7 +75,7 @@ dependencies: salomon_bottom_bar: ^3.2.0 scroll_to_index: ^2.1.1 sentry: ^6.3.0 - share_plus: ^4.0.8 + share_plus: ^4.0.6 shared_preferences: ^2.0.13 slugify: ^2.0.0 snapping_sheet: ^3.1.0 @@ -88,6 +88,10 @@ dependencies: video_player: ^2.2.18 vrouter: ^1.2.0+21 wakelock: ^0.6.1+1 + flutter_foreground_task: + git: + url: git@github.com:Techno-Disaster/flutter_foreground_task.git + ref: td/lockScreen dev_dependencies: dart_code_metrics: ^4.10.1 @@ -146,7 +150,7 @@ dependency_overrides: keyboard_shortcuts: git: url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git - ref: null-safety + ref: 2906e65ffaa96afbe6c72e8477d4dfcdfd06c2c3 provider: 5.0.0 # For Flutter 3.0.0 compatibility # https://github.com/juliuscanute/qr_code_scanner/issues/532 @@ -160,3 +164,9 @@ dependency_overrides: git: url: https://github.com/TheOneWithTheBraid/snapping_sheet.git ref: listenable + matrix: + path: /home/techno_disaster/Projects/Famedly/famedlysdk + flutter_webrtc: + path: /home/techno_disaster/Projects/Famedly/flutter-webrtc + webrtc_interface: + path: /home/techno_disaster/Projects/Famedly/webrtc-interface \ No newline at end of file diff --git a/test/utils/test_client.dart b/test/utils/test_client.dart index ac9bacf4..7cbf2339 100644 --- a/test/utils/test_client.dart +++ b/test/utils/test_client.dart @@ -2,7 +2,7 @@ import 'package:matrix/encryption/utils/key_verification.dart'; import 'package:matrix/matrix.dart'; import 'package:matrix_api_lite/fake_matrix_api.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions.dart/flutter_hive_collections_database.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart'; Future prepareTestClient({ bool loggedIn = false, @@ -20,7 +20,7 @@ Future prepareTestClient({ importantStateEvents: { 'im.ponies.room_emotes', // we want emotes to work properly }, - databaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder, + databaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder, supportedLoginTypes: { AuthenticationTypes.password, AuthenticationTypes.sso