mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-11-04 06:17:26 +01:00 
			
		
		
		
	feat: group calls stash
This commit is contained in:
		
							parent
							
								
									a3d41da047
								
							
						
					
					
						commit
						77880666e7
					
				@ -23,9 +23,10 @@
 | 
			
		||||
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 | 
			
		||||
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
 | 
			
		||||
    <uses-permission android:name="android.permission.CALL_PHONE" />
 | 
			
		||||
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
 | 
			
		||||
 | 
			
		||||
    <uses-sdk
 | 
			
		||||
            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"/>
 | 
			
		||||
            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"/>
 | 
			
		||||
 | 
			
		||||
    <application
 | 
			
		||||
        android:label="FluffyChat"
 | 
			
		||||
@ -36,11 +37,13 @@
 | 
			
		||||
    >
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".MainActivity"
 | 
			
		||||
            android:launchMode="singleTask"
 | 
			
		||||
            android:launchMode="singleTop"
 | 
			
		||||
            android:theme="@style/LaunchTheme"
 | 
			
		||||
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
 | 
			
		||||
            android:hardwareAccelerated="true"
 | 
			
		||||
            android:windowSoftInputMode="adjustResize"
 | 
			
		||||
            android:showOnLockScreen="false"
 | 
			
		||||
            android:turnScreenOn="true"
 | 
			
		||||
            android:exported="true">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.MAIN"/>
 | 
			
		||||
@ -102,6 +105,8 @@
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </activity>
 | 
			
		||||
 | 
			
		||||
        <service android:name="com.pravera.flutter_foreground_task.service.ForegroundService" />
 | 
			
		||||
        
 | 
			
		||||
        <service android:name=".FcmPushService"
 | 
			
		||||
          android:exported="false">
 | 
			
		||||
          <intent-filter>
 | 
			
		||||
@ -129,4 +134,7 @@
 | 
			
		||||
            android:name="flutterEmbedding"
 | 
			
		||||
            android:value="2" />
 | 
			
		||||
    </application>
 | 
			
		||||
    <queries>
 | 
			
		||||
        <package android:name="chat.fluffy.fluffychat" />
 | 
			
		||||
    </queries>
 | 
			
		||||
</manifest>
 | 
			
		||||
 | 
			
		||||
@ -2,5 +2,4 @@
 | 
			
		||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <background android:drawable="@color/ic_launcher_background"/>
 | 
			
		||||
    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
 | 
			
		||||
    <monochrome android:drawable="@drawable/ic_launcher_foreground"/>
 | 
			
		||||
</adaptive-icon>
 | 
			
		||||
@ -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": {}
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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": {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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-----""";
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -121,12 +121,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
 | 
			
		||||
                  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);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
 | 
			
		||||
