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