feat: group calls stash

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

View File

@ -23,9 +23,10 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.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>

View File

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

View File

@ -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 sha pogut definir l'àlies",
"couldNotSetDisplayname": "No sha 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": "Sha 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": "Lesdeveniment «{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 dextrem 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": {}
}
}

View File

@ -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": {}
}

View File

@ -1,32 +0,0 @@
// ignore: constant_identifier_names
const String ISRG_X1 = """-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----""";

View File

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

View File

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

View File

@ -25,7 +25,6 @@ import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/ios_badge_client_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/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,

View File

@ -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']),

View File

@ -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,

View File

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

View File

@ -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),

View File

@ -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,

View File

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

View File

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

View File

@ -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,
],
),

View File

@ -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),

View File

@ -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(),
),
),
),
],

View File

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

View File

@ -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(

View File

@ -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() {

View File

@ -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(

View File

@ -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: [

View File

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

View File

@ -0,0 +1,166 @@
import 'package:fluffychat/utils/voip/group_call_state.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:matrix/matrix.dart';
class GroupCallView extends StatefulWidget {
final GroupCallSessionState call;
const GroupCallView({
Key? key,
required this.call,
}) : super(key: key);
@override
State<GroupCallView> createState() => _GroupCallViewState();
}
class _GroupCallViewState extends State<GroupCallView> {
WrappedMediaStream? get primaryStream => widget.call.primaryStream;
List<WrappedMediaStream> get screenSharingStreams =>
widget.call.screenSharingStreams;
List<WrappedMediaStream> get userMediaStreams => widget.call.userMediaStreams;
WrappedMediaStream? get primaryScreenShare =>
widget.call.screenSharingStreams.first;
void updateStreams() {
Logs().i('Group calls, updating streams');
widget.call.groupCall.onStreamAdd.stream.listen((event) {
if (event.purpose == SDPStreamMetadataPurpose.Usermedia) {
if (userMediaStreams.indexWhere((element) => element == event) == -1) {
setState(() {
userMediaStreams.add(event);
});
}
} else if (event.purpose == SDPStreamMetadataPurpose.Screenshare) {
if (screenSharingStreams.indexWhere((element) => element == event) ==
-1) {
setState(() {
screenSharingStreams.add(event);
});
}
}
});
widget.call.groupCall.onStreamRemoved.stream.listen((event) {
if (event.purpose == SDPStreamMetadataPurpose.Usermedia) {
userMediaStreams
.removeWhere((element) => element.stream!.id == event.stream!.id);
setState(() {
userMediaStreams.remove(event);
});
} else if (event.purpose == SDPStreamMetadataPurpose.Screenshare) {
screenSharingStreams
.removeWhere((element) => element.stream!.id == event.stream!.id);
setState(() {
screenSharingStreams.remove(event);
});
}
});
}
@override
void initState() {
updateStreams();
super.initState();
}
@override
Widget build(BuildContext context) {
Logs().e('Group call state: ' + widget.call.groupCall.state);
Logs().e('Group call state: ' +
widget.call.groupCall.participants.length.toString());
if (screenSharingStreams.isNotEmpty) {
return Center(
child: ListView(
children: [
RTCVideoView(
primaryScreenShare!.renderer as RTCVideoRenderer,
mirror: primaryScreenShare!.isLocal(),
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
),
const SizedBox(
height: 100,
),
CallGrid(
call: widget.call,
screenSharing: true,
userMediaStreams: userMediaStreams,
),
],
),
);
} else {
// No one is screen sharing, show avatars and user streams here
return Center(
child: CallGrid(
call: widget.call,
screenSharing: false,
userMediaStreams: userMediaStreams,
),
);
}
}
}
class CallGrid extends StatefulWidget {
final GroupCallSessionState call;
final bool screenSharing;
final List<WrappedMediaStream> userMediaStreams;
const CallGrid(
{Key? key,
required this.call,
required this.screenSharing,
required this.userMediaStreams})
: super(key: key);
@override
State<CallGrid> createState() => _CallGridState();
}
class _CallGridState extends State<CallGrid> {
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
return GridView.builder(
itemCount: widget.call.groupCall.participants.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.screenSharing ? 4 : 2),
itemBuilder: (context, index) {
final participant = widget.call.groupCall.participants[index];
Logs().e('Group calls participants - ' +
participant.displayName.toString());
if (widget.userMediaStreams
.map((stream) => stream.userId)
.contains(participant.id)) {
return Container(
color: Colors.amber,
child: RTCVideoView(
widget.userMediaStreams
.firstWhere((element) => element.userId == participant.id)
.renderer as RTCVideoRenderer,
mirror: false,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
),
);
} else {
return Center(
child: Container(
height: 200,
width: 200,
color: Colors.red,
child: Avatar(
mxContent: participant.avatarUrl,
name: participant.displayName,
size: 24,
client: client,
),
),
);
}
});
}
}

View File

@ -38,7 +38,7 @@ class InvitationSelectionController extends State<InvitationSelection> {
final participantsIds = participants.map((p) => p.stateKey).toList();
final 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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:fluffychat/config/isrg_x1.dart';
class CustomHttpClient {
static HttpClient customHttpClient(String? cert) {
final context = SecurityContext.defaultContext;
try {
if (cert != null) {
final bytes = utf8.encode(cert);
context.setTrustedCertificatesBytes(bytes);
}
} on TlsException catch (e) {
if (e.osError != null &&
e.osError!.message.contains('CERT_ALREADY_IN_HASH_TABLE')) {
} else {
rethrow;
}
}
return HttpClient(context: context);
}
static http.Client createHTTPClient() => IOClient(customHttpClient(ISRG_X1));
}

View File

@ -9,11 +9,7 @@ import 'package:fluffychat/utils/platform_infos.dart';
abstract class FluffyShare {
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),

View File

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

View File

@ -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),

View File

@ -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 {

View File

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

View File

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

View File

@ -0,0 +1,191 @@
import 'package:fluffychat/utils/voip/call_state_proxy.dart';
import 'package:matrix/matrix.dart';
class CallSessionState implements CallStateProxy {
final CallSession call;
Function()? callback;
CallSessionState(this.call) {
call.onCallEventChanged.stream.listen((CallEvent event) {
if (event == CallEvent.kState ||
event == CallEvent.kFeedsChanged ||
event == CallEvent.kLocalHoldUnhold ||
event == CallEvent.kRemoteHoldUnhold) {
if (event == CallEvent.kFeedsChanged) {
call.tryRemoveStopedStreams();
}
callback?.call();
}
});
call.onCallStateChanged.stream.listen((state) {
callback?.call();
});
}
@override
bool get voiceonly => call.type == CallType.kVoice;
@override
bool get connecting => call.state == CallState.kConnecting;
@override
bool get connected => call.state == CallState.kConnected;
@override
bool get ended => call.state == CallState.kEnded;
@override
bool get isOutgoing => call.isOutgoing;
@override
bool get ringingPlay => call.state == CallState.kInviteSent;
@override
void answer() => call.answer();
@override
void enter() {
// TODO: implement enter
}
@override
void hangup() {
if (call.isRinging) {
call.reject();
} else {
call.hangup();
}
}
@override
bool get isLocalVideoMuted => call.isLocalVideoMuted;
@override
bool get isMicrophoneMuted => call.isMicrophoneMuted;
@override
bool get isRemoteOnHold => call.remoteOnHold;
@override
bool get localHold => call.localHold;
@override
bool get remoteOnHold => call.remoteOnHold;
@override
bool get isScreensharingEnabled => call.screensharingEnabled;
@override
bool get callOnHold => call.localHold || call.remoteOnHold;
@override
void setLocalVideoMuted(bool muted) {
call.setLocalVideoMuted(muted);
callback?.call();
}
@override
void setMicrophoneMuted(bool muted) {
call.setMicrophoneMuted(muted);
// TODO(Nico): Refactor this to be more granular
callback?.call();
}
@override
void setRemoteOnHold(bool onHold) {
call.setRemoteOnHold(onHold);
callback?.call();
}
@override
void setScreensharingEnabled(bool enabled) {
call.setScreensharingEnabled(enabled);
callback?.call();
}
@override
String get callState {
switch (call.state) {
case CallState.kCreateAnswer:
case CallState.kFledgling:
case CallState.kWaitLocalMedia:
case CallState.kCreateOffer:
break;
case CallState.kRinging:
state = 'Ringing';
break;
case CallState.kInviteSent:
state = 'Invite Sent';
break;
case CallState.kConnecting:
state = 'Connecting';
break;
case CallState.kConnected:
state = 'Connected';
break;
case CallState.kEnded:
state = 'Ended';
break;
}
return state;
}
String state = 'New Call';
@override
WrappedMediaStream? get localUserMediaStream => call.localUserMediaStream;
@override
WrappedMediaStream? get localScreenSharingStream =>
call.localScreenSharingStream;
@override
List<WrappedMediaStream> get screenSharingStreams {
final streams = <WrappedMediaStream>[];
if (connected) {
if (call.remoteScreenSharingStream != null) {
streams.add(call.remoteScreenSharingStream!);
}
if (call.localScreenSharingStream != null) {
streams.add(call.localScreenSharingStream!);
}
}
return streams;
}
@override
List<WrappedMediaStream> get userMediaStreams {
final streams = <WrappedMediaStream>[];
if (connected) {
if (call.remoteUserMediaStream != null) {
streams.add(call.remoteUserMediaStream!);
}
if (call.localUserMediaStream != null) {
streams.add(call.localUserMediaStream!);
}
}
return streams;
}
@override
WrappedMediaStream? get primaryStream {
if (screenSharingStreams.isNotEmpty) {
return screenSharingStreams.first;
}
if (userMediaStreams.isNotEmpty) {
return userMediaStreams.first;
}
if (!connected) {
return call.localUserMediaStream;
}
return call.localScreenSharingStream ?? call.localUserMediaStream;
}
@override
String? get displayName => call.displayName;
@override
void onStateChanged(Function() handler) {
callback = handler;
}
}

View File

@ -0,0 +1,45 @@
import 'package:matrix/matrix.dart';
abstract class CallStateProxy {
String? get displayName;
bool get isMicrophoneMuted;
bool get isLocalVideoMuted;
bool get isScreensharingEnabled;
bool get isRemoteOnHold;
bool get localHold;
bool get remoteOnHold;
bool get voiceonly;
bool get connecting;
bool get connected;
bool get ended;
bool get callOnHold;
bool get isOutgoing;
bool get ringingPlay;
String get callState;
void answer();
void hangup();
void enter();
void setMicrophoneMuted(bool muted);
void setLocalVideoMuted(bool muted);
void setScreensharingEnabled(bool enabled);
void setRemoteOnHold(bool onHold);
WrappedMediaStream? get localUserMediaStream;
WrappedMediaStream? get localScreenSharingStream;
WrappedMediaStream? get primaryStream;
List<WrappedMediaStream> get screenSharingStreams;
List<WrappedMediaStream> get userMediaStreams;
void onStateChanged(Function() callback);
}

View File

@ -1,312 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:callkeep/callkeep.dart';
import 'package:matrix/matrix.dart';
import 'package:uuid/uuid.dart';
import 'package:fluffychat/utils/voip_plugin.dart';
class CallKeeper {
CallKeeper(this.callKeepManager, this.uuid, this.number, this.call) {
call?.onCallStateChanged.listen(_handleCallState);
}
CallKeepManager callKeepManager;
String number;
String uuid;
bool held = false;
bool muted = false;
bool connected = false;
CallSession? call;
void _handleCallState(CallState state) {
Logs().v('CallKeepManager::handleCallState: ${state.toString()}');
switch (state) {
case CallState.kConnecting:
break;
case CallState.kConnected:
if (!connected) {
callKeepManager.answer(uuid);
} else {
callKeepManager.setMutedCall(uuid, false);
callKeepManager.setOnHold(uuid, false);
}
break;
case CallState.kEnded:
callKeepManager.hangup(uuid);
break;
/* TODO:
case CallState.kMuted:
callKeepManager.setMutedCall(uuid, true);
break;
case CallState.kHeld:
callKeepManager.setOnHold(uuid, true);
break;
*/
case CallState.kFledgling:
// TODO: Handle this case.
break;
case CallState.kInviteSent:
// TODO: Handle this case.
break;
case CallState.kWaitLocalMedia:
// TODO: Handle this case.
break;
case CallState.kCreateOffer:
// TODO: Handle this case.
break;
case CallState.kCreateAnswer:
// TODO: Handle this case.
break;
case CallState.kRinging:
// TODO: Handle this case.
break;
}
}
}
class CallKeepManager {
factory CallKeepManager() {
return _instance;
}
CallKeepManager._internal() {
_callKeep = FlutterCallkeep();
}
static final CallKeepManager _instance = CallKeepManager._internal();
late FlutterCallkeep _callKeep;
VoipPlugin? _voipPlugin;
Map<String, CallKeeper> calls = <String, CallKeeper>{};
String newUUID() => const Uuid().v4();
String get appName => 'Famedly';
Map<String, dynamic> get alertOptions => <String, dynamic>{
'alertTitle': 'Permissions required',
'alertDescription': '$appName needs to access your phone accounts!',
'cancelButton': 'Cancel',
'okButton': 'ok',
// Required to get audio in background when using Android 11
'foregroundService': {
'channelId': 'com.famedly.talk',
'channelName': 'Foreground service for my app',
'notificationTitle': '$appName is running on background',
'notificationIcon': 'mipmap/ic_notification_launcher',
},
};
void setVoipPlugin(VoipPlugin plugin) {
if (kIsWeb) {
throw 'Not support callkeep for flutter web';
}
_voipPlugin = plugin;
_voipPlugin!.onIncomingCall = (CallSession call) async {
await _callKeep.setup(
null,
<String, dynamic>{
'ios': <String, dynamic>{
'appName': appName,
},
'android': alertOptions,
},
backgroundMode: true);
await displayIncomingCall(call);
call.onCallStateChanged.listen((state) {
if (state == CallState.kEnded) {
_callKeep.endAllCalls();
}
});
call.onCallEventChanged.listen((event) {
if (event == CallEvent.kLocalHoldUnhold) {
Logs().i(
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}');
}
});
};
}
void removeCall(String callUUID) {
calls.remove(callUUID);
}
void addCall(String callUUID, CallKeeper callKeeper) {
calls[callUUID] = callKeeper;
}
String findCallUUID(String number) {
var uuid = '';
calls.forEach((String key, CallKeeper item) {
if (item.number == number) {
uuid = key;
return;
}
});
return uuid;
}
void setCallHeld(String callUUID, bool held) {
calls[callUUID]!.held = held;
}
void setCallMuted(String callUUID, bool muted) {
calls[callUUID]!.muted = muted;
}
void didDisplayIncomingCall(CallKeepDidDisplayIncomingCall event) {
final callUUID = event.callUUID;
final number = event.handle;
Logs().v('[displayIncomingCall] $callUUID number: $number');
addCall(callUUID!, CallKeeper(this, callUUID, number!, null));
}
void onPushKitToken(CallKeepPushKitToken event) {
Logs().v('[onPushKitToken] token => ${event.token}');
}
Future<void> initialize() async {
_callKeep.on(CallKeepPerformAnswerCallAction(), answerCall);
_callKeep.on(CallKeepDidPerformDTMFAction(), didPerformDTMFAction);
_callKeep.on(
CallKeepDidReceiveStartCallAction(), didReceiveStartCallAction);
_callKeep.on(CallKeepDidToggleHoldAction(), didToggleHoldCallAction);
_callKeep.on(
CallKeepDidPerformSetMutedCallAction(), didPerformSetMutedCallAction);
_callKeep.on(CallKeepPerformEndCallAction(), endCall);
_callKeep.on(CallKeepPushKitToken(), onPushKitToken);
_callKeep.on(CallKeepDidDisplayIncomingCall(), didDisplayIncomingCall);
}
Future<void> hangup(String callUUID) async {
await _callKeep.endCall(callUUID);
removeCall(callUUID);
}
Future<void> reject(String callUUID) async {
await _callKeep.rejectCall(callUUID);
}
Future<void> answer(String callUUID) async {
final keeper = calls[callUUID];
if (!keeper!.connected) {
await _callKeep.answerIncomingCall(callUUID);
keeper.connected = true;
}
}
Future<void> setOnHold(String callUUID, bool held) async {
await _callKeep.setOnHold(callUUID, held);
setCallHeld(callUUID, held);
}
Future<void> setMutedCall(String callUUID, bool muted) async {
await _callKeep.setMutedCall(callUUID, muted);
setCallMuted(callUUID, muted);
}
Future<void> updateDisplay(String callUUID) async {
final number = calls[callUUID]!.number;
// Workaround because Android doesn't display well displayName, se we have to switch ...
if (isIOS) {
await _callKeep.updateDisplay(callUUID,
displayName: 'New Name', handle: number);
} else {
await _callKeep.updateDisplay(callUUID,
displayName: number, handle: 'New Name');
}
}
Future<CallKeeper> displayIncomingCall(CallSession call) async {
final callUUID = newUUID();
final callKeeper = CallKeeper(this, callUUID, call.displayName!, call);
addCall(callUUID, callKeeper);
await _callKeep.displayIncomingCall(callUUID, call.displayName!,
handleType: 'number', hasVideo: call.type == CallType.kVideo);
return callKeeper;
}
Future<void> checkoutPhoneAccountSetting(BuildContext context) async {
await _callKeep.setup(context, <String, dynamic>{
'ios': <String, dynamic>{
'appName': appName,
},
'android': alertOptions,
});
final hasPhoneAccount = await _callKeep.hasPhoneAccount();
if (!hasPhoneAccount) {
await _callKeep.hasDefaultPhoneAccount(context, alertOptions);
}
}
/// CallActions.
Future<void> answerCall(CallKeepPerformAnswerCallAction event) async {
final callUUID = event.callUUID;
final keeper = calls[event.callUUID]!;
if (!keeper.connected) {
// Answer Call
keeper.call!.answer();
keeper.connected = true;
}
Timer(const Duration(seconds: 1), () {
_callKeep.setCurrentCallActive(callUUID!);
});
}
Future<void> endCall(CallKeepPerformEndCallAction event) async {
final keeper = calls[event.callUUID];
keeper?.call?.hangup();
removeCall(event.callUUID!);
}
Future<void> didPerformDTMFAction(CallKeepDidPerformDTMFAction event) async {
final keeper = calls[event.callUUID]!;
keeper.call?.sendDTMF(event.digits!);
}
Future<void> didReceiveStartCallAction(
CallKeepDidReceiveStartCallAction event) async {
if (event.handle == null) {
// @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined`
return;
}
final callUUID = event.callUUID ?? newUUID();
if (event.callUUID == null) {
final call =
await _voipPlugin!.voip.inviteToCall(event.handle!, CallType.kVideo);
addCall(callUUID, CallKeeper(this, callUUID, call.displayName!, call));
}
await _callKeep.startCall(callUUID, event.handle!, event.handle!);
Timer(const Duration(seconds: 1), () {
_callKeep.setCurrentCallActive(callUUID);
});
}
Future<void> didPerformSetMutedCallAction(
CallKeepDidPerformSetMutedCallAction event) async {
final keeper = calls[event.callUUID]!;
if (event.muted ?? false) {
keeper.call?.setMicrophoneMuted(true);
} else {
keeper.call?.setMicrophoneMuted(false);
}
setCallMuted(event.callUUID!, event.muted!);
}
Future<void> didToggleHoldCallAction(
CallKeepDidToggleHoldAction event) async {
final keeper = calls[event.callUUID]!;
if (event.hold ?? false) {
keeper.call?.setRemoteOnHold(true);
} else {
keeper.call?.setRemoteOnHold(false);
}
setCallHeld(event.callUUID!, event.hold!);
}
}

View File

@ -0,0 +1,123 @@
import 'package:fluffychat/utils/voip/call_state_proxy.dart';
import 'package:matrix/matrix.dart';
class GroupCallSessionState implements CallStateProxy {
Function()? callback;
final GroupCall groupCall;
GroupCallSessionState(this.groupCall) {
groupCall.onGroupCallEvent.stream.listen((event) {
Logs().i("onGroupCallEvent ${event.toString()}");
callback?.call();
});
groupCall.onGroupCallState.stream.listen((event) {
Logs().i("onGroupCallState ${event.toString()}");
callback?.call();
});
}
@override
void answer() {
// TODO: implement answer
}
@override
bool get callOnHold => false;
@override
String get callState => 'New Group Call...';
@override
bool get connected => groupCall.state == GroupCallState.Entered;
@override
bool get connecting => groupCall.state == GroupCallState.Entering;
@override
String? get displayName => groupCall.displayName;
@override
bool get ended =>
groupCall.state == GroupCallState.Ended ||
groupCall.state == GroupCallState.LocalCallFeedUninitialized;
@override
void enter() async {
await groupCall.initLocalStream();
groupCall.enter();
}
@override
void hangup() async {
groupCall.leave();
}
@override
bool get isLocalVideoMuted => groupCall.isLocalVideoMuted;
@override
bool get isMicrophoneMuted => groupCall.isMicrophoneMuted;
@override
bool get isOutgoing => false;
@override
bool get isRemoteOnHold => false;
@override
bool get isScreensharingEnabled => groupCall.isScreensharing();
@override
bool get localHold => false;
@override
WrappedMediaStream? get localScreenSharingStream =>
groupCall.localScreenshareStream;
@override
WrappedMediaStream? get localUserMediaStream =>
groupCall.localUserMediaStream;
@override
void onStateChanged(Function() handler) {
callback = handler;
}
@override
WrappedMediaStream? get primaryStream => groupCall.localUserMediaStream;
@override
bool get remoteOnHold => false;
@override
bool get ringingPlay => false;
@override
List<WrappedMediaStream> get screenSharingStreams =>
groupCall.screenshareStreams;
@override
List<WrappedMediaStream> get userMediaStreams => groupCall.userMediaStreams;
@override
void setLocalVideoMuted(bool muted) {
groupCall.setLocalVideoMuted(muted);
}
@override
void setMicrophoneMuted(bool muted) {
groupCall.setMicrophoneMuted(muted);
}
@override
void setRemoteOnHold(bool onHold) {
// TODO: implement setRemoteOnHold
}
@override
void setScreensharingEnabled(bool enabled) {
groupCall.setScreensharingEnabled(enabled, '');
}
@override
bool get voiceonly => false;
}

View File

@ -4,11 +4,13 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package: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;
}
}
}

View File

@ -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) {

View File

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

View File

@ -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:

View File

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

View File

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