@ -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<Chat> {
 | 
			
		||||
  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<Chat> {
 | 
			
		||||
    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<Chat> {
 | 
			
		||||
      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<Chat> {
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    final callType = await showModalActionSheet<CallType>(
 | 
			
		||||
    final callType = await showModalActionSheet<VoipType>(
 | 
			
		||||
      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<Chat> {
 | 
			
		||||
            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,
 | 
			
		||||
 | 
			
		||||
@ -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']),
 | 
			
		||||
 | 
			
		||||
@ -293,14 +293,14 @@ class _ChatAccountPicker extends StatelessWidget {
 | 
			
		||||
    return Padding(
 | 
			
		||||
      padding: const EdgeInsets.all(8.0),
 | 
			
		||||
      child: FutureBuilder<Profile>(
 | 
			
		||||
        future: controller.sendingClient!.fetchOwnProfile(),
 | 
			
		||||
        future: controller.sendingClient!.ownProfile,
 | 
			
		||||
        builder: (context, snapshot) => PopupMenuButton<String>(
 | 
			
		||||
          onSelected: _popupMenuButtonSelected,
 | 
			
		||||
          itemBuilder: (BuildContext context) => clients
 | 
			
		||||
              .map((client) => PopupMenuItem<String>(
 | 
			
		||||
                    value: client!.userID,
 | 
			
		||||
                    child: FutureBuilder<Profile>(
 | 
			
		||||
                      future: client.fetchOwnProfile(),
 | 
			
		||||
                      future: client.ownProfile,
 | 
			
		||||
                      builder: (context, snapshot) => ListTile(
 | 
			
		||||
                        leading: Avatar(
 | 
			
		||||
                          mxContent: snapshot.data?.avatarUrl,
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
 | 
			
		||||
@ -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<User?>(
 | 
			
		||||
              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<User?>(
 | 
			
		||||
                        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,
 | 
			
		||||
 | 
			
		||||
@ -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<User?>(
 | 
			
		||||
                  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<String>(
 | 
			
		||||
                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<User?>(
 | 
			
		||||
            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<User?>(
 | 
			
		||||
            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),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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: <Widget>[
 | 
			
		||||
              FutureBuilder<User?>(
 | 
			
		||||
                  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,
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -39,24 +39,16 @@ class StateMessage extends StatelessWidget {
 | 
			
		||||
            child: Column(
 | 
			
		||||
              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
              children: [
 | 
			
		||||
                FutureBuilder<String>(
 | 
			
		||||
                    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),
 | 
			
		||||
 | 
			
		||||
@ -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<String>(
 | 
			
		||||
                          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(),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
 | 
			
		||||
@ -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<String>(
 | 
			
		||||
            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,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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<ChatList> with TickerProviderStateMixin {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
 | 
			
		||||
      FlutterLocalNotificationsPlugin();
 | 
			
		||||
  Future<void> 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<String>('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<ChatList> with TickerProviderStateMixin {
 | 
			
		||||
 | 
			
		||||
  void _hackyWebRTCFixForWeb() {
 | 
			
		||||
    Matrix.of(context).voipPlugin?.context = context;
 | 
			
		||||
    Matrix.of(context).voipPlugin?.start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void snapBackSpacesSheet() {
 | 
			
		||||
 | 
			
		||||
@ -262,47 +262,32 @@ class ChatListItem extends StatelessWidget {
 | 
			
		||||
                      ),
 | 
			
		||||
                      softWrap: false,
 | 
			
		||||
                    )
 | 
			
		||||
                  : FutureBuilder<String>(
 | 
			
		||||
                      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(
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,7 @@ class ClientChooserButton extends StatelessWidget {
 | 
			
		||||
              (client) => PopupMenuItem(
 | 
			
		||||
                value: client,
 | 
			
		||||
                child: FutureBuilder<Profile>(
 | 
			
		||||
                  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<Profile>(
 | 
			
		||||
        future: matrix.client.fetchOwnProfile(),
 | 
			
		||||
        future: matrix.client.ownProfile,
 | 
			
		||||
        builder: (context, snapshot) => Stack(
 | 
			
		||||
          alignment: Alignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
 | 
			
		||||
@ -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: <Widget>[
 | 
			
		||||
            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: <Widget>[
 | 
			
		||||
          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<Calling> {
 | 
			
		||||
  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<WrappedMediaStream> get screenSharingStreams =>
 | 
			
		||||
      (proxy?.screenSharingStreams ?? []);
 | 
			
		||||
 | 
			
		||||
  List<WrappedMediaStream> get userMediaStreams {
 | 
			
		||||
    if (isGroupCall) {
 | 
			
		||||
      return (proxy?.userMediaStreams ?? []);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
    final streams = <WrappedMediaStream>[
 | 
			
		||||
      ...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<WrappedMediaStream> 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<Calling> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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<Calling> {
 | 
			
		||||
  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<Calling> {
 | 
			
		||||
  */
 | 
			
		||||
 | 
			
		||||
  List<Widget> _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<Calling> {
 | 
			
		||||
      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
 | 
			
		||||
            ? <Widget>[hangupButton]
 | 
			
		||||
            : <Widget>[answerButton, hangupButton];
 | 
			
		||||
      case CallState.kConnected:
 | 
			
		||||
        return <Widget>[
 | 
			
		||||
          muteMicButton,
 | 
			
		||||
          //switchSpeakerButton,
 | 
			
		||||
          if (!voiceonly && !kIsWeb) switchCameraButton,
 | 
			
		||||
          if (!voiceonly) muteCameraButton,
 | 
			
		||||
          if (PlatformInfos.isMobile || PlatformInfos.isWeb)
 | 
			
		||||
            screenSharingButton,
 | 
			
		||||
          holdButton,
 | 
			
		||||
          hangupButton,
 | 
			
		||||
        ];
 | 
			
		||||
      case CallState.kEnded:
 | 
			
		||||
        return <Widget>[
 | 
			
		||||
          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 <Widget>[];
 | 
			
		||||
    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<Widget> _buildContent(Orientation orientation, bool isFloating) {
 | 
			
		||||
  List<Widget> _buildP2PView(Orientation orientation, bool isFloating) {
 | 
			
		||||
    final stackWidgets = <Widget>[];
 | 
			
		||||
 | 
			
		||||
    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<Calling> {
 | 
			
		||||
      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<Calling> {
 | 
			
		||||
 | 
			
		||||
    _resizeLocalVideo(orientation);
 | 
			
		||||
 | 
			
		||||
    if (call.getRemoteStreams.isEmpty) {
 | 
			
		||||
    if (userMediaStreams.isEmpty) {
 | 
			
		||||
      return stackWidgets;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final secondaryStreamViews = <Widget>[];
 | 
			
		||||
 | 
			
		||||
    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<Calling> {
 | 
			
		||||
    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<Calling> {
 | 
			
		||||
          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<Calling> {
 | 
			
		||||
                  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);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										166
									
								
								lib/pages/dialer/group_call_view.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								lib/pages/dialer/group_call_view.dart
									
									
									
									
									
										Normal file
									
								
							@ -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<GroupCallView> createState() => _GroupCallViewState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _GroupCallViewState extends State<GroupCallView> {
 | 
			
		||||
  WrappedMediaStream? get primaryStream => widget.call.primaryStream;
 | 
			
		||||
 | 
			
		||||
  List<WrappedMediaStream> get screenSharingStreams =>
 | 
			
		||||
      widget.call.screenSharingStreams;
 | 
			
		||||
  List<WrappedMediaStream> 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<WrappedMediaStream> userMediaStreams;
 | 
			
		||||
  const CallGrid(
 | 
			
		||||
      {Key? key,
 | 
			
		||||
      required this.call,
 | 
			
		||||
      required this.screenSharing,
 | 
			
		||||
      required this.userMediaStreams})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<CallGrid> createState() => _CallGridState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _CallGridState extends State<CallGrid> {
 | 
			
		||||
  @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,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -38,7 +38,7 @@ class InvitationSelectionController extends State<InvitationSelection> {
 | 
			
		||||
    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(
 | 
			
		||||
 | 
			
		||||
@ -111,7 +111,7 @@ class _KeyVerificationPageState extends State<KeyVerificationDialog> {
 | 
			
		||||
    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!;
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -370,7 +370,7 @@ class StoryPageController extends State<StoryPage> {
 | 
			
		||||
      .client
 | 
			
		||||
      .getRoomById(roomId)
 | 
			
		||||
      ?.getState(EventTypes.RoomCreate)
 | 
			
		||||
      ?.senderFromMemoryOrFallback
 | 
			
		||||
      ?.sender
 | 
			
		||||
      .avatarUrl;
 | 
			
		||||
 | 
			
		||||
  String get title =>
 | 
			
		||||
@ -378,7 +378,7 @@ class StoryPageController extends State<StoryPage> {
 | 
			
		||||
          .client
 | 
			
		||||
          .getRoomById(roomId)
 | 
			
		||||
          ?.getState(EventTypes.RoomCreate)
 | 
			
		||||
          ?.senderFromMemoryOrFallback
 | 
			
		||||
          ?.sender
 | 
			
		||||
          .calcDisplayname() ??
 | 
			
		||||
      'Story not found';
 | 
			
		||||
 | 
			
		||||
@ -485,8 +485,7 @@ class StoryPageController extends State<StoryPage> {
 | 
			
		||||
      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!]);
 | 
			
		||||
 | 
			
		||||
@ -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<void> 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<void> _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);
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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: <String>{
 | 
			
		||||
        // 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: <String>{
 | 
			
		||||
          // 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,
 | 
			
		||||
      );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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));
 | 
			
		||||
}
 | 
			
		||||
@ -9,11 +9,7 @@ import 'package:fluffychat/utils/platform_infos.dart';
 | 
			
		||||
abstract class FluffyShare {
 | 
			
		||||
  static Future<void> 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),
 | 
			
		||||
 | 
			
		||||
@ -12,8 +12,7 @@ extension ClientStoriesExtension on Client {
 | 
			
		||||
 | 
			
		||||
  List<User> get contacts => rooms
 | 
			
		||||
      .where((room) => room.isDirectChat)
 | 
			
		||||
      .map((room) =>
 | 
			
		||||
          room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!))
 | 
			
		||||
      .map((room) => room.getUserByMXIDSync(room.directChatMatrixID!))
 | 
			
		||||
      .toList();
 | 
			
		||||
 | 
			
		||||
  List<Room> get storiesRooms => rooms
 | 
			
		||||
 | 
			
		||||
@ -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<FluffyBoxDatabase> 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),
 | 
			
		||||
 | 
			
		||||
@ -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<FlutterHiveCollectionsDatabase> databaseBuilder(
 | 
			
		||||
  static Future<FamedlySdkHiveDatabase> 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<String> _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<String> _getFileStoreDirectory() async {
 | 
			
		||||
    try {
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<void> pushHelper(
 | 
			
		||||
@ -32,7 +35,6 @@ Future<void> 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<void> 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<void> 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<void> 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(<int>[insistentFlag]) : null,
 | 
			
		||||
    groupKey: event.roomId,
 | 
			
		||||
  );
 | 
			
		||||
  const iOSPlatformChannelSpecifics = IOSNotificationDetails();
 | 
			
		||||
  final platformChannelSpecifics = NotificationDetails(
 | 
			
		||||
@ -119,8 +128,25 @@ Future<void> 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!');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										191
									
								
								lib/utils/voip/call_session_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								lib/utils/voip/call_session_state.dart
									
									
									
									
									
										Normal file
									
								
							@ -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<WrappedMediaStream> get screenSharingStreams {
 | 
			
		||||
    final streams = <WrappedMediaStream>[];
 | 
			
		||||
    if (connected) {
 | 
			
		||||
      if (call.remoteScreenSharingStream != null) {
 | 
			
		||||
        streams.add(call.remoteScreenSharingStream!);
 | 
			
		||||
      }
 | 
			
		||||
      if (call.localScreenSharingStream != null) {
 | 
			
		||||
        streams.add(call.localScreenSharingStream!);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return streams;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<WrappedMediaStream> get userMediaStreams {
 | 
			
		||||
    final streams = <WrappedMediaStream>[];
 | 
			
		||||
    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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								lib/utils/voip/call_state_proxy.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								lib/utils/voip/call_state_proxy.dart
									
									
									
									
									
										Normal file
									
								
							@ -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<WrappedMediaStream> get screenSharingStreams;
 | 
			
		||||
 | 
			
		||||
  List<WrappedMediaStream> get userMediaStreams;
 | 
			
		||||
 | 
			
		||||
  void onStateChanged(Function() callback);
 | 
			
		||||
}
 | 
			
		||||
@ -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<String, CallKeeper> calls = <String, CallKeeper>{};
 | 
			
		||||
 | 
			
		||||
  String newUUID() => const Uuid().v4();
 | 
			
		||||
 | 
			
		||||
  String get appName => 'Famedly';
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> get alertOptions => <String, dynamic>{
 | 
			
		||||
        '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,
 | 
			
		||||
          <String, dynamic>{
 | 
			
		||||
            'ios': <String, dynamic>{
 | 
			
		||||
              '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<void> 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<void> hangup(String callUUID) async {
 | 
			
		||||
    await _callKeep.endCall(callUUID);
 | 
			
		||||
    removeCall(callUUID);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> reject(String callUUID) async {
 | 
			
		||||
    await _callKeep.rejectCall(callUUID);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> answer(String callUUID) async {
 | 
			
		||||
    final keeper = calls[callUUID];
 | 
			
		||||
    if (!keeper!.connected) {
 | 
			
		||||
      await _callKeep.answerIncomingCall(callUUID);
 | 
			
		||||
      keeper.connected = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setOnHold(String callUUID, bool held) async {
 | 
			
		||||
    await _callKeep.setOnHold(callUUID, held);
 | 
			
		||||
    setCallHeld(callUUID, held);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setMutedCall(String callUUID, bool muted) async {
 | 
			
		||||
    await _callKeep.setMutedCall(callUUID, muted);
 | 
			
		||||
    setCallMuted(callUUID, muted);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> 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<CallKeeper> 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<void> checkoutPhoneAccountSetting(BuildContext context) async {
 | 
			
		||||
    await _callKeep.setup(context, <String, dynamic>{
 | 
			
		||||
      'ios': <String, dynamic>{
 | 
			
		||||
        'appName': appName,
 | 
			
		||||
      },
 | 
			
		||||
      'android': alertOptions,
 | 
			
		||||
    });
 | 
			
		||||
    final hasPhoneAccount = await _callKeep.hasPhoneAccount();
 | 
			
		||||
    if (!hasPhoneAccount) {
 | 
			
		||||
      await _callKeep.hasDefaultPhoneAccount(context, alertOptions);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// CallActions.
 | 
			
		||||
  Future<void> 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<void> endCall(CallKeepPerformEndCallAction event) async {
 | 
			
		||||
    final keeper = calls[event.callUUID];
 | 
			
		||||
    keeper?.call?.hangup();
 | 
			
		||||
    removeCall(event.callUUID!);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> didPerformDTMFAction(CallKeepDidPerformDTMFAction event) async {
 | 
			
		||||
    final keeper = calls[event.callUUID]!;
 | 
			
		||||
    keeper.call?.sendDTMF(event.digits!);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> 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<void> 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<void> 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!);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										123
									
								
								lib/utils/voip/group_call_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								lib/utils/voip/group_call_state.dart
									
									
									
									
									
										Normal file
									
								
							@ -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<WrappedMediaStream> get screenSharingStreams =>
 | 
			
		||||
      groupCall.screenshareStreams;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<WrappedMediaStream> 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;
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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<Matrix> 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();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										43
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								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:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								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
 | 
			
		||||
@ -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<Client> prepareTestClient({
 | 
			
		||||
  bool loggedIn = false,
 | 
			
		||||
@ -20,7 +20,7 @@ Future<Client> prepareTestClient({
 | 
			
		||||
    importantStateEvents: <String>{
 | 
			
		||||
      'im.ponies.room_emotes', // we want emotes to work properly
 | 
			
		||||
    },
 | 
			
		||||
    databaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder,
 | 
			
		||||
    databaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder,
 | 
			
		||||
    supportedLoginTypes: {
 | 
			
		||||
      AuthenticationTypes.password,
 | 
			
		||||
      AuthenticationTypes.sso
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user