feat: group calls stash

This commit is contained in:
Jayesh Nirve 2022-06-28 16:20:06 +05:30
parent a3d41da047
commit 77880666e7
No known key found for this signature in database
GPG Key ID: F6D9E9BF14C7D103
48 changed files with 1270 additions and 1168 deletions

View File

@ -23,9 +23,10 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-sdk <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 <application
android:label="FluffyChat" android:label="FluffyChat"
@ -36,11 +37,13 @@
> >
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTask" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:showOnLockScreen="false"
android:turnScreenOn="true"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
@ -102,6 +105,8 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name="com.pravera.flutter_foreground_task.service.ForegroundService" />
<service android:name=".FcmPushService" <service android:name=".FcmPushService"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
@ -129,4 +134,7 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<queries>
<package android:name="chat.fluffy.fluffychat" />
</queries>
</manifest> </manifest>

View File

@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View File

@ -185,7 +185,7 @@
"username": {} "username": {}
} }
}, },
"changedTheChatDescriptionTo": "{username} ha canviat la descripció del xat a: '{description}'", "changedTheChatDescriptionTo": "{username} ha canviat la descripció del xat a: «{description}»",
"@changedTheChatDescriptionTo": { "@changedTheChatDescriptionTo": {
"type": "text", "type": "text",
"placeholders": { "placeholders": {
@ -193,7 +193,7 @@
"description": {} "description": {}
} }
}, },
"changedTheChatNameTo": "{username} ha canviat el nom del xat a: '{chatname}'", "changedTheChatNameTo": "{username} ha canviat el nom del xat a: «{chatname}»",
"@changedTheChatNameTo": { "@changedTheChatNameTo": {
"type": "text", "type": "text",
"placeholders": { "placeholders": {
@ -404,7 +404,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"containsDisplayName": "Conté l'àlies", "containsDisplayName": "Conté el nom visible",
"@containsDisplayName": { "@containsDisplayName": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -441,7 +441,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"couldNotSetDisplayname": "No sha pogut definir l'àlies", "couldNotSetDisplayname": "No sha pogut definir el nom visible",
"@couldNotSetDisplayname": { "@couldNotSetDisplayname": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -503,7 +503,7 @@
"timeOfDay": {} "timeOfDay": {}
} }
}, },
"dateWithoutYear": "{day}-{month}", "dateWithoutYear": "{day}/{month}",
"@dateWithoutYear": { "@dateWithoutYear": {
"type": "text", "type": "text",
"placeholders": { "placeholders": {
@ -511,7 +511,7 @@
"day": {} "day": {}
} }
}, },
"dateWithYear": "{day}-{month}-{year}", "dateWithYear": "{day}/{month}/{year}",
"@dateWithYear": { "@dateWithYear": {
"type": "text", "type": "text",
"placeholders": { "placeholders": {
@ -570,7 +570,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"displaynameHasBeenChanged": "Ha canviat l'àlies", "displaynameHasBeenChanged": "Sha canviat el nom visible",
"@displaynameHasBeenChanged": { "@displaynameHasBeenChanged": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -590,7 +590,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"editDisplayname": "Edita l'àlies", "editDisplayname": "Edita el nom visible",
"@editDisplayname": { "@editDisplayname": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -1725,7 +1725,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"unknownEvent": "Esdeveniment desconegut '{type}'", "unknownEvent": "Lesdeveniment «{type}» és desconegut",
"@unknownEvent": { "@unknownEvent": {
"type": "text", "type": "text",
"placeholders": { "placeholders": {
@ -1829,7 +1829,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"verifySuccess": "T'has verificat correctament!", "verifySuccess": "Us heu verificat correctament.",
"@verifySuccess": { "@verifySuccess": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -1889,7 +1889,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"warningEncryptionInBeta": "El xifrat d'extrem a extrem es troba actualment en proves (Beta. Utilitza'l sota la teva responsabilitat!", "warningEncryptionInBeta": "El xifratge dextrem a extrem es troba actualment en proves. Utilitzeu-lo sota la vostra responsabilitat.",
"@warningEncryptionInBeta": { "@warningEncryptionInBeta": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -2417,7 +2417,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"changedTheDisplaynameTo": "{username} ha canviat el seu àlies a: '{displayname}'", "changedTheDisplaynameTo": "{username} ha canviat el propi nom visible a: «{displayname}»",
"@changedTheDisplaynameTo": { "@changedTheDisplaynameTo": {
"type": "text", "type": "text",
"placeholders": { "placeholders": {
@ -2492,38 +2492,5 @@
"@bubbleSize": { "@bubbleSize": {
"type": "text", "type": "text",
"placeholders": {} "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": {}
} }
} }

View File

@ -2858,7 +2858,5 @@
} }
}, },
"showSpaces": "Mostrar lista de espazos", "showSpaces": "Mostrar lista de espazos",
"@showSpaces": {}, "@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": {}
} }

View File

@ -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-----""";

View File

@ -22,7 +22,6 @@ import 'config/themes.dart';
import 'utils/background_push.dart'; import 'utils/background_push.dart';
import 'utils/custom_scroll_behaviour.dart'; import 'utils/custom_scroll_behaviour.dart';
import 'utils/localized_exception_extension.dart'; import 'utils/localized_exception_extension.dart';
import 'utils/platform_infos.dart';
import 'widgets/lock_screen.dart'; import 'widgets/lock_screen.dart';
import 'widgets/matrix.dart'; import 'widgets/matrix.dart';

View File

@ -121,12 +121,7 @@ class _BootstrapDialogState extends State<BootstrapDialog> {
icon: const Icon(Icons.save_alt_outlined), icon: const Icon(Icons.save_alt_outlined),
label: Text(L10n.of(context)!.saveTheSecurityKeyNow), label: Text(L10n.of(context)!.saveTheSecurityKeyNow),
onPressed: () { onPressed: () {
final box = context.findRenderObject() as RenderBox; Share.share(key!);
Share.share(
key!,
sharePositionOrigin:
box.localToGlobal(Offset.zero) & box.size,
);
setState(() => _recoveryKeyCopied = true); setState(() => _recoveryKeyCopied = true);
}, },
), ),

View File

@ -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/ios_badge_client_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/voip/callkeep_manager.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/account_bundles.dart'; import '../../utils/account_bundles.dart';
import '../../utils/localized_exception_extension.dart'; import '../../utils/localized_exception_extension.dart';
@ -35,6 +34,8 @@ import 'send_file_dialog.dart';
import 'send_location_dialog.dart'; import 'send_location_dialog.dart';
import 'sticker_picker_dialog.dart'; import 'sticker_picker_dialog.dart';
enum VoipType { kVoice, kVideo, kGroup }
class Chat extends StatefulWidget { class Chat extends StatefulWidget {
final Widget? sideView; final Widget? sideView;
@ -168,14 +169,6 @@ class ChatController extends State<Chat> {
void initState() { void initState() {
scrollController.addListener(_updateScrollController); scrollController.addListener(_updateScrollController);
inputFocus.addListener(_inputFocusListener); inputFocus.addListener(_inputFocusListener);
final voipPlugin = Matrix.of(context).voipPlugin;
if (voipPlugin != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
CallKeepManager().setVoipPlugin(voipPlugin);
CallKeepManager().initialize().catchError((_) => true);
});
}
super.initState(); super.initState();
} }
@ -461,11 +454,11 @@ class ChatController extends State<Chat> {
if (selectedEvents.length == 1) { if (selectedEvents.length == 1) {
return selectedEvents.first return selectedEvents.first
.getDisplayEvent(timeline!) .getDisplayEvent(timeline!)
.calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!)); .getLocalizedBody(MatrixLocals(L10n.of(context)!));
} }
for (final event in selectedEvents) { for (final event in selectedEvents) {
if (copyString.isNotEmpty) copyString += '\n\n'; if (copyString.isNotEmpty) copyString += '\n\n';
copyString += event.getDisplayEvent(timeline!).calcLocalizedBodyFallback( copyString += event.getDisplayEvent(timeline!).getLocalizedBody(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true); withSenderNamePrefix: true);
} }
@ -773,7 +766,7 @@ class ChatController extends State<Chat> {
editEvent = selectedEvents.first; editEvent = selectedEvents.first;
inputText = sendController.text = editEvent! inputText = sendController.text = editEvent!
.getDisplayEvent(timeline!) .getDisplayEvent(timeline!)
.calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!), .getLocalizedBody(MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false, hideReply: true); withSenderNamePrefix: false, hideReply: true);
selectedEvents.clear(); selectedEvents.clear();
}); });
@ -961,22 +954,30 @@ class ChatController extends State<Chat> {
} }
}); });
} }
final callType = await showModalActionSheet<CallType>( final callType = await showModalActionSheet<VoipType>(
context: context, context: context,
title: L10n.of(context)!.warning, title: L10n.of(context)!.warning,
message: L10n.of(context)!.videoCallsBetaWarning, message: L10n.of(context)!.videoCallsBetaWarning,
cancelLabel: L10n.of(context)!.cancel, cancelLabel: L10n.of(context)!.cancel,
actions: [ actions: [
SheetAction( if (room!.isDirectChat)
label: L10n.of(context)!.voiceCall, SheetAction(
icon: Icons.phone_outlined, label: L10n.of(context)!.voiceCall,
key: CallType.kVoice, icon: Icons.phone_outlined,
), key: VoipType.kVoice,
SheetAction( ),
label: L10n.of(context)!.videoCall, if (room!.isDirectChat)
icon: Icons.video_call_outlined, SheetAction(
key: CallType.kVideo, 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; if (callType == null) return;
@ -987,11 +988,22 @@ class ChatController extends State<Chat> {
Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials()); Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials());
if (success.result != null) { if (success.result != null) {
final voipPlugin = Matrix.of(context).voipPlugin; final voipPlugin = Matrix.of(context).voipPlugin;
await voipPlugin!.voip.inviteToCall(room!.id, callType).catchError((e) { if ({VoipType.kVideo, VoipType.kVoice}.contains(callType)) {
ScaffoldMessenger.of(context).showSnackBar( await voipPlugin!.voip
SnackBar(content: Text((e as Object).toLocalizedString(context))), .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 { } else {
await showOkAlertDialog( await showOkAlertDialog(
context: context, context: context,

View File

@ -29,11 +29,10 @@ class ChatAppBarTitle extends StatelessWidget {
? () => showModalBottomSheet( ? () => showModalBottomSheet(
context: context, context: context,
builder: (c) => UserBottomSheet( builder: (c) => UserBottomSheet(
user: room user: room.getUserByMXIDSync(directChatMatrixID),
.unsafeGetUserFromMemoryOrFallback(directChatMatrixID),
outerContext: context, outerContext: context,
onMention: () => controller.sendController.text += onMention: () => controller.sendController.text +=
'${room.unsafeGetUserFromMemoryOrFallback(directChatMatrixID).mention} ', '${room.getUserByMXIDSync(directChatMatrixID).mention} ',
), ),
) )
: () => VRouter.of(context).toSegments(['rooms', room.id, 'details']), : () => VRouter.of(context).toSegments(['rooms', room.id, 'details']),

View File

@ -293,14 +293,14 @@ class _ChatAccountPicker extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: FutureBuilder<Profile>( child: FutureBuilder<Profile>(
future: controller.sendingClient!.fetchOwnProfile(), future: controller.sendingClient!.ownProfile,
builder: (context, snapshot) => PopupMenuButton<String>( builder: (context, snapshot) => PopupMenuButton<String>(
onSelected: _popupMenuButtonSelected, onSelected: _popupMenuButtonSelected,
itemBuilder: (BuildContext context) => clients itemBuilder: (BuildContext context) => clients
.map((client) => PopupMenuItem<String>( .map((client) => PopupMenuItem<String>(
value: client!.userID, value: client!.userID,
child: FutureBuilder<Profile>( child: FutureBuilder<Profile>(
future: client.fetchOwnProfile(), future: client.ownProfile,
builder: (context, snapshot) => ListTile( builder: (context, snapshot) => ListTile(
leading: Avatar( leading: Avatar(
mxContent: snapshot.data?.avatarUrl, mxContent: snapshot.data?.avatarUrl,

View File

@ -113,8 +113,7 @@ class ChatView extends StatelessWidget {
]; ];
} else { } else {
return [ return [
if (Matrix.of(context).voipPlugin != null && if (Matrix.of(context).voipPlugin != null)
controller.room!.isDirectChat)
IconButton( IconButton(
onPressed: controller.onPhoneButtonTap, onPressed: controller.onPhoneButtonTap,
icon: const Icon(Icons.call_outlined), icon: const Icon(Icons.call_outlined),
@ -350,12 +349,12 @@ class ChatView extends StatelessWidget {
builder: (c) => builder: (c) =>
UserBottomSheet( UserBottomSheet(
user: event user: event
.senderFromMemoryOrFallback, .sender,
outerContext: outerContext:
context, context,
onMention: () => controller onMention: () => controller
.sendController .sendController
.text += '${event.senderFromMemoryOrFallback.mention} ', .text += '${event.sender.mention} ',
), ),
), ),
unfold: controller unfold: controller

View File

@ -48,12 +48,12 @@ class EventInfoDialog extends StatelessWidget {
children: [ children: [
ListTile( ListTile(
leading: Avatar( leading: Avatar(
mxContent: event.senderFromMemoryOrFallback.avatarUrl, mxContent: event.sender.avatarUrl,
name: event.senderFromMemoryOrFallback.calcDisplayname(), name: event.sender.calcDisplayname(),
), ),
title: Text(L10n.of(context)!.sender), title: Text(L10n.of(context)!.sender),
subtitle: Text( subtitle:
'${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]'), Text('${event.sender.calcDisplayname()} [${event.senderId}]'),
), ),
ListTile( ListTile(
title: Text(L10n.of(context)!.time), title: Text(L10n.of(context)!.time),

View File

@ -75,7 +75,7 @@ class Message extends StatelessWidget {
EventTypes.Sticker, EventTypes.Sticker,
EventTypes.Encrypted, EventTypes.Encrypted,
].contains(nextEvent!.type) ].contains(nextEvent!.type)
? nextEvent!.senderId == event.senderId && !displayTime ? nextEvent!.sender.id == event.sender.id && !displayTime
: false; : false;
final textColor = ownMessage final textColor = ownMessage
? Theme.of(context).colorScheme.onPrimary ? Theme.of(context).colorScheme.onPrimary
@ -125,16 +125,11 @@ class Message extends StatelessWidget {
), ),
), ),
)) ))
: FutureBuilder<User?>( : Avatar(
future: event.fetchSenderUser(), mxContent: event.sender.avatarUrl,
builder: (context, snapshot) { name: event.sender.calcDisplayname(),
final user = snapshot.data ?? event.senderFromMemoryOrFallback; onTap: () => onAvatarTab!(event),
return Avatar( ),
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
onTap: () => onAvatarTab!(event),
);
}),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -145,22 +140,14 @@ class Message extends StatelessWidget {
padding: const EdgeInsets.only(left: 8.0, bottom: 4), padding: const EdgeInsets.only(left: 8.0, bottom: 4),
child: ownMessage || event.room.isDirectChat child: ownMessage || event.room.isDirectChat
? const SizedBox(height: 12) ? const SizedBox(height: 12)
: FutureBuilder<User?>( : Text(
future: event.fetchSenderUser(), event.sender.calcDisplayname(),
builder: (context, snapshot) { style: TextStyle(
final displayname = fontSize: 12,
snapshot.data?.calcDisplayname() ?? fontWeight: FontWeight.bold,
event.senderFromMemoryOrFallback color: event.sender.calcDisplayname().color,
.calcDisplayname(); ),
return Text( ),
displayname,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: displayname.color,
),
);
}),
), ),
Container( Container(
alignment: alignment, alignment: alignment,

View File

@ -34,7 +34,7 @@ class MessageContent extends StatelessWidget {
content: Text( content: Text(
event.type == EventTypes.Encrypted event.type == EventTypes.Encrypted
? L10n.of(context)!.needPantalaimonWarning ? L10n.of(context)!.needPantalaimonWarning
: event.calcLocalizedBodyFallback( : event.getLocalizedBody(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
), ),
))); )));
@ -172,73 +172,48 @@ class MessageContent extends StatelessWidget {
textmessage: textmessage:
default: default:
if (event.redacted) { if (event.redacted) {
return FutureBuilder<User?>( return _ButtonContent(
future: event.fetchSenderUser(), label: L10n.of(context)!
builder: (context, snapshot) { .redactedAnEvent(event.sender.calcDisplayname()),
return _ButtonContent( icon: const Icon(Icons.delete_outlined),
label: L10n.of(context)!.redactedAnEvent(snapshot.data textColor: buttonTextColor,
?.calcDisplayname() ?? onPressed: () => onInfoTab!(event),
event.senderFromMemoryOrFallback.calcDisplayname()), );
icon: const Icon(Icons.delete_outlined),
textColor: buttonTextColor,
onPressed: () => onInfoTab!(event),
);
});
} }
final bigEmotes = event.onlyEmotes && final bigEmotes = event.onlyEmotes &&
event.numberEmotes > 0 && event.numberEmotes > 0 &&
event.numberEmotes <= 10; event.numberEmotes <= 10;
return FutureBuilder<String>( return LinkText(
future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!), text: event.getLocalizedBody(MatrixLocals(L10n.of(context)!),
hideReply: true), hideReply: true),
builder: (context, snapshot) { textStyle: TextStyle(
return LinkText( color: textColor,
text: snapshot.data ?? fontSize: bigEmotes ? fontSize * 3 : fontSize,
event.calcLocalizedBodyFallback( decoration: event.redacted ? TextDecoration.lineThrough : null,
MatrixLocals(L10n.of(context)!), ),
hideReply: true), linkStyle: TextStyle(
textStyle: TextStyle( color: textColor.withAlpha(150),
color: textColor, fontSize: bigEmotes ? fontSize * 3 : fontSize,
fontSize: bigEmotes ? fontSize * 3 : fontSize, decoration: TextDecoration.underline,
decoration: ),
event.redacted ? TextDecoration.lineThrough : null, onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
), );
linkStyle: TextStyle(
color: textColor.withAlpha(150),
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration: TextDecoration.underline,
),
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
);
});
} }
case EventTypes.CallInvite: case EventTypes.CallInvite:
return FutureBuilder<User?>( return _ButtonContent(
future: event.fetchSenderUser(), label: L10n.of(context)!.startedACall(event.sender.calcDisplayname()),
builder: (context, snapshot) { icon: const Icon(Icons.phone_outlined),
return _ButtonContent( textColor: buttonTextColor,
label: L10n.of(context)!.startedACall( onPressed: () => onInfoTab!(event),
snapshot.data?.calcDisplayname() ?? );
event.senderFromMemoryOrFallback.calcDisplayname()),
icon: const Icon(Icons.phone_outlined),
textColor: buttonTextColor,
onPressed: () => onInfoTab!(event),
);
});
default: default:
return FutureBuilder<User?>( return _ButtonContent(
future: event.fetchSenderUser(), label: L10n.of(context)!
builder: (context, snapshot) { .userSentUnknownEvent(event.sender.calcDisplayname(), event.type),
return _ButtonContent( icon: const Icon(Icons.info_outlined),
label: L10n.of(context)!.userSentUnknownEvent( textColor: buttonTextColor,
snapshot.data?.calcDisplayname() ?? onPressed: () => onInfoTab!(event),
event.senderFromMemoryOrFallback.calcDisplayname(), );
event.type),
icon: const Icon(Icons.info_outlined),
textColor: buttonTextColor,
onPressed: () => onInfoTab!(event),
);
});
} }
} }
} }

View File

@ -39,7 +39,7 @@ class MessageReactions extends StatelessWidget {
); );
} }
reactionMap[key]!.count++; reactionMap[key]!.count++;
reactionMap[key]!.reactors!.add(e.senderFromMemoryOrFallback); reactionMap[key]!.reactors!.add(e.sender);
reactionMap[key]!.reacted |= e.senderId == e.room.client.userID; reactionMap[key]!.reacted |= e.senderId == e.room.client.userID;
} }
} }

View File

@ -52,7 +52,7 @@ class ReplyContent extends StatelessWidget {
); );
} else { } else {
replyBody = Text( replyBody = Text(
displayEvent.calcLocalizedBodyFallback( displayEvent.getLocalizedBody(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false, withSenderNamePrefix: false,
hideReply: true, hideReply: true,
@ -83,25 +83,18 @@ class ReplyContent extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
FutureBuilder<User?>( Text(
future: displayEvent.fetchSenderUser(), displayEvent.sender.calcDisplayname() + ':',
builder: (context, snapshot) { maxLines: 1,
return Text( overflow: TextOverflow.ellipsis,
(snapshot.data?.calcDisplayname() ?? style: TextStyle(
displayEvent.senderFromMemoryOrFallback fontWeight: FontWeight.bold,
.calcDisplayname()) + color: ownMessage
':', ? Theme.of(context).colorScheme.onPrimary
maxLines: 1, : Theme.of(context).colorScheme.onBackground,
overflow: TextOverflow.ellipsis, fontSize: fontSize,
style: TextStyle( ),
fontWeight: FontWeight.bold, ),
color: ownMessage
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onBackground,
fontSize: fontSize,
),
);
}),
replyBody, replyBody,
], ],
), ),

View File

@ -39,24 +39,16 @@ class StateMessage extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
FutureBuilder<String>( Text(
future: event event.getLocalizedBody(MatrixLocals(L10n.of(context)!)),
.calcLocalizedBody(MatrixLocals(L10n.of(context)!)), textAlign: TextAlign.center,
builder: (context, snapshot) { style: TextStyle(
return Text( fontSize: 14 * AppConfig.fontSizeFactor,
snapshot.data ?? color: Theme.of(context).textTheme.bodyText2!.color,
event.calcLocalizedBodyFallback( decoration:
MatrixLocals(L10n.of(context)!)), event.redacted ? TextDecoration.lineThrough : null,
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) if (counter != 0)
Text( Text(
L10n.of(context)!.moreEvents(counter), L10n.of(context)!.moreEvents(counter),

View File

@ -26,7 +26,7 @@ class PinnedEvents extends StatelessWidget {
actions: events actions: events
.map((event) => SheetAction( .map((event) => SheetAction(
key: event?.eventId ?? '', key: event?.eventId ?? '',
label: event?.calcLocalizedBodyFallback( label: event?.getLocalizedBody(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true, withSenderNamePrefix: true,
hideReply: true, hideReply: true,
@ -90,41 +90,32 @@ class PinnedEvents extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: FutureBuilder<String>( child: LinkText(
future: event.calcLocalizedBody( text: event.getLocalizedBody(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true, withSenderNamePrefix: true,
hideReply: true, hideReply: true,
), ),
builder: (context, snapshot) { maxLines: 2,
return LinkText( textStyle: TextStyle(
text: snapshot.data ?? overflow: TextOverflow.ellipsis,
event.calcLocalizedBodyFallback( fontSize: fontSize,
MatrixLocals(L10n.of(context)!), decoration: event.redacted
withSenderNamePrefix: true, ? TextDecoration.lineThrough
hideReply: true, : null,
), ),
maxLines: 2, linkStyle: TextStyle(
textStyle: TextStyle( color: Theme.of(context)
overflow: TextOverflow.ellipsis, .textTheme
fontSize: fontSize, .bodyText1
decoration: event.redacted ?.color
? TextDecoration.lineThrough ?.withAlpha(150),
: null, fontSize: fontSize,
), decoration: TextDecoration.underline,
linkStyle: TextStyle( ),
color: Theme.of(context) onLinkTap: (url) =>
.textTheme UrlLauncher(context, url).launchUrl(),
.bodyText1 ),
?.color
?.withAlpha(150),
fontSize: fontSize,
decoration: TextDecoration.underline,
),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
);
}),
), ),
), ),
], ],

View File

@ -50,7 +50,6 @@ class _EditContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final event = this.event;
if (event == null) { if (event == null) {
return Container(); return Container();
} }
@ -61,27 +60,19 @@ class _EditContent extends StatelessWidget {
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
), ),
Container(width: 15.0), Container(width: 15.0),
FutureBuilder<String>( Text(
future: event.calcLocalizedBody( event?.getLocalizedBody(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false, withSenderNamePrefix: false,
hideReply: true, hideReply: true,
), ) ??
builder: (context, snapshot) { '',
return Text( overflow: TextOverflow.ellipsis,
snapshot.data ?? maxLines: 1,
event.calcLocalizedBodyFallback( style: TextStyle(
MatrixLocals(L10n.of(context)!), color: Theme.of(context).textTheme.bodyText2!.color,
withSenderNamePrefix: false, ),
hideReply: true, ),
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: Theme.of(context).textTheme.bodyText2!.color,
),
);
}),
], ],
); );
} }

View File

@ -94,18 +94,15 @@ class ChatEncryptionSettingsView extends StatelessWidget {
child: ListTile( child: ListTile(
leading: Avatar( leading: Avatar(
mxContent: room mxContent: room
.unsafeGetUserFromMemoryOrFallback( .getUserByMXIDSync(deviceKeys[i].userId)
deviceKeys[i].userId)
.avatarUrl, .avatarUrl,
name: room name: room
.unsafeGetUserFromMemoryOrFallback( .getUserByMXIDSync(deviceKeys[i].userId)
deviceKeys[i].userId)
.calcDisplayname(), .calcDisplayname(),
), ),
title: Text( title: Text(
room room
.unsafeGetUserFromMemoryOrFallback( .getUserByMXIDSync(deviceKeys[i].userId)
deviceKeys[i].userId)
.calcDisplayname(), .calcDisplayname(),
), ),
subtitle: Text( subtitle: Text(

View File

@ -1,10 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.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:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.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_bottom_bar.dart';
import 'package:fluffychat/pages/chat_list/spaces_entry.dart'; import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
import 'package:fluffychat/utils/fluffy_share.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 'package:fluffychat/utils/platform_infos.dart';
import '../../../utils/account_bundles.dart'; import '../../../utils/account_bundles.dart';
import '../../main.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 @override
void initState() { void initState() {
_initReceiveSharingIntent(); _initReceiveSharingIntent();
scrollController.addListener(_onScroll); scrollController.addListener(_onScroll);
_waitForFirstSync(); _waitForFirstSync();
_hackyWebRTCFixForWeb(); _hackyWebRTCFixForWeb();
doStuffIfOpenedFromNotification();
super.initState(); super.initState();
} }
@ -591,6 +635,7 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
void _hackyWebRTCFixForWeb() { void _hackyWebRTCFixForWeb() {
Matrix.of(context).voipPlugin?.context = context; Matrix.of(context).voipPlugin?.context = context;
Matrix.of(context).voipPlugin?.start();
} }
void snapBackSpacesSheet() { void snapBackSpacesSheet() {

View File

@ -262,47 +262,32 @@ class ChatListItem extends StatelessWidget {
), ),
softWrap: false, softWrap: false,
) )
: FutureBuilder<String>( : Text(
future: room.lastEvent?.calcLocalizedBody( room.membership == Membership.invite
MatrixLocals(L10n.of(context)!), ? L10n.of(context)!.youAreInvitedToThisChat
hideReply: true, : room.lastEvent?.getLocalizedBody(
hideEdit: true, MatrixLocals(L10n.of(context)!),
plaintextBody: true, hideReply: true,
removeMarkdown: true, hideEdit: true,
withSenderNamePrefix: !room.isDirectChat || plaintextBody: true,
room.directChatMatrixID != removeMarkdown: true,
room.lastEvent?.senderId, withSenderNamePrefix: !room.isDirectChat ||
) ?? room.directChatMatrixID !=
Future.value(L10n.of(context)!.emptyChat), room.lastEvent?.senderId,
builder: (context, snapshot) { ) ??
return Text( L10n.of(context)!.emptyChat,
room.membership == Membership.invite softWrap: false,
? L10n.of(context)!.youAreInvitedToThisChat maxLines: 1,
: snapshot.data ?? overflow: TextOverflow.ellipsis,
room.lastEvent?.calcLocalizedBodyFallback( style: TextStyle(
MatrixLocals(L10n.of(context)!), color: unread
hideReply: true, ? Theme.of(context).colorScheme.secondary
hideEdit: true, : Theme.of(context).textTheme.bodyText2!.color,
plaintextBody: true, decoration: room.lastEvent?.redacted == true
removeMarkdown: true, ? TextDecoration.lineThrough
withSenderNamePrefix: !room.isDirectChat || : null,
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), const SizedBox(width: 8),
AnimatedContainer( AnimatedContainer(

View File

@ -48,7 +48,7 @@ class ClientChooserButton extends StatelessWidget {
(client) => PopupMenuItem( (client) => PopupMenuItem(
value: client, value: client,
child: FutureBuilder<Profile>( child: FutureBuilder<Profile>(
future: client!.fetchOwnProfile(), future: client!.ownProfile,
builder: (context, snapshot) => Row( builder: (context, snapshot) => Row(
children: [ children: [
Avatar( Avatar(
@ -90,7 +90,7 @@ class ClientChooserButton extends StatelessWidget {
matrix.accountBundles.forEach((key, value) => clientCount += value.length); matrix.accountBundles.forEach((key, value) => clientCount += value.length);
return Center( return Center(
child: FutureBuilder<Profile>( child: FutureBuilder<Profile>(
future: matrix.client.fetchOwnProfile(), future: matrix.client.ownProfile,
builder: (context, snapshot) => Stack( builder: (context, snapshot) => Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [

View File

@ -19,6 +19,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; 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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -64,60 +69,52 @@ class _StreamView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.black54, color: Colors.black54,
), ),
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: <Widget>[ children: <Widget>[
if (videoMuted) if (videoMuted)
Container( Container(
color: Colors.transparent, color: Colors.transparent,
), ),
if (!videoMuted) if (!videoMuted)
RTCVideoView( RTCVideoView(
// yes, it must explicitly be casted even though I do not feel // yes, it must explicitly be casted even though I do not feel
// comfortable with it... // comfortable with it...
wrappedStream.renderer as RTCVideoRenderer, wrappedStream.renderer as RTCVideoRenderer,
mirror: mirrored, mirror: mirrored,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
), ),
if (videoMuted) if (videoMuted)
Positioned( Positioned(
child: Avatar( child: Avatar(
mxContent: avatarUrl, mxContent: avatarUrl,
name: displayName, name: displayName,
size: mainView ? 96 : 48, size: mainView ? 96 : 48,
client: matrixClient, client: matrixClient,
// textSize: mainView ? 36 : 24, // textSize: mainView ? 36 : 24,
// matrixClient: matrixClient, // matrixClient: matrixClient,
)), ),
if (!isScreenSharing) ),
Positioned( if (!isScreenSharing)
left: 4.0, Positioned(
bottom: 4.0, left: 4.0,
child: Icon(audioMuted ? Icons.mic_off : Icons.mic, bottom: 4.0,
color: Colors.white, size: 18.0), child: Icon(audioMuted ? Icons.mic_off : Icons.mic,
) color: Colors.white, size: 18.0),
], )
)); ],
),
);
} }
} }
class Calling extends StatefulWidget { class Calling extends StatefulWidget {
final VoidCallback? onClear; final VoipPlugin voipPlugin;
final BuildContext context; final VoidCallback onClear;
final String callId; const Calling({required this.voipPlugin, required this.onClear, Key? key})
final CallSession call;
final Client client;
const Calling(
{required this.context,
required this.call,
required this.client,
required this.callId,
this.onClear,
Key? key})
: super(key: key); : super(key: key);
@override @override
@ -125,51 +122,88 @@ class Calling extends StatefulWidget {
} }
class _MyCallingPage extends State<Calling> { class _MyCallingPage extends State<Calling> {
Room? get room => call?.room; late CallStateProxy? proxy;
late Room room;
String get displayName => call?.displayName ?? ''; String get displayName => proxy?.displayName ?? '';
String get callId => widget.callId;
CallSession? get call => widget.call;
MediaStream? get localStream { MediaStream? get localStream {
if (call != null && call!.localUserMediaStream != null) { if (proxy != null && proxy!.localUserMediaStream != null) {
return call!.localUserMediaStream!.stream!; return proxy!.localUserMediaStream!.stream!;
} }
return null; return null;
} }
MediaStream? get remoteStream { bool get isMicrophoneMuted => proxy?.isMicrophoneMuted ?? false;
if (call != null && call!.getRemoteStreams.isNotEmpty) { bool get isLocalVideoMuted => proxy?.isLocalVideoMuted ?? false;
return call!.getRemoteStreams[0].stream!; 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? _localVideoHeight;
double? _localVideoWidth; double? _localVideoWidth;
EdgeInsetsGeometry? _localVideoMargin; EdgeInsetsGeometry? _localVideoMargin;
CallState? _state;
void _playCallSound() async { void _playCallSound() async {
const path = 'assets/sounds/call.ogg'; const path = 'assets/sounds/call.ogg';
@ -190,25 +224,18 @@ class _MyCallingPage extends State<Calling> {
} }
void initialize() async { void initialize() async {
final call = this.call; if (voipPlugin.currentGroupCall != null) {
if (call == null) return; 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); proxy!.onStateChanged(_handleCallState);
call.onCallEventChanged.listen((event) { if (!proxy!.voiceonly) {
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) {
try { try {
// Enable wakelock (keep screen on) // Enable wakelock (keep screen on)
unawaited(Wakelock.enable()); unawaited(Wakelock.enable());
@ -219,97 +246,93 @@ class _MyCallingPage extends State<Calling> {
void cleanUp() { void cleanUp() {
Timer( Timer(
const Duration(seconds: 2), const Duration(seconds: 2),
() => widget.onClear?.call(), () => widget.onClear.call(),
); );
if (call?.type == CallType.kVideo) { if (!proxy!.voiceonly) {
try { try {
unawaited(Wakelock.disable()); unawaited(Wakelock.disable());
} catch (_) {} } catch (_) {}
} }
} }
@override
void dispose() {
super.dispose();
call?.cleanUp.call();
}
void _resizeLocalVideo(Orientation orientation) { void _resizeLocalVideo(Orientation orientation) {
final shortSide = min( final shortSide = min(
MediaQuery.of(context).size.width, MediaQuery.of(context).size.height); 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) ? const EdgeInsets.only(top: 20.0, right: 20.0)
: EdgeInsets.zero; : EdgeInsets.zero;
_localVideoWidth = remoteStream != null _localVideoWidth = userMediaStreams.isNotEmpty
? shortSide / 3 ? shortSide / 3
: MediaQuery.of(context).size.width; : MediaQuery.of(context).size.width;
_localVideoHeight = remoteStream != null _localVideoHeight = userMediaStreams.isNotEmpty
? shortSide / 4 ? shortSide / 4
: MediaQuery.of(context).size.height; : MediaQuery.of(context).size.height;
} }
void _handleCallState(CallState state) { void _handleCallState() {
Logs().v('CallingPage::handleCallState: ${state.toString()}'); Logs().v('CallingPage::handleCallState');
if (mounted) { if (mounted) {
setState(() { setState(() {
_state = state; if (proxy!.callState.toLowerCase() == 'ended') cleanUp();
if (_state == CallState.kEnded) cleanUp();
}); });
} }
} }
void _answerCall() { void handleAnswerButtonClick() {
setState(() { if (mounted) {
call?.answer(); setState(() {
}); proxy?.answer();
});
}
}
void handleHangupButtonClick() {
_hangUp();
} }
void _hangUp() { void _hangUp() {
setState(() { setState(() {
if (call != null && (call?.isRinging ?? false)) { proxy!.hangup();
call?.reject();
} else {
call?.hangup();
}
}); });
} }
void _muteMic() { void handleMicMuteButtonClick() {
setState(() { setState(() {
call?.setMicrophoneMuted(!call!.isMicrophoneMuted); proxy?.setMicrophoneMuted(!isMicrophoneMuted);
}); });
} }
void _screenSharing() { void handleScreenSharingButtonClick() {
setState(() { setState(() {
call?.setScreensharingEnabled(!call!.screensharingEnabled); proxy?.setScreensharingEnabled(!isScreensharingEnabled);
}); });
} }
void _remoteOnHold() { void handleHoldButtonClick() {
setState(() { setState(() {
call?.setRemoteOnHold(!call!.remoteOnHold); proxy?.setRemoteOnHold(!isRemoteOnHold);
}); });
} }
void _muteCamera() { void handleVideoMuteButtonClick() {
setState(() { setState(() {
call?.setLocalVideoMuted(!call!.isLocalVideoMuted); proxy?.setLocalVideoMuted(!isLocalVideoMuted);
}); });
} }
void _switchCamera() async { // Waiting for https://gitlab.com/famedly/company/frontend/famedlysdk/-/issues/301
if (call!.localUserMediaStream != null) { // void _switchCamera() async {
await Helper.switchCamera( // if (proxy!.localUserMediaStream != null) {
call!.localUserMediaStream!.stream!.getVideoTracks()[0]); // await Helper.switchCamera(
if (PlatformInfos.isMobile) { // proxy!.localUserMediaStream!.stream!.getVideoTracks()[0]);
call!.facingMode == 'user' // if (PlatformInfos.isMobile) {
? call!.facingMode = 'environment' // proxy!.facingMode == 'user'
: call!.facingMode = 'user'; // ? proxy!.facingMode = 'environment'
} // : proxy!.facingMode = 'user';
} // }
setState(() {}); // }
} // setState(() {});
// }
/* /*
void _switchSpeaker() { void _switchSpeaker() {
@ -320,16 +343,10 @@ class _MyCallingPage extends State<Calling> {
*/ */
List<Widget> _buildActionButtons(bool isFloating) { List<Widget> _buildActionButtons(bool isFloating) {
if (isFloating || call == null) { if (isFloating || proxy == null) {
return []; return [];
} }
final switchCameraButton = FloatingActionButton(
heroTag: 'switchCamera',
onPressed: _switchCamera,
backgroundColor: Colors.black45,
child: const Icon(Icons.switch_camera),
);
/* /*
var switchSpeakerButton = FloatingActionButton( var switchSpeakerButton = FloatingActionButton(
heroTag: 'switchSpeaker', heroTag: 'switchSpeaker',
@ -339,106 +356,79 @@ class _MyCallingPage extends State<Calling> {
backgroundColor: Theme.of(context).backgroundColor, backgroundColor: Theme.of(context).backgroundColor,
); );
*/ */
final hangupButton = FloatingActionButton( return [
heroTag: 'hangup', FloatingActionButton(
onPressed: _hangUp, heroTag: 'hangup',
tooltip: 'Hangup', onPressed: handleHangupButtonClick,
backgroundColor: _state == CallState.kEnded ? Colors.black45 : Colors.red, tooltip: 'Hangup',
child: const Icon(Icons.call_end), backgroundColor: proxy!.callState.toLowerCase() == 'ended'
); ? Colors.black45
: Colors.red,
final answerButton = FloatingActionButton( child: const Icon(Icons.call_end),
heroTag: 'answer', ),
onPressed: _answerCall, if (showAnswerButton)
tooltip: 'Answer', FloatingActionButton(
backgroundColor: Colors.green, heroTag: 'answer',
child: const Icon(Icons.phone), onPressed: handleAnswerButtonClick,
); tooltip: 'Answer',
backgroundColor: Colors.green,
final muteMicButton = FloatingActionButton( child: const Icon(Icons.phone),
heroTag: 'muteMic', ),
onPressed: _muteMic, if (showMicMuteButton)
foregroundColor: isMicrophoneMuted ? Colors.black26 : Colors.white, FloatingActionButton(
backgroundColor: isMicrophoneMuted ? Colors.white : Colors.black45, heroTag: 'muteMic',
child: Icon(isMicrophoneMuted ? Icons.mic_off : Icons.mic), onPressed: handleMicMuteButtonClick,
); foregroundColor: isMicrophoneMuted ? Colors.black26 : Colors.white,
backgroundColor: isMicrophoneMuted ? Colors.white : Colors.black45,
final screenSharingButton = FloatingActionButton( child: Icon(isMicrophoneMuted ? Icons.mic_off : Icons.mic),
heroTag: 'screenSharing', ),
onPressed: _screenSharing, if (showScreenSharingButton)
foregroundColor: isScreensharingEnabled ? Colors.black26 : Colors.white, FloatingActionButton(
backgroundColor: isScreensharingEnabled ? Colors.white : Colors.black45, heroTag: 'screenSharing',
child: const Icon(Icons.desktop_mac), onPressed: handleScreenSharingButtonClick,
); foregroundColor:
isScreensharingEnabled ? Colors.black26 : Colors.white,
final holdButton = FloatingActionButton( backgroundColor:
heroTag: 'hold', isScreensharingEnabled ? Colors.white : Colors.black45,
onPressed: _remoteOnHold, child: const Icon(Icons.desktop_mac),
foregroundColor: isRemoteOnHold ? Colors.black26 : Colors.white, ),
backgroundColor: isRemoteOnHold ? Colors.white : Colors.black45, if (showHoldButton)
child: const Icon(Icons.pause), FloatingActionButton(
); heroTag: 'hold',
onPressed: handleHoldButtonClick,
final muteCameraButton = FloatingActionButton( foregroundColor: isRemoteOnHold ? Colors.black26 : Colors.white,
heroTag: 'muteCam', backgroundColor: isRemoteOnHold ? Colors.white : Colors.black45,
onPressed: _muteCamera, child: const Icon(Icons.pause),
foregroundColor: isLocalVideoMuted ? Colors.black26 : Colors.white, ),
backgroundColor: isLocalVideoMuted ? Colors.white : Colors.black45, // FloatingActionButton(
child: Icon(isLocalVideoMuted ? Icons.videocam_off : Icons.videocam), // heroTag: 'switchCamera',
); // onPressed: _switchCamera,
// backgroundColor: Colors.black45,
switch (_state) { // child: const Icon(Icons.switch_camera),
case CallState.kRinging: // ),
case CallState.kInviteSent: if (showVideoMuteButton)
case CallState.kCreateAnswer: FloatingActionButton(
case CallState.kConnecting: heroTag: 'muteCam',
return call!.isOutgoing onPressed: handleVideoMuteButtonClick,
? <Widget>[hangupButton] foregroundColor: isLocalVideoMuted ? Colors.black26 : Colors.white,
: <Widget>[answerButton, hangupButton]; backgroundColor: isLocalVideoMuted ? Colors.white : Colors.black45,
case CallState.kConnected: child: Icon(isLocalVideoMuted ? Icons.videocam_off : Icons.videocam),
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>[];
} }
List<Widget> _buildContent(Orientation orientation, bool isFloating) { List<Widget> _buildP2PView(Orientation orientation, bool isFloating) {
final stackWidgets = <Widget>[]; final stackWidgets = <Widget>[];
final call = this.call; if (proxy == null || proxy!.ended) {
if (call == null || call.callHasEnded) {
return stackWidgets; return stackWidgets;
} }
if (call.localHold || call.remoteOnHold) { if (proxy!.localHold || proxy!.remoteOnHold) {
var title = ''; var title = '';
if (call.localHold) { if (proxy!.localHold) {
title = '${call.displayName} held the call.'; title = '${proxy!.displayName} held the call.';
} else if (call.remoteOnHold) { } else if (proxy!.remoteOnHold) {
title = 'You held the call.'; title = 'You held the call.';
} }
stackWidgets.add(Center( stackWidgets.add(Center(
@ -460,20 +450,16 @@ class _MyCallingPage extends State<Calling> {
return stackWidgets; return stackWidgets;
} }
var primaryStream = call.remoteScreenSharingStream ??
call.localScreenSharingStream ??
call.remoteUserMediaStream ??
call.localUserMediaStream;
if (!connected) {
primaryStream = call.localUserMediaStream;
}
if (primaryStream != null) { if (primaryStream != null) {
stackWidgets.add(Center( stackWidgets.add(
child: _StreamView(primaryStream, Center(
mainView: true, matrixClient: widget.client), child: _StreamView(
)); primaryStream!,
mainView: true,
matrixClient: voipPlugin.client,
),
),
);
} }
if (isFloating || !connected) { if (isFloating || !connected) {
@ -482,39 +468,20 @@ class _MyCallingPage extends State<Calling> {
_resizeLocalVideo(orientation); _resizeLocalVideo(orientation);
if (call.getRemoteStreams.isEmpty) { if (userMediaStreams.isEmpty) {
return stackWidgets; return stackWidgets;
} }
final secondaryStreamViews = <Widget>[]; final secondaryStreamViews = <Widget>[];
if (call.remoteScreenSharingStream != null) { for (final stream in userMediaStreams) {
final remoteUserMediaStream = call.remoteUserMediaStream;
secondaryStreamViews.add(SizedBox( secondaryStreamViews.add(SizedBox(
width: _localVideoWidth, width: _localVideoWidth,
height: _localVideoHeight, height: _localVideoHeight,
child: _StreamView(remoteUserMediaStream!, matrixClient: widget.client), child: _StreamView(
)); stream,
secondaryStreamViews.add(const SizedBox(height: 10)); matrixClient: voipPlugin.client,
} ),
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),
)); ));
secondaryStreamViews.add(const SizedBox(height: 10)); secondaryStreamViews.add(const SizedBox(height: 10));
} }
@ -536,6 +503,8 @@ class _MyCallingPage extends State<Calling> {
return stackWidgets; return stackWidgets;
} }
WrappedMediaStream get screenSharing => screenSharingStreams.elementAt(0);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PIPView(builder: (context, isFloating) { return PIPView(builder: (context, isFloating) {
@ -544,11 +513,13 @@ class _MyCallingPage extends State<Calling> {
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat, FloatingActionButtonLocation.centerFloat,
floatingActionButton: SizedBox( floatingActionButton: SizedBox(
width: 320.0, width: 320.0,
height: 150.0, height: 150.0,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _buildActionButtons(isFloating))), children: _buildActionButtons(isFloating),
),
),
body: OrientationBuilder( body: OrientationBuilder(
builder: (BuildContext context, Orientation orientation) { builder: (BuildContext context, Orientation orientation) {
return Container( return Container(
@ -556,13 +527,16 @@ class _MyCallingPage extends State<Calling> {
color: Colors.black87, color: Colors.black87,
), ),
child: Stack(children: [ child: Stack(children: [
..._buildContent(orientation, isFloating), if (isGroupCall)
GroupCallView(call: proxy as GroupCallSessionState)
else
..._buildP2PView(orientation, isFloating),
if (!isFloating) if (!isFloating)
Positioned( Positioned(
top: 24.0, top: 24.0,
left: 24.0, left: 24.0,
child: IconButton( child: IconButton(
color: Colors.black45, color: Colors.red,
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () { onPressed: () {
PIPView.of(context)?.setFloating(true); PIPView.of(context)?.setFloating(true);

View 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,
),
),
);
}
});
}
}

View File

@ -38,7 +38,7 @@ class InvitationSelectionController extends State<InvitationSelection> {
final participantsIds = participants.map((p) => p.stateKey).toList(); final participantsIds = participants.map((p) => p.stateKey).toList();
final contacts = client.rooms final contacts = client.rooms
.where((r) => r.isDirectChat) .where((r) => r.isDirectChat)
.map((r) => r.unsafeGetUserFromMemoryOrFallback(r.directChatMatrixID!)) .map((r) => r.getUserByMXIDSync(r.directChatMatrixID!))
.toList() .toList()
..removeWhere((u) => participantsIds.contains(u.stateKey)); ..removeWhere((u) => participantsIds.contains(u.stateKey));
contacts.sort( contacts.sort(

View File

@ -111,7 +111,7 @@ class _KeyVerificationPageState extends State<KeyVerificationDialog> {
if (directChatId != null) { if (directChatId != null) {
user = widget.request.client user = widget.request.client
.getRoomById(directChatId)! .getRoomById(directChatId)!
.unsafeGetUserFromMemoryOrFallback(widget.request.userId); .getUserByMXIDSync(widget.request.userId);
} }
final displayName = final displayName =
user?.calcDisplayname() ?? widget.request.userId.localpart!; user?.calcDisplayname() ?? widget.request.userId.localpart!;

View File

@ -6,7 +6,6 @@ import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import '../../config/app_config.dart';
import '../../widgets/content_banner.dart'; import '../../widgets/content_banner.dart';
import 'settings.dart'; import 'settings.dart';

View File

@ -370,7 +370,7 @@ class StoryPageController extends State<StoryPage> {
.client .client
.getRoomById(roomId) .getRoomById(roomId)
?.getState(EventTypes.RoomCreate) ?.getState(EventTypes.RoomCreate)
?.senderFromMemoryOrFallback ?.sender
.avatarUrl; .avatarUrl;
String get title => String get title =>
@ -378,7 +378,7 @@ class StoryPageController extends State<StoryPage> {
.client .client
.getRoomById(roomId) .getRoomById(roomId)
?.getState(EventTypes.RoomCreate) ?.getState(EventTypes.RoomCreate)
?.senderFromMemoryOrFallback ?.sender
.calcDisplayname() ?? .calcDisplayname() ??
'Story not found'; 'Story not found';
@ -485,8 +485,7 @@ class StoryPageController extends State<StoryPage> {
case PopupStoryAction.message: case PopupStoryAction.message:
final roomIdResult = await showFutureLoadingDialog( final roomIdResult = await showFutureLoadingDialog(
context: context, context: context,
future: () => future: () => currentEvent!.sender.startDirectChat(),
currentEvent!.senderFromMemoryOrFallback.startDirectChat(),
); );
if (roomIdResult.error != null) return; if (roomIdResult.error != null) return;
VRouter.of(context).toSegments(['rooms', roomIdResult.result!]); VRouter.of(context).toSegments(['rooms', roomIdResult.result!]);

View File

@ -84,7 +84,13 @@ class BackgroundPush {
client: client, client: client,
l10n: l10n, l10n: l10n,
activeRoomId: router?.currentState?.pathParameters['roomid'], 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, onNewToken: _newFcmToken,
); );
@ -215,8 +221,6 @@ class BackgroundPush {
} }
} }
bool _wentToRoomOnStartup = false;
Future<void> setupPush() async { Future<void> setupPush() async {
Logs().d("SetupPush"); Logs().d("SetupPush");
if (client.loginState != LoginState.loggedIn || if (client.loginState != LoginState.loggedIn ||
@ -235,20 +239,6 @@ class BackgroundPush {
} else { } else {
await setupFirebase(); 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 { Future<void> _noFcmWarning() async {
@ -384,6 +374,13 @@ class BackgroundPush {
client: client, client: client,
l10n: l10n, l10n: l10n,
activeRoomId: router?.currentState?.pathParameters['roomid'], activeRoomId: router?.currentState?.pathParameters['roomid'],
onSelectNotification: (string) async {
if (string != null) {
final payload = jsonDecode(string);
final roomId = payload['roomId'];
goToRoom(roomId);
}
},
); );
} }

View File

@ -7,12 +7,11 @@ import 'package:matrix/encryption/utils/key_verification.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.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/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 'package:fluffychat/utils/platform_infos.dart';
import 'famedlysdk_store.dart'; import 'famedlysdk_store.dart';
import 'matrix_sdk_extensions.dart/fluffybox_database.dart'; import 'matrix_sdk_extensions.dart/fluffybox_database.dart';
import 'matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart';
abstract class ClientManager { abstract class ClientManager {
static const String clientNamespace = 'im.fluffychat.store.clients'; static const String clientNamespace = 'im.fluffychat.store.clients';
@ -83,33 +82,29 @@ abstract class ClientManager {
await Store().setItem(clientNamespace, jsonEncode(clientNamesList)); await Store().setItem(clientNamespace, jsonEncode(clientNamesList));
} }
static Client createClient(String clientName) { static Client createClient(String clientName) => Client(
final _client = CustomHttpClient.createHTTPClient(); clientName,
return Client( verificationMethods: {
clientName, KeyVerificationMethod.numbers,
httpClient: _client, if (kIsWeb || PlatformInfos.isMobile || PlatformInfos.isLinux)
verificationMethods: { KeyVerificationMethod.emoji,
KeyVerificationMethod.numbers, },
if (kIsWeb || PlatformInfos.isMobile || PlatformInfos.isLinux) importantStateEvents: <String>{
KeyVerificationMethod.emoji, // To make room emotes work
}, 'im.ponies.room_emotes',
importantStateEvents: <String>{ // To check which story room we can post in
// To make room emotes work EventTypes.RoomPowerLevels,
'im.ponies.room_emotes', },
// To check which story room we can post in databaseBuilder: FlutterFluffyBoxDatabase.databaseBuilder,
EventTypes.RoomPowerLevels, legacyDatabaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder,
}, supportedLoginTypes: {
databaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder, AuthenticationTypes.password,
legacyDatabaseBuilder: FlutterFluffyBoxDatabase.databaseBuilder, if (PlatformInfos.isMobile ||
supportedLoginTypes: { PlatformInfos.isWeb ||
AuthenticationTypes.password, PlatformInfos.isMacOS)
if (PlatformInfos.isMobile || AuthenticationTypes.sso
PlatformInfos.isWeb || },
PlatformInfos.isMacOS) compute: compute,
AuthenticationTypes.sso customImageResizer: PlatformInfos.isMobile ? customImageResizer : null,
}, );
compute: compute,
customImageResizer: PlatformInfos.isMobile ? customImageResizer : null,
);
}
} }

View File

@ -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));
}

View File

@ -9,11 +9,7 @@ import 'package:fluffychat/utils/platform_infos.dart';
abstract class FluffyShare { abstract class FluffyShare {
static Future<void> share(String text, BuildContext context) async { static Future<void> share(String text, BuildContext context) async {
if (PlatformInfos.isMobile) { if (PlatformInfos.isMobile) {
final box = context.findRenderObject() as RenderBox; return Share.share(text);
return Share.share(
text,
sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size,
);
} }
await Clipboard.setData( await Clipboard.setData(
ClipboardData(text: text), ClipboardData(text: text),

View File

@ -12,8 +12,7 @@ extension ClientStoriesExtension on Client {
List<User> get contacts => rooms List<User> get contacts => rooms
.where((room) => room.isDirectChat) .where((room) => room.isDirectChat)
.map((room) => .map((room) => room.getUserByMXIDSync(room.directChatMatrixID!))
room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!))
.toList(); .toList();
List<Room> get storiesRooms => rooms List<Room> get storiesRooms => rooms

View File

@ -14,7 +14,6 @@ import 'package:path_provider/path_provider.dart';
import '../client_manager.dart'; import '../client_manager.dart';
import '../famedlysdk_store.dart'; import '../famedlysdk_store.dart';
// ignore: deprecated_member_use
class FlutterFluffyBoxDatabase extends FluffyBoxDatabase { class FlutterFluffyBoxDatabase extends FluffyBoxDatabase {
FlutterFluffyBoxDatabase( FlutterFluffyBoxDatabase(
String name, String name,
@ -28,7 +27,6 @@ class FlutterFluffyBoxDatabase extends FluffyBoxDatabase {
static const String _cipherStorageKey = 'database_encryption_key'; static const String _cipherStorageKey = 'database_encryption_key';
// ignore: deprecated_member_use
static Future<FluffyBoxDatabase> databaseBuilder(Client client) async { static Future<FluffyBoxDatabase> databaseBuilder(Client client) async {
Logs().d('Open FluffyBox...'); Logs().d('Open FluffyBox...');
fluffybox.HiveAesCipher? hiverCipher; fluffybox.HiveAesCipher? hiverCipher;
@ -61,7 +59,6 @@ class FlutterFluffyBoxDatabase extends FluffyBoxDatabase {
rethrow; rethrow;
} }
// ignore: deprecated_member_use
final db = FluffyBoxDatabase( final db = FluffyBoxDatabase(
'fluffybox_${client.clientName.replaceAll(' ', '_').toLowerCase()}', 'fluffybox_${client.clientName.replaceAll(' ', '_').toLowerCase()}',
await _findDatabasePath(client), await _findDatabasePath(client),

View File

@ -2,108 +2,78 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart' hide Key; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { import '../platform_infos.dart';
FlutterHiveCollectionsDatabase(
String name, class FlutterMatrixHiveStore extends FamedlySdkHiveDatabase {
String path, { FlutterMatrixHiveStore(String name, {HiveCipher? encryptionCipher})
HiveCipher? key, : super(
}) : super(
name, name,
path, encryptionCipher: encryptionCipher,
key: key,
); );
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 { Client client) async {
Logs().d('Open Hive...'); if (!kIsWeb && !_hiveInitialized) {
HiveAesCipher? hiverCipher; _hiveInitialized = true;
}
HiveCipher? hiverCipher;
try { try {
// Workaround for secure storage is calling Platform.operatingSystem on web // Workaround for secure storage is calling Platform.operatingSystem on web
if (kIsWeb) throw MissingPluginException(); if (kIsWeb || Platform.isLinux) throw MissingPluginException();
const secureStorage = FlutterSecureStorage(); const secureStorage = FlutterSecureStorage();
final containsEncryptionKey = final containsEncryptionKey =
await secureStorage.containsKey(key: _cipherStorageKey); await secureStorage.containsKey(key: _hiveCipherStorageKey);
if (!containsEncryptionKey) { if (!containsEncryptionKey) {
// do not try to create a buggy secure storage for new Linux users
if (Platform.isLinux) throw MissingPluginException();
final key = Hive.generateSecureKey(); final key = Hive.generateSecureKey();
await secureStorage.write( await secureStorage.write(
key: _cipherStorageKey, key: _hiveCipherStorageKey,
value: base64UrlEncode(key), value: base64UrlEncode(key),
); );
} }
// workaround for if we just wrote to the key and it still doesn't exist // 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(); if (rawEncryptionKey == null) throw MissingPluginException();
hiverCipher = HiveAesCipher(base64Url.decode(rawEncryptionKey)); final encryptionKey = base64Url.decode(rawEncryptionKey);
hiverCipher = HiveAesCipher(encryptionKey);
} on MissingPluginException catch (_) { } on MissingPluginException catch (_) {
Logs().i('Hive encryption is not supported on this platform'); Logs().i('Hive encryption is not supported on this platform');
} catch (_) {
const FlutterSecureStorage().delete(key: _cipherStorageKey);
rethrow;
} }
final db = FlutterMatrixHiveStore(
final db = FlutterHiveCollectionsDatabase( client.clientName,
'hive_collections_${client.clientName.replaceAll(' ', '_').toLowerCase()}', encryptionCipher: hiverCipher,
await _findDatabasePath(client),
key: hiverCipher,
); );
try { try {
await db.open(); await db.open();
} catch (_) { } catch (e, s) {
Logs().w('Unable to open Hive. Delete database and storage key...'); Logs().e('Unable to open Hive. Delete and try again...', e, s);
const FlutterSecureStorage().delete(key: _cipherStorageKey);
await db.clear(); await db.clear();
rethrow; await db.open();
} }
Logs().d('Hive is ready');
return db; 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 @override
int get maxFileSize => supportsFileStoring ? 100 * 1024 * 1024 : 0; int get maxFileSize => supportsFileStoring ? 100 * 1024 * 1024 : 0;
@override @override
bool get supportsFileStoring => !kIsWeb; bool get supportsFileStoring => (PlatformInfos.isIOS ||
PlatformInfos.isAndroid ||
PlatformInfos.isDesktop);
Future<String> _getFileStoreDirectory() async { Future<String> _getFileStoreDirectory() async {
try { try {

View File

@ -25,11 +25,7 @@ extension MatrixFileExtension on MatrixFile {
final tmpDirectory = await getTemporaryDirectory(); final tmpDirectory = await getTemporaryDirectory();
final path = '${tmpDirectory.path}$fileName'; final path = '${tmpDirectory.path}$fileName';
await File(path).writeAsBytes(bytes); await File(path).writeAsBytes(bytes);
final box = context.findRenderObject() as RenderBox; await Share.shareFiles([path]);
await Share.shareFiles(
[path],
sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size,
);
return; return;
} }

View File

@ -1,7 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 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_gen/gen_l10n/l10n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:matrix/matrix.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/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/utils/client_manager.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'; import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
Future<void> pushHelper( Future<void> pushHelper(
@ -32,7 +35,6 @@ Future<void> pushHelper(
Logs().v('Room is in foreground. Stop push helper here.'); Logs().v('Room is in foreground. Stop push helper here.');
return; return;
} }
// Initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project // Initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
final _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await _flutterLocalNotificationsPlugin.initialize( await _flutterLocalNotificationsPlugin.initialize(
@ -56,8 +58,11 @@ Future<void> pushHelper(
await store.setString(SettingKeys.notificationCurrentIds, json.encode({})); await store.setString(SettingKeys.notificationCurrentIds, json.encode({}));
return; 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); l10n ??= await L10n.delegate.load(window.locale);
final matrixLocals = MatrixLocals(l10n); final matrixLocals = MatrixLocals(l10n);
@ -81,8 +86,9 @@ Future<void> pushHelper(
.toString(); .toString();
final avatarFile = final avatarFile =
avatar == null ? null : await DefaultCacheManager().getSingleFile(avatar); avatar == null ? null : await DefaultCacheManager().getSingleFile(avatar);
final bool isCall = event.type != EventTypes.CallHangup; // TODO: handle this properly
// Show notification // Show notification
const insistentFlag = 4;
final androidPlatformChannelSpecifics = AndroidNotificationDetails( final androidPlatformChannelSpecifics = AndroidNotificationDetails(
AppConfig.pushNotificationsChannelId, AppConfig.pushNotificationsChannelId,
AppConfig.pushNotificationsChannelName, AppConfig.pushNotificationsChannelName,
@ -106,8 +112,11 @@ Future<void> pushHelper(
), ),
ticker: l10n.unreadChats(notification.counts?.unread ?? 1), ticker: l10n.unreadChats(notification.counts?.unread ?? 1),
importance: Importance.max, importance: Importance.max,
priority: Priority.high, priority: Priority.max,
groupKey: event.room.id, category: isCall ? 'call' : 'msg',
fullScreenIntent: isCall ? true : false,
additionalFlags: isCall ? Int32List.fromList(<int>[insistentFlag]) : null,
groupKey: event.roomId,
); );
const iOSPlatformChannelSpecifics = IOSNotificationDetails(); const iOSPlatformChannelSpecifics = IOSNotificationDetails();
final platformChannelSpecifics = NotificationDetails( final platformChannelSpecifics = NotificationDetails(
@ -119,8 +128,25 @@ Future<void> pushHelper(
event.room.displayname, event.room.displayname,
body, body,
platformChannelSpecifics, 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!'); Logs().v('Push helper has been completed!');
} }

View 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;
}
}

View 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);
}

View File

@ -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!);
}
}

View 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;
}

View File

@ -4,11 +4,13 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.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:flutter_webrtc/flutter_webrtc.dart' as webrtc_impl;
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:webrtc_interface/webrtc_interface.dart' hide Navigator; import 'package:webrtc_interface/webrtc_interface.dart' hide Navigator;
import 'package:fluffychat/pages/dialer/dialer.dart'; import 'package:fluffychat/pages/dialer/dialer.dart';
import 'package:fluffychat/utils/famedlysdk_store.dart';
import '../../utils/voip/user_media_manager.dart'; import '../../utils/voip/user_media_manager.dart';
class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate { class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
@ -46,6 +48,43 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
// //
// hours wasted: 5 // hours wasted: 5
BuildContext context; 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 { void _handleNetworkChanged(ConnectivityResult result) async {
/// Got a new connectivity status! /// Got a new connectivity status!
@ -64,8 +103,8 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
state != AppLifecycleState.paused); state != AppLifecycleState.paused);
} }
void addCallingOverlay( void addCallingOverlay(BuildContext context) {
BuildContext context, String callId, CallSession call) { Logs().d('[VOIP] addCallingOverlay: adding overlay');
if (overlayEntry != null) { if (overlayEntry != null) {
Logs().w('[VOIP] addCallingOverlay: The call session already exists?'); Logs().w('[VOIP] addCallingOverlay: The call session already exists?');
overlayEntry?.remove(); overlayEntry?.remove();
@ -76,20 +115,14 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
showDialog( showDialog(
context: context, context: context,
builder: (context) => Calling( builder: (context) => Calling(
context: context, voipPlugin: this,
client: client,
callId: callId,
call: call,
onClear: () => Navigator.of(context).pop(), onClear: () => Navigator.of(context).pop(),
), ),
); );
} else { } else {
overlayEntry = OverlayEntry( overlayEntry = OverlayEntry(
builder: (_) => Calling( builder: (_) => Calling(
context: context, voipPlugin: this,
client: client,
callId: callId,
call: call,
onClear: () { onClear: () {
overlayEntry?.remove(); overlayEntry?.remove();
overlayEntry = null; overlayEntry = null;
@ -97,6 +130,7 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
); );
Overlay.of(context)!.insert(overlayEntry!); Overlay.of(context)!.insert(overlayEntry!);
} }
Logs().d('[VOIP] addCallingOverlay: adding done');
} }
@override @override
@ -139,12 +173,11 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
@override @override
void handleNewCall(CallSession call) async { void handleNewCall(CallSession call) async {
Logs().d('[VOIP] handling new call');
/// Popup CallingPage for incoming call. /// Popup CallingPage for incoming call.
if (!background) { addCallingOverlay(context);
addCallingOverlay(context, call.callId, call); Logs().d('[VOIP] overlay stuff should be there up there');
} else {
onIncomingCall?.call(call);
}
} }
@override @override
@ -152,6 +185,29 @@ class VoipPlugin extends WidgetsBindingObserver implements WebRTCDelegate {
if (overlayEntry != null) { if (overlayEntry != null) {
overlayEntry?.remove(); overlayEntry?.remove();
overlayEntry = null; 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;
} }
} }
} }

View File

@ -31,7 +31,7 @@ extension LocalNotificationsExtension on MatrixState {
final event = Event.fromJson(eventUpdate.content, room); final event = Event.fromJson(eventUpdate.content, room);
final title = final title =
room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)!)); room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)!));
final body = await event.calcLocalizedBody( final body = event.getLocalizedBody(
MatrixLocals(L10n.of(widget.context)!), MatrixLocals(L10n.of(widget.context)!),
withSenderNamePrefix: withSenderNamePrefix:
!room.isDirectChat || room.lastEvent?.senderId == client.userID, !room.isDirectChat || room.lastEvent?.senderId == client.userID,
@ -40,11 +40,8 @@ extension LocalNotificationsExtension on MatrixState {
hideEdit: true, hideEdit: true,
removeMarkdown: true, removeMarkdown: true,
); );
final icon = event.senderFromMemoryOrFallback.avatarUrl?.getThumbnail( final icon = event.sender.avatarUrl?.getThumbnail(client,
client, width: 64, height: 64, method: ThumbnailMethod.crop) ??
width: 64,
height: 64,
method: ThumbnailMethod.crop) ??
room.avatar?.getThumbnail(client, room.avatar?.getThumbnail(client,
width: 64, height: 64, method: ThumbnailMethod.crop); width: 64, height: 64, method: ThumbnailMethod.crop);
if (kIsWeb) { if (kIsWeb) {

View File

@ -33,7 +33,6 @@ import '../pages/key_verification/key_verification_dialog.dart';
import '../utils/account_bundles.dart'; import '../utils/account_bundles.dart';
import '../utils/background_push.dart'; import '../utils/background_push.dart';
import '../utils/famedlysdk_store.dart'; import '../utils/famedlysdk_store.dart';
import '../utils/platform_infos.dart';
import 'local_notifications_extension.dart'; import 'local_notifications_extension.dart';
// import 'package:flutter_secure_storage/flutter_secure_storage.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()); onLoginStateChanged.values.map((s) => s.cancel());
onOwnPresence.values.map((s) => s.cancel()); onOwnPresence.values.map((s) => s.cancel());
onNotification.values.map((s) => s.cancel()); onNotification.values.map((s) => s.cancel());
client.httpClient.close();
onFocusSub?.cancel(); onFocusSub?.cancel();
onBlurSub?.cancel(); onBlurSub?.cancel();
_backgroundPush?.onLogin?.cancel(); _backgroundPush?.onLogin?.cancel();

View File

@ -517,6 +517,15 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.3.0" 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: flutter_highlight:
dependency: transitive dependency: transitive
description: description:
@ -717,10 +726,10 @@ packages:
flutter_webrtc: flutter_webrtc:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc path: "/home/techno_disaster/Projects/Famedly/flutter-webrtc"
url: "https://pub.dartlang.org" relative: false
source: hosted source: path
version: "0.8.7" version: "0.8.9"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -938,8 +947,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: null-safety ref: "2906e65ffaa96afbe6c72e8477d4dfcdfd06c2c3"
resolved-ref: a3d4020911860ff091d90638ab708604b71d2c5a resolved-ref: "2906e65ffaa96afbe6c72e8477d4dfcdfd06c2c3"
url: "https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git" url: "https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git"
source: git source: git
version: "0.1.4" version: "0.1.4"
@ -1016,10 +1025,10 @@ packages:
matrix: matrix:
dependency: "direct main" dependency: "direct main"
description: description:
name: matrix path: "/home/techno_disaster/Projects/Famedly/famedlysdk"
url: "https://pub.dartlang.org" relative: false
source: hosted source: path
version: "0.9.12" version: "0.10.0"
matrix_api_lite: matrix_api_lite:
dependency: transitive dependency: transitive
description: description:
@ -1464,7 +1473,7 @@ packages:
name: share_plus name: share_plus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.8" version: "4.0.6"
share_plus_linux: share_plus_linux:
dependency: transitive dependency: transitive
description: description:
@ -1947,7 +1956,7 @@ packages:
name: visibility_detector name: visibility_detector
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.3" version: "0.2.2"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -2026,12 +2035,12 @@ packages:
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
webrtc_interface: webrtc_interface:
dependency: transitive dependency: "direct overridden"
description: description:
name: webrtc_interface path: "/home/techno_disaster/Projects/Famedly/webrtc-interface"
url: "https://pub.dartlang.org" relative: false
source: hosted source: path
version: "1.0.4" version: "1.0.5"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View File

@ -57,7 +57,7 @@ dependencies:
keyboard_shortcuts: ^0.1.4 keyboard_shortcuts: ^0.1.4
localstorage: ^4.0.0+1 localstorage: ^4.0.0+1
lottie: ^1.2.2 lottie: ^1.2.2
matrix: ^0.9.12 matrix: ^0.9.4
matrix_homeserver_recommendations: ^0.2.0 matrix_homeserver_recommendations: ^0.2.0
matrix_link_text: ^1.0.2 matrix_link_text: ^1.0.2
native_imaging: native_imaging:
@ -75,7 +75,7 @@ dependencies:
salomon_bottom_bar: ^3.2.0 salomon_bottom_bar: ^3.2.0
scroll_to_index: ^2.1.1 scroll_to_index: ^2.1.1
sentry: ^6.3.0 sentry: ^6.3.0
share_plus: ^4.0.8 share_plus: ^4.0.6
shared_preferences: ^2.0.13 shared_preferences: ^2.0.13
slugify: ^2.0.0 slugify: ^2.0.0
snapping_sheet: ^3.1.0 snapping_sheet: ^3.1.0
@ -88,6 +88,10 @@ dependencies:
video_player: ^2.2.18 video_player: ^2.2.18
vrouter: ^1.2.0+21 vrouter: ^1.2.0+21
wakelock: ^0.6.1+1 wakelock: ^0.6.1+1
flutter_foreground_task:
git:
url: git@github.com:Techno-Disaster/flutter_foreground_task.git
ref: td/lockScreen
dev_dependencies: dev_dependencies:
dart_code_metrics: ^4.10.1 dart_code_metrics: ^4.10.1
@ -146,7 +150,7 @@ dependency_overrides:
keyboard_shortcuts: keyboard_shortcuts:
git: git:
url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git
ref: null-safety ref: 2906e65ffaa96afbe6c72e8477d4dfcdfd06c2c3
provider: 5.0.0 provider: 5.0.0
# For Flutter 3.0.0 compatibility # For Flutter 3.0.0 compatibility
# https://github.com/juliuscanute/qr_code_scanner/issues/532 # https://github.com/juliuscanute/qr_code_scanner/issues/532
@ -160,3 +164,9 @@ dependency_overrides:
git: git:
url: https://github.com/TheOneWithTheBraid/snapping_sheet.git url: https://github.com/TheOneWithTheBraid/snapping_sheet.git
ref: listenable 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

View File

@ -2,7 +2,7 @@ import 'package:matrix/encryption/utils/key_verification.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:matrix_api_lite/fake_matrix_api.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({ Future<Client> prepareTestClient({
bool loggedIn = false, bool loggedIn = false,
@ -20,7 +20,7 @@ Future<Client> prepareTestClient({
importantStateEvents: <String>{ importantStateEvents: <String>{
'im.ponies.room_emotes', // we want emotes to work properly 'im.ponies.room_emotes', // we want emotes to work properly
}, },
databaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder, databaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder,
supportedLoginTypes: { supportedLoginTypes: {
AuthenticationTypes.password, AuthenticationTypes.password,
AuthenticationTypes.sso AuthenticationTypes.sso