diff --git a/lib/components/chat_settings_popup_menu.dart b/lib/components/chat_settings_popup_menu.dart index b4f3573..81c9c21 100644 --- a/lib/components/chat_settings_popup_menu.dart +++ b/lib/components/chat_settings_popup_menu.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/utils/app_route.dart'; import 'package:fluffychat/views/chat_details.dart'; import 'package:fluffychat/views/chat_list.dart'; import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'dialogs/simple_dialogs.dart'; import 'matrix.dart'; @@ -29,6 +30,18 @@ class _ChatSettingsPopupMenuState extends State { super.dispose(); } + void startCallAction(BuildContext context) async { + final url = + '${Matrix.of(context).jitsiInstance}${Uri.encodeComponent(widget.room.id.localpart)}'; + final success = await Matrix.of(context) + .tryRequestWithLoadingDialog(widget.room.sendEvent({ + 'msgtype': Matrix.callNamespace, + 'body': url, + })); + if (success == false) return; + await launch(url); + } + @override Widget build(BuildContext context) { notificationChangeSub ??= Matrix.of(context) @@ -49,6 +62,10 @@ class _ChatSettingsPopupMenuState extends State { value: "unmute", child: Text(I18n.of(context).unmuteChat), ), + PopupMenuItem( + value: "call", + child: Text(I18n.of(context).videoCall), + ), PopupMenuItem( value: "leave", child: Text(I18n.of(context).leave), @@ -86,6 +103,9 @@ class _ChatSettingsPopupMenuState extends State { await Matrix.of(context).tryRequestWithLoadingDialog( widget.room.setPushRuleState(PushRuleState.notify)); break; + case "call": + startCallAction(context); + break; case "details": await Navigator.of(context).push( AppRoute.defaultRoute( diff --git a/lib/components/dialogs/simple_dialogs.dart b/lib/components/dialogs/simple_dialogs.dart index bc88597..36514be 100644 --- a/lib/components/dialogs/simple_dialogs.dart +++ b/lib/components/dialogs/simple_dialogs.dart @@ -26,6 +26,7 @@ class SimpleDialogs { content: TextField( controller: controller, autofocus: true, + autocorrect: false, onSubmitted: (s) { input = s; Navigator.of(context).pop(); diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 14f9804..77f4e57 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:localstorage/localstorage.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../i18n/i18n.dart'; import '../utils/app_route.dart'; @@ -18,8 +19,11 @@ import '../utils/event_extension.dart'; import '../utils/famedlysdk_store.dart'; import '../utils/room_extension.dart'; import '../views/chat.dart'; +import 'avatar.dart'; class Matrix extends StatefulWidget { + static const String callNamespace = 'chat.fluffy.jitsi_call'; + final Widget child; final String clientName; @@ -52,6 +56,8 @@ class MatrixState extends State { String activeRoomId; File wallpaper; + String jitsiInstance = 'https://meet.jit.si/'; + void clean() async { if (!kIsWeb) return; @@ -343,12 +349,73 @@ class MatrixState extends State { }; StreamSubscription onRoomKeyRequestSub; + StreamSubscription onJitsiCallSub; + + void onJitsiCall(EventUpdate eventUpdate) { + final event = Event.fromJson( + eventUpdate.content, client.getRoomById(eventUpdate.roomID)); + if (DateTime.now().millisecondsSinceEpoch - + event.time.millisecondsSinceEpoch > + 1000 * 60 * 5) { + return; + } + final senderName = event.sender.calcDisplayname(); + final senderAvatar = event.sender.avatarUrl; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: ListTile( + contentPadding: EdgeInsets.all(0), + leading: Avatar(senderAvatar, senderName), + title: Text( + senderName, + style: TextStyle(fontSize: 18), + ), + subtitle: + event.room.isDirectChat ? null : Text(event.room.displayname), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Divider(), + Row( + children: [ + Spacer(), + FloatingActionButton( + backgroundColor: Colors.red, + child: Icon(Icons.phone_missed), + onPressed: () => Navigator.of(context).pop(), + ), + Spacer(), + FloatingActionButton( + backgroundColor: Colors.green, + child: Icon(Icons.phone), + onPressed: () { + Navigator.of(context).pop(); + launch(event.body); + }, + ), + Spacer(), + ], + ), + ], + ), + ), + ); + return; + } @override void initState() { if (widget.client == null) { debugPrint("[Matrix] Init matrix client"); client = Client(widget.clientName, debug: false); + onJitsiCallSub ??= client.onEvent.stream + .where((e) => + e.eventType == 'm.room.message' && + e.content['content']['msgtype'] == Matrix.callNamespace && + e.content['sender'] != client.userID) + .listen(onJitsiCall); onRoomKeyRequestSub ??= client.onRoomKeyRequest.stream.listen((RoomKeyRequest request) async { final Room room = request.room; @@ -368,6 +435,9 @@ class MatrixState extends State { client = widget.client; } if (client.storeAPI != null) { + client.storeAPI + .getItem("chat.fluffy.jitsi_instance") + .then((final instance) => jitsiInstance = instance ?? jitsiInstance); client.storeAPI.getItem("chat.fluffy.wallpaper").then((final path) async { if (path == null) return; final file = File(path); @@ -382,6 +452,7 @@ class MatrixState extends State { @override void dispose() { onRoomKeyRequestSub?.cancel(); + onJitsiCallSub?.cancel(); super.dispose(); } diff --git a/lib/components/message_content.dart b/lib/components/message_content.dart index 2165f65..b17206c 100644 --- a/lib/components/message_content.dart +++ b/lib/components/message_content.dart @@ -99,6 +99,19 @@ class MessageContent extends StatelessWidget { case MessageTypes.Notice: case MessageTypes.Emote: default: + if (event.content['msgtype'] == Matrix.callNamespace) { + return RaisedButton( + color: Theme.of(context).backgroundColor, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.phone), + Text(I18n.of(context).videoCall), + ], + ), + onPressed: () => launch(event.body), + ); + } return LinkText( text: event.getLocalizedBody(context, hideReply: true), textStyle: TextStyle( diff --git a/lib/i18n/i18n.dart b/lib/i18n/i18n.dart index dd3b9fd..b15dbfb 100644 --- a/lib/i18n/i18n.dart +++ b/lib/i18n/i18n.dart @@ -371,6 +371,8 @@ class I18n { String get isTyping => Intl.message("is typing..."); + String get editJitsiInstance => Intl.message('Edit Jitsi instance'); + String joinedTheChat(String username) => Intl.message( "$username joined the chat", name: "joinedTheChat", @@ -714,6 +716,8 @@ class I18n { String get verify => Intl.message("Verify"); + String get videoCall => Intl.message('Video call'); + String get visibleForAllParticipants => Intl.message("Visible for all participants"); diff --git a/lib/views/settings.dart b/lib/views/settings.dart index b242cca..31d3aa7 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -49,6 +49,21 @@ class _SettingsState extends State { AppRoute.defaultRoute(context, SignUp()), (r) => false); } + void setJitsiInstanceAction(BuildContext context) async { + var jitsi = await SimpleDialogs(context).enterText( + titleText: I18n.of(context).editJitsiInstance, + hintText: Matrix.of(context).jitsiInstance, + labelText: I18n.of(context).editJitsiInstance, + ); + if (jitsi == null) return; + if (!jitsi.endsWith('/')) { + jitsi += '/'; + } + final MatrixState matrix = Matrix.of(context); + await matrix.client.storeAPI.setItem('chat.fluffy.jitsi_instance', jitsi); + matrix.jitsiInstance = jitsi; + } + void setDisplaynameAction(BuildContext context) async { final String displayname = await SimpleDialogs(context).enterText( titleText: I18n.of(context).editDisplayname, @@ -205,6 +220,12 @@ class _SettingsState extends State { subtitle: Text(profile?.displayname ?? client.userID.localpart), onTap: () => setDisplaynameAction(context), ), + ListTile( + trailing: Icon(Icons.phone), + title: Text(I18n.of(context).editJitsiInstance), + subtitle: Text(Matrix.of(context).jitsiInstance), + onTap: () => setJitsiInstanceAction(context), + ), ListTile( trailing: Icon(Icons.devices_other), title: Text(I18n.of(context).devices), diff --git a/pubspec.lock b/pubspec.lock index 6f30b4b..22b82f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,28 +21,28 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "2.0.11" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "1.5.2" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.4.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "1.0.5" bubble: dependency: "direct main" description: @@ -63,14 +63,14 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.1.2" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.12" + version: "1.14.11" convert: dependency: transitive description: @@ -91,7 +91,7 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.3" csslib: dependency: transitive description: @@ -260,7 +260,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.1.12" + version: "2.1.4" image_picker: dependency: "direct main" description: @@ -274,7 +274,7 @@ packages: name: intl url: "https://pub.dartlang.org" source: hosted - version: "0.16.1" + version: "0.16.0" intl_translation: dependency: "direct main" description: @@ -502,7 +502,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.0.5" receive_sharing_intent: dependency: "direct main" description: @@ -570,7 +570,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.5.5" sqflite: dependency: "direct main" description: @@ -619,21 +619,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.13.0" + version: "1.9.4" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.2.11" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.2.15" typed_data: dependency: transitive description: @@ -718,13 +718,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.0+1" webview_flutter: dependency: "direct main" description: @@ -738,7 +731,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.6.1" + version: "3.5.0" yaml: dependency: transitive description: