diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2ba109a..040e0fd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,14 +1,95 @@ PODS: - file_picker (0.0.1): - Flutter + - Firebase/Core (6.14.0): + - Firebase/CoreOnly + - FirebaseAnalytics (= 6.1.7) + - Firebase/CoreOnly (6.14.0): + - FirebaseCore (= 6.5.0) + - Firebase/Messaging (6.14.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 4.1.10) + - firebase_messaging (0.0.1): + - Firebase/Core + - Firebase/Messaging + - Flutter + - FirebaseAnalytics (6.1.7): + - FirebaseCore (~> 6.5) + - FirebaseInstanceID (~> 4.2) + - GoogleAppMeasurement (= 6.1.7) + - GoogleUtilities/AppDelegateSwizzler (~> 6.0) + - GoogleUtilities/MethodSwizzler (~> 6.0) + - GoogleUtilities/Network (~> 6.0) + - "GoogleUtilities/NSData+zlib (~> 6.0)" + - nanopb (= 0.3.9011) + - FirebaseAnalyticsInterop (1.4.0) + - FirebaseCore (6.5.0): + - FirebaseCoreDiagnostics (~> 1.0) + - FirebaseCoreDiagnosticsInterop (~> 1.0) + - GoogleUtilities/Environment (~> 6.4) + - GoogleUtilities/Logger (~> 6.4) + - FirebaseCoreDiagnostics (1.1.2): + - FirebaseCoreDiagnosticsInterop (~> 1.0) + - GoogleDataTransportCCTSupport (~> 1.0) + - GoogleUtilities/Environment (~> 6.2) + - GoogleUtilities/Logger (~> 6.2) + - nanopb (~> 0.3.901) + - FirebaseCoreDiagnosticsInterop (1.1.0) + - FirebaseInstanceID (4.2.8): + - FirebaseCore (~> 6.5) + - GoogleUtilities/Environment (~> 6.4) + - GoogleUtilities/UserDefaults (~> 6.4) + - FirebaseMessaging (4.1.10): + - FirebaseAnalyticsInterop (~> 1.3) + - FirebaseCore (~> 6.2) + - FirebaseInstanceID (~> 4.1) + - GoogleUtilities/AppDelegateSwizzler (~> 6.2) + - GoogleUtilities/Environment (~> 6.2) + - GoogleUtilities/Reachability (~> 6.2) + - GoogleUtilities/UserDefaults (~> 6.2) + - Protobuf (>= 3.9.2, ~> 3.9) - Flutter (1.0.0) - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) + - GoogleAppMeasurement (6.1.7): + - GoogleUtilities/AppDelegateSwizzler (~> 6.0) + - GoogleUtilities/MethodSwizzler (~> 6.0) + - GoogleUtilities/Network (~> 6.0) + - "GoogleUtilities/NSData+zlib (~> 6.0)" + - nanopb (= 0.3.9011) + - GoogleDataTransport (3.2.0) + - GoogleDataTransportCCTSupport (1.2.3): + - GoogleDataTransport (~> 3.2) + - nanopb (~> 0.3.901) + - GoogleUtilities/AppDelegateSwizzler (6.4.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (6.4.0) + - GoogleUtilities/Logger (6.4.0): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (6.4.0): + - GoogleUtilities/Logger + - GoogleUtilities/Network (6.4.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (6.4.0)" + - GoogleUtilities/Reachability (6.4.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (6.4.0): + - GoogleUtilities/Logger - image_picker (0.0.1): - Flutter + - nanopb (0.3.9011): + - nanopb/decode (= 0.3.9011) + - nanopb/encode (= 0.3.9011) + - nanopb/decode (0.3.9011) + - nanopb/encode (0.3.9011) - path_provider (0.0.1): - Flutter + - Protobuf (3.11.2) - sqflite (0.0.1): - Flutter - FMDB (~> 2.7.2) @@ -21,6 +102,7 @@ PODS: DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) + - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) @@ -31,11 +113,27 @@ DEPENDENCIES: SPEC REPOS: trunk: + - Firebase + - FirebaseAnalytics + - FirebaseAnalyticsInterop + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseCoreDiagnosticsInterop + - FirebaseInstanceID + - FirebaseMessaging - FMDB + - GoogleAppMeasurement + - GoogleDataTransport + - GoogleDataTransportCCTSupport + - GoogleUtilities + - nanopb + - Protobuf EXTERNAL SOURCES: file_picker: :path: ".symlinks/plugins/file_picker/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" Flutter: :path: Flutter image_picker: @@ -53,10 +151,25 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: file_picker: 408623be2125b79a4539cf703be3d4b3abe5e245 + Firebase: 0219bb4782eb1406f1b9b0628a2e625484ce910d + firebase_messaging: 73b3e7dd7b3b6a7e4bdac10d5295211ca4f87f90 + FirebaseAnalytics: f68b9f3f1241385129ae0a83b63627fc420c05e5 + FirebaseAnalyticsInterop: d48b6ab67bcf016a05e55b71fc39c61c0cb6b7f3 + FirebaseCore: 632e05cc5e1199d9147122c16d92305eb04c34bd + FirebaseCoreDiagnostics: 511f4f3ed7d440bb69127e8b97c2bc8befae639e + FirebaseCoreDiagnosticsInterop: e9b1b023157e3a2fc6418b5cb601e79b9af7b3a0 + FirebaseInstanceID: ce993a3c3670a8f5d47ce371ac5d143c560608c5 + FirebaseMessaging: 089b7a4991425783384acc8bcefcd78c0af913bd Flutter: 0e3d915762c693b495b44d77113d4970485de6ec FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + GoogleAppMeasurement: db118eb61a97dd8c4f7014e368d3c335cbbcf80a + GoogleDataTransport: 8e9b210c97d55fbff306cc5468ff91b9cb32dcf5 + GoogleDataTransportCCTSupport: 202d7cdf9c4a7d81a2bb7f7e7e1ba6faa421b1f2 + GoogleUtilities: 29bd0d8f850efbd28cff6d99e8b7da1f8d236bcf image_picker: e3eacd46b94694dde7cf2705955cece853aa1a8f + nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d + Protobuf: dd1aaea7140debfe4dd0683fb8ef208e527ae153 sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0 url_launcher: a1c0cc845906122c4784c542523d8cacbded5626 url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313 diff --git a/lib/components/dialogs/redact_message_dialog.dart b/lib/components/dialogs/redact_message_dialog.dart index c1c5ee0..2d39821 100644 --- a/lib/components/dialogs/redact_message_dialog.dart +++ b/lib/components/dialogs/redact_message_dialog.dart @@ -23,7 +23,10 @@ class RedactMessageDialog extends StatelessWidget { onPressed: () => Navigator.of(context).pop(), ), FlatButton( - child: Text("Remove".toUpperCase()), + child: Text( + "Remove".toUpperCase(), + style: TextStyle(color: Colors.red), + ), onPressed: () => removeAction(context), ), ], diff --git a/lib/components/list_items/chat_list_item.dart b/lib/components/list_items/chat_list_item.dart index 9c2bc9e..34adfe9 100644 --- a/lib/components/list_items/chat_list_item.dart +++ b/lib/components/list_items/chat_list_item.dart @@ -4,14 +4,76 @@ import 'package:fluffychat/utils/chat_time.dart'; import 'package:fluffychat/utils/app_route.dart'; import 'package:fluffychat/views/chat.dart'; import 'package:flutter/material.dart'; +import 'package:toast/toast.dart'; import '../avatar.dart'; +import '../matrix.dart'; class ChatListItem extends StatelessWidget { final Room room; final bool activeChat; + final Function onForget; - const ChatListItem(this.room, {this.activeChat = false}); + const ChatListItem(this.room, {this.activeChat = false, this.onForget}); + + void clickAction(BuildContext context) async { + if (!activeChat) { + if (room.membership == Membership.invite && + await Matrix.of(context).tryRequestWithLoadingDialog(room.join()) == + false) { + return; + } + + if (room.membership == Membership.ban) { + Toast.show("You have been banned from this chat", context, duration: 5); + return; + } + + if (room.membership == Membership.leave) { + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text("Archived Room"), + content: Text("This room has been archived."), + actions: [ + FlatButton( + child: Text("Close".toUpperCase(), + style: TextStyle(color: Colors.blueGrey)), + onPressed: () => Navigator.of(context).pop(), + ), + FlatButton( + child: Text("Forget".toUpperCase(), + style: TextStyle(color: Colors.red)), + onPressed: () async { + await Matrix.of(context) + .tryRequestWithLoadingDialog(room.forget()); + await Navigator.of(context).pop(); + if (this.onForget != null) this.onForget(); + }, + ), + FlatButton( + child: Text("Rejoin".toUpperCase(), + style: TextStyle(color: Colors.blue)), + onPressed: () async { + await Matrix.of(context) + .tryRequestWithLoadingDialog(room.join()); + await Navigator.of(context).pop(); + }, + ), + ], + ), + ); + } + + if (room.membership == Membership.join) { + await Navigator.pushAndRemoveUntil( + context, + AppRoute.defaultRoute(context, Chat(room.id)), + (r) => r.isFirst, + ); + } + } + } @override Widget build(BuildContext context) { @@ -41,7 +103,19 @@ class ChatListItem extends StatelessWidget { subtitle: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded(child: MessageContent(room.lastEvent, textOnly: true)), + Expanded( + child: room.membership == Membership.invite + ? Text( + "You are invited to this chat", + style: TextStyle( + color: Theme.of(context).primaryColor, + ), + ) + : MessageContent( + room.lastEvent, + textOnly: true, + ), + ), SizedBox(width: 8), room.pushRuleState == PushRuleState.notify ? Container() @@ -70,48 +144,7 @@ class ChatListItem extends StatelessWidget { : Text(" "), ], ), - onTap: () { - if (activeChat) { - Navigator.pushReplacement( - context, - AppRoute.defaultRoute(context, Chat(room.id)), - ); - } else { - Navigator.push( - context, - AppRoute.defaultRoute(context, Chat(room.id)), - ); - } - }, - onLongPress: () {}, - /*trailing: Container( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text(ChatTime(room.timeCreated).toEventTimeString()), - room.notificationCount > 0 - ? Container( - width: 20, - height: 20, - margin: EdgeInsets.only(top: 3), - decoration: BoxDecoration( - color: room.highlightCount > 0 - ? Colors.red - : Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(20), - ), - child: Center( - child: Text( - room.notificationCount.toString(), - style: TextStyle(color: Colors.white), - ), - ), - ) - : Text(" "), - ], - ), - ),*/ + onTap: () => clickAction(context), ), ); } diff --git a/lib/components/list_items/message.dart b/lib/components/list_items/message.dart index 21b61e8..b13bbce 100644 --- a/lib/components/list_items/message.dart +++ b/lib/components/list_items/message.dart @@ -103,7 +103,9 @@ class Message extends StatelessWidget { SizedBox(width: 4), Text( ChatTime(event.time).toEventTimeString(), - style: TextStyle(color: textColor, fontSize: 12), + style: TextStyle( + color: textColor.withAlpha(200), + ), ), ], ), diff --git a/lib/components/list_items/state_message.dart b/lib/components/list_items/state_message.dart index 29963e2..9a1e933 100644 --- a/lib/components/list_items/state_message.dart +++ b/lib/components/list_items/state_message.dart @@ -10,6 +10,7 @@ class StateMessage extends StatelessWidget { @override Widget build(BuildContext context) { + if (event.type == EventTypes.Redaction) return Container(); return Padding( padding: const EdgeInsets.all(8.0), child: Opacity( diff --git a/lib/components/message_content.dart b/lib/components/message_content.dart index 6442ede..873791c 100644 --- a/lib/components/message_content.dart +++ b/lib/components/message_content.dart @@ -36,7 +36,7 @@ class MessageContent extends StatelessWidget { case MessageTypes.Sticker: if (textOnly) { return Text( - "${event.sender.calcDisplayname()} has sent an image", + "${event.sender.calcDisplayname()} sent a picture", maxLines: maxLines, style: TextStyle( color: textColor, @@ -75,6 +75,17 @@ class MessageContent extends StatelessWidget { case MessageTypes.Audio: case MessageTypes.File: case MessageTypes.Video: + if (textOnly) { + return Text( + "${event.sender.calcDisplayname()} sent a file", + maxLines: maxLines, + style: TextStyle( + color: textColor, + decoration: + event.redacted ? TextDecoration.lineThrough : null, + ), + ); + } return Container( width: 200, child: RaisedButton( diff --git a/lib/utils/sqflite_store.dart b/lib/utils/sqflite_store.dart index bb96562..c3da0aa 100644 --- a/lib/utils/sqflite_store.dart +++ b/lib/utils/sqflite_store.dart @@ -49,7 +49,7 @@ class Store extends StoreAPI { _init() async { var databasePath = await getDatabasesPath(); String path = p.join(databasePath, "FluffyMatrix.db"); - _db = await openDatabase(path, version: 14, + _db = await openDatabase(path, version: 15, onCreate: (Database db, int version) async { await createTables(db); }, onUpgrade: (Database db, int oldVersion, int newVersion) async { @@ -311,7 +311,7 @@ class Store extends StoreAPI { if (type == "history") return null; - if (eventUpdate.content["event_id"] != null || + if (type != "account_data" && eventUpdate.content["event_id"] != null || eventUpdate.content["state_key"] != null) { final String now = DateTime.now().millisecondsSinceEpoch.toString(); txn.rawInsert( @@ -578,7 +578,7 @@ class Store extends StoreAPI { /// The database scheme for room states. 'RoomAccountData': 'CREATE TABLE IF NOT EXISTS RoomAccountData(' + - 'type TEXT PRIMARY KEY, ' + + 'type TEXT, ' + 'room_id TEXT, ' + 'content TEXT, ' + 'UNIQUE(type,room_id))', diff --git a/lib/views/archive.dart b/lib/views/archive.dart new file mode 100644 index 0000000..b09ed0b --- /dev/null +++ b/lib/views/archive.dart @@ -0,0 +1,52 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/list_items/chat_list_item.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:flutter/material.dart'; + +class Archive extends StatefulWidget { + @override + _ArchiveState createState() => _ArchiveState(); +} + +class _ArchiveState extends State { + List archive; + + Future> getArchive(BuildContext context) async { + if (archive != null) return archive; + return await Matrix.of(context).client.archive; + } + + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + firstScaffold: Scaffold( + appBar: AppBar( + title: Text("Archive"), + ), + body: FutureBuilder>( + future: getArchive(context), + builder: (BuildContext context, snapshot) { + if (!snapshot.hasData) { + return Center(child: CircularProgressIndicator()); + } else { + archive = snapshot.data; + return ListView.builder( + itemCount: archive.length, + itemBuilder: (BuildContext context, int i) => ChatListItem( + archive[i], + onForget: () => setState(() => archive = null)), + ); + } + }, + ), + ), + secondScaffold: Scaffold( + body: Center( + child: Image.asset("assets/logo.png", width: 100, height: 100), + ), + ), + primaryPage: FocusPage.FIRST, + ); + } +} diff --git a/lib/views/chat.dart b/lib/views/chat.dart index ad57958..f53ce99 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:famedlysdk/famedlysdk.dart'; @@ -26,8 +27,14 @@ class _ChatState extends State { Timeline timeline; + String seenByText = ""; + final ScrollController _scrollController = ScrollController(); + Timer typingCoolDown; + Timer typingTimeout; + bool currentlyTyping = false; + @override void initState() { _scrollController.addListener(() async { @@ -43,10 +50,31 @@ class _ChatState extends State { super.initState(); } - Future getTimeline() async { - timeline ??= await room.getTimeline(onUpdate: () { - setState(() {}); + void updateView() { + String seenByText = ""; + if (timeline.events.isNotEmpty) { + List lastReceipts = List.from(timeline.events.first.receipts); + lastReceipts.removeWhere((r) => + r.user.id == room.client.userID || + r.user.id == timeline.events.first.senderId); + if (lastReceipts.length == 1) { + seenByText = "Seen by ${lastReceipts.first.user.calcDisplayname()}"; + } else if (lastReceipts.length == 2) { + seenByText = + "Seen by ${lastReceipts.first.user.calcDisplayname()} and ${lastReceipts[1].user.calcDisplayname()}"; + } else if (lastReceipts.length > 2) { + seenByText = + "Seen by ${lastReceipts.first.user.calcDisplayname()} and ${lastReceipts.length - 1} others"; + } + } + setState(() { + this.seenByText = seenByText; }); + } + + Future getTimeline() async { + timeline ??= await room.getTimeline(onUpdate: updateView); + updateView(); return true; } @@ -115,8 +143,29 @@ class _ChatState extends State { Widget build(BuildContext context) { Client client = Matrix.of(context).client; room ??= client.getRoomById(widget.id); + if (room == null) { + return Center( + child: Text("You are no longer participating in this chat"), + ); + } - if (room.membership == Membership.invite) room.join(); + if (room.membership == Membership.invite) { + Matrix.of(context).tryRequestWithLoadingDialog(room.join()); + } + + String typingText = ""; + List typingUsers = room.typingUsers; + typingUsers.removeWhere((User u) => u.id == client.userID); + + if (typingUsers.length == 1) { + typingText = "${typingUsers.first.calcDisplayname()} is typing..."; + } else if (typingUsers.length == 2) { + typingText = + "${typingUsers.first.calcDisplayname()} and ${typingUsers[1].calcDisplayname()} are typing..."; + } else if (typingUsers.length > 2) { + typingText = + "${typingUsers.first.calcDisplayname()} and ${typingUsers.length - 1} others are typing..."; + } return AdaptivePageLayout( primaryPage: FocusPage.SECOND, @@ -125,7 +174,33 @@ class _ChatState extends State { ), secondScaffold: Scaffold( appBar: AppBar( - title: Text(room.displayname), + title: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(room.displayname), + AnimatedContainer( + duration: Duration(milliseconds: 500), + height: typingText.isEmpty ? 0 : 20, + child: Row( + children: [ + typingText.isEmpty + ? Container() + : Icon(Icons.edit, + color: Theme.of(context).primaryColor, size: 10), + SizedBox(width: 4), + Text( + typingText, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ), + ], + ), actions: [ChatSettingsPopupMenu(room, !room.isDirectChat)], ), body: SafeArea( @@ -140,6 +215,7 @@ class _ChatState extends State { child: CircularProgressIndicator(), ); } + if (room.notificationCount != null && room.notificationCount > 0 && timeline != null && @@ -148,92 +224,138 @@ class _ChatState extends State { } return ListView.builder( reverse: true, - itemCount: timeline.events.length, + itemCount: timeline.events.length + 1, controller: _scrollController, - itemBuilder: (BuildContext context, int i) => - Message(timeline.events[i]), + itemBuilder: (BuildContext context, int i) => i == 0 + ? AnimatedContainer( + height: seenByText.isEmpty ? 0 : 36, + duration: seenByText.isEmpty + ? Duration(milliseconds: 0) + : Duration(milliseconds: 500), + alignment: timeline.events.first.senderId == + client.userID + ? Alignment.centerRight + : Alignment.centerLeft, + child: Text( + seenByText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).primaryColor, + ), + ), + padding: EdgeInsets.all(8), + ) + : Message(timeline.events[i - 1]), ); }, ), ), - Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - spreadRadius: 1, - blurRadius: 2, - offset: Offset(0, -1), // changes position of shadow - ), - ], - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - kIsWeb - ? Container() - : PopupMenuButton( - icon: Icon(Icons.add), - onSelected: (String choice) async { - if (choice == "file") { - sendFileAction(context); - } else if (choice == "image") { - sendImageAction(context); - } - if (choice == "camera") openCameraAction(context); - }, - itemBuilder: (BuildContext context) => - >[ - const PopupMenuItem( - value: "file", - child: ListTile( - leading: Icon(Icons.attach_file), - title: Text('Send file'), - contentPadding: EdgeInsets.all(0), - ), - ), - const PopupMenuItem( - value: "image", - child: ListTile( - leading: Icon(Icons.image), - title: Text('Send image'), - contentPadding: EdgeInsets.all(0), - ), - ), - const PopupMenuItem( - value: "camera", - child: ListTile( - leading: Icon(Icons.camera), - title: Text('Open camera'), - contentPadding: EdgeInsets.all(0), - ), - ), - ], + room.canSendDefaultMessages && room.membership == Membership.join + ? Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 2, + offset: Offset(0, -1), // changes position of shadow ), - SizedBox(width: 8), - Expanded( - child: TextField( - minLines: 1, - maxLines: kIsWeb ? 1 : null, - keyboardType: - kIsWeb ? TextInputType.text : TextInputType.multiline, - onSubmitted: (t) => send(), - controller: sendController, - decoration: InputDecoration( - labelText: "Write a message...", - hintText: "You're message", - border: InputBorder.none, + ], ), - )), - SizedBox(width: 8), - IconButton( - icon: Icon(Icons.send), - onPressed: () => send(), - ), - ], - ), - ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + kIsWeb + ? Container() + : PopupMenuButton( + icon: Icon(Icons.add), + onSelected: (String choice) async { + if (choice == "file") { + sendFileAction(context); + } else if (choice == "image") { + sendImageAction(context); + } + if (choice == "camera") { + openCameraAction(context); + } + }, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: "file", + child: ListTile( + leading: Icon(Icons.attach_file), + title: Text('Send file'), + contentPadding: EdgeInsets.all(0), + ), + ), + const PopupMenuItem( + value: "image", + child: ListTile( + leading: Icon(Icons.image), + title: Text('Send image'), + contentPadding: EdgeInsets.all(0), + ), + ), + const PopupMenuItem( + value: "camera", + child: ListTile( + leading: Icon(Icons.camera), + title: Text('Open camera'), + contentPadding: EdgeInsets.all(0), + ), + ), + ], + ), + SizedBox(width: 8), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TextField( + minLines: 1, + maxLines: kIsWeb ? 1 : null, + keyboardType: kIsWeb + ? TextInputType.text + : TextInputType.multiline, + onSubmitted: (t) => send(), + controller: sendController, + decoration: InputDecoration( + hintText: "Write a message...", + border: InputBorder.none, + ), + onChanged: (String text) { + this.typingCoolDown?.cancel(); + this.typingCoolDown = + Timer(Duration(seconds: 2), () { + this.typingCoolDown = null; + this.currentlyTyping = false; + room.sendTypingInfo(false); + }); + this.typingTimeout ??= + Timer(Duration(seconds: 30), () { + this.typingTimeout = null; + this.currentlyTyping = false; + }); + if (!this.currentlyTyping) { + this.currentlyTyping = true; + room.sendTypingInfo(true, + timeout: + Duration(seconds: 30).inMilliseconds); + } + }, + ), + )), + SizedBox(width: 8), + IconButton( + icon: Icon(Icons.send), + onPressed: () => send(), + ), + ], + ), + ) + : Container(), ], ), ), diff --git a/lib/views/chat_details.dart b/lib/views/chat_details.dart index 8236843..c729b27 100644 --- a/lib/views/chat_details.dart +++ b/lib/views/chat_details.dart @@ -81,6 +81,11 @@ class _ChatDetailsState extends State { @override Widget build(BuildContext context) { + if (widget.room == null) { + return Center( + child: Text("You are no longer participating in this chat"), + ); + } members ??= widget.room.getParticipants(); final int actualMembersCount = widget.room.mInvitedMemberCount + widget.room.mJoinedMemberCount; @@ -127,28 +132,17 @@ class _ChatDetailsState extends State { ), ) : Container(), - Row( - children: [ - Expanded( - child: Container( - height: 8, - color: Theme.of(context).secondaryHeaderColor), + ListTile( + title: Text( + "$actualMembersCount participant" + + (actualMembersCount > 1 ? "s:" : ":"), + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, ), - SizedBox(width: 8), - Text( - "$actualMembersCount participant(s)", - style: TextStyle( - fontSize: 15, - ), - ), - SizedBox(width: 8), - Expanded( - child: Container( - height: 8, - color: Theme.of(context).secondaryHeaderColor), - ), - ], + ), ), + Divider(height: 1), widget.room.canInvite ? ListTile( title: Text("Invite contact"), diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart index 0bb7333..0645e68 100644 --- a/lib/views/chat_list.dart +++ b/lib/views/chat_list.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/components/dialogs/new_private_chat_dialog.dart'; import 'package:fluffychat/components/list_items/chat_list_item.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/views/archive.dart'; import 'package:fluffychat/views/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; @@ -100,16 +101,31 @@ class _ChatListState extends State { onSelected: (String choice) { switch (choice) { case "settings": - Navigator.of(context).push( + Navigator.of(context).pushAndRemoveUntil( AppRoute.defaultRoute( context, SettingsView(), ), + (r) => r.isFirst, ); + break; + case "archive": + Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute( + context, + Archive(), + ), + (r) => r.isFirst, + ); + break; } }, itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: "archive", + child: Text('Archive'), + ), const PopupMenuItem( value: "settings", child: Text('Settings'), diff --git a/pubspec.lock b/pubspec.lock index eea0e62..586a698 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -82,8 +82,8 @@ packages: dependency: "direct main" description: path: "." - ref: "90a06ebce547ed853e501532a03491356a93e483" - resolved-ref: "90a06ebce547ed853e501532a03491356a93e483" + ref: "3fbb837f85dc40e741677823f323ad8c3eac6014" + resolved-ref: "3fbb837f85dc40e741677823f323ad8c3eac6014" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index cec91ec..c1ff3fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: ae1c757e4ec3e7a41a2471e471d7ae47d974e821 + ref: 3fbb837f85dc40e741677823f323ad8c3eac6014 localstorage: ^3.0.1+4 bubble: ^1.1.9+1