diff --git a/CHANGELOG.md b/CHANGELOG.md index b08c2c2..12674e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Version 0.7.0 - 2020-02-?? +### New features +- Select mode in chat +- Implement replies + # Version 0.6.0 - 2020-02-09 ### New features - Add e2ee settings diff --git a/lib/components/list_items/message.dart b/lib/components/list_items/message.dart index bb81d02..849a53e 100644 --- a/lib/components/list_items/message.dart +++ b/lib/components/list_items/message.dart @@ -17,8 +17,12 @@ import 'state_message.dart'; class Message extends StatelessWidget { final Event event; final Event nextEvent; + final Function(Event) onSelect; + final bool longPressSelect; + final bool selected; - const Message(this.event, {this.nextEvent}); + const Message(this.event, + {this.nextEvent, this.longPressSelect, this.onSelect, this.selected}); @override Widget build(BuildContext context) { @@ -100,40 +104,12 @@ class Message extends StatelessWidget { List rowChildren = [ Expanded( - child: PopupMenuButton( - tooltip: I18n.of(context).tapToShowMenu, - onSelected: (String choice) async { - switch (choice) { - case "remove": - await showDialog( - context: context, - builder: (BuildContext context) => ConfirmDialog( - I18n.of(context).messageWillBeRemovedWarning, - I18n.of(context).remove, (context) { - Matrix.of(context) - .tryRequestWithLoadingDialog(event.redact()); - }), - ); - break; - case "resend": - await event.sendAgain(); - break; - case "delete": - await event.remove(); - break; - case "copy": - await Clipboard.setData(ClipboardData(text: event.body)); - break; - case "forward": - Matrix.of(context).shareContent = event.content; - Navigator.of(context).popUntil((r) => r.isFirst); - break; - } - }, - itemBuilder: (BuildContext context) => popupMenuList, + child: InkWell( + onTap: longPressSelect ? null : () => onSelect(event), + onLongPress: !longPressSelect ? null : () => onSelect(event), child: AnimatedOpacity( duration: Duration(milliseconds: 500), - opacity: event.status == 0 ? 0.5 : 1, + opacity: (event.status == 0 || event.redacted) ? 0.5 : 1, child: Bubble( elevation: 0, radius: Radius.circular(8), @@ -197,7 +173,12 @@ class Message extends StatelessWidget { rowChildren.insert(0, avatarOrSizedBox); } - return Padding( + return AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + color: selected + ? Theme.of(context).primaryColor.withAlpha(100) + : Theme.of(context).backgroundColor, padding: EdgeInsets.only( left: 8.0, right: 8.0, bottom: sameSender ? 4.0 : 8.0), child: Row( diff --git a/lib/i18n/i18n.dart b/lib/i18n/i18n.dart index b217169..23b29f2 100644 --- a/lib/i18n/i18n.dart +++ b/lib/i18n/i18n.dart @@ -413,6 +413,9 @@ class I18n { String get notSupportedInWeb => Intl.message("Not supported in web"); + String numberSelected(String number) => + Intl.message("$number selected", name: "numberSelected", args: [number]); + String get oopsSomethingWentWrong => Intl.message("Oops something went wrong..."); @@ -470,6 +473,8 @@ class I18n { String get removeMessage => Intl.message('Remove message'); + String get reply => Intl.message('Reply'); + String get saturday => Intl.message("Saturday"); String get share => Intl.message("Share"); diff --git a/lib/i18n/intl_de.arb b/lib/i18n/intl_de.arb index 78b5f9c..10e8dbf 100644 --- a/lib/i18n/intl_de.arb +++ b/lib/i18n/intl_de.arb @@ -629,6 +629,13 @@ "type": "text", "placeholders": {} }, + "numberSelected": "{number} ausgewählt", + "@numberSelected": { + "type": "text", + "placeholders": { + "number": {} + } + }, "Oops something went wrong...": "Hoppla! Da ist etwas schief gelaufen ...", "@Oops something went wrong...": { "type": "text", @@ -727,6 +734,11 @@ "type": "text", "placeholders": {} }, + "Reply": "Antworten", + "@Reply": { + "type": "text", + "placeholders": {} + }, "Saturday": "Samstag", "@Saturday": { "type": "text", diff --git a/lib/i18n/intl_messages.arb b/lib/i18n/intl_messages.arb index 27c6328..36a3ed8 100644 --- a/lib/i18n/intl_messages.arb +++ b/lib/i18n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2020-02-04T14:02:33.828211", + "@@last_modified": "2020-02-09T15:11:56.012667", "About": "About", "@About": { "type": "text", @@ -629,6 +629,13 @@ "type": "text", "placeholders": {} }, + "numberSelected": "{number} selected", + "@numberSelected": { + "type": "text", + "placeholders": { + "number": {} + } + }, "Oops something went wrong...": "Oops something went wrong...", "@Oops something went wrong...": { "type": "text", @@ -727,6 +734,11 @@ "type": "text", "placeholders": {} }, + "Reply": "Reply", + "@Reply": { + "type": "text", + "placeholders": {} + }, "Saturday": "Saturday", "@Saturday": { "type": "text", diff --git a/lib/i18n/messages_de.dart b/lib/i18n/messages_de.dart index 8c54ef3..320f846 100644 --- a/lib/i18n/messages_de.dart +++ b/lib/i18n/messages_de.dart @@ -81,51 +81,53 @@ class MessageLookup extends MessageLookupByLibrary { static m30(count) => "${count} weitere Teilnehmer laden"; - static m31(fileName) => "Play ${fileName}"; + static m31(number) => "${number} ausgewählt"; - static m32(username) => "${username} hat ein Event enternt"; + static m32(fileName) => "Play ${fileName}"; - static m33(username) => "${username} hat die Einladung abgelehnt"; + static m33(username) => "${username} hat ein Event enternt"; - static m34(username) => "Entfernt von ${username}"; + static m34(username) => "${username} hat die Einladung abgelehnt"; - static m35(username) => "Gelesen von ${username}"; + static m35(username) => "Entfernt von ${username}"; - static m36(username, count) => "Gelesen von ${username} und ${count} anderen"; + static m36(username) => "Gelesen von ${username}"; - static m37(username, username2) => "Gelesen von ${username} und ${username2}"; + static m37(username, count) => "Gelesen von ${username} und ${count} anderen"; - static m38(username) => "${username} hat eine Datei gesendet"; + static m38(username, username2) => "Gelesen von ${username} und ${username2}"; - static m39(username) => "${username} hat ein Bild gesendet"; + static m39(username) => "${username} hat eine Datei gesendet"; - static m40(username) => "${username} hat einen Sticker gesendet"; + static m40(username) => "${username} hat ein Bild gesendet"; - static m41(username) => "${username} hat ein Video gesendet"; + static m41(username) => "${username} hat einen Sticker gesendet"; - static m42(username) => "${username} hat eine Audio-Datei gesendet"; + static m42(username) => "${username} hat ein Video gesendet"; - static m43(username) => "${username} hat den Standort geteilt"; + static m43(username) => "${username} hat eine Audio-Datei gesendet"; - static m44(hours12, hours24, minutes, suffix) => "${hours24}:${minutes}"; + static m44(username) => "${username} hat den Standort geteilt"; - static m45(username, targetName) => "${username} hat die Verbannung von ${targetName} aufgehoben"; + static m45(hours12, hours24, minutes, suffix) => "${hours24}:${minutes}"; - static m46(type) => "Unbekanntes Event \'${type}\'"; + static m46(username, targetName) => "${username} hat die Verbannung von ${targetName} aufgehoben"; - static m47(unreadEvents) => "${unreadEvents} ungelesene Nachrichten"; + static m47(type) => "Unbekanntes Event \'${type}\'"; - static m48(unreadEvents, unreadChats) => "${unreadEvents} ungelesene Nachrichten in ${unreadChats} Chats"; + static m48(unreadEvents) => "${unreadEvents} ungelesene Nachrichten"; - static m49(username, count) => "${username} und ${count} andere schreiben ..."; + static m49(unreadEvents, unreadChats) => "${unreadEvents} ungelesene Nachrichten in ${unreadChats} Chats"; - static m50(username, username2) => "${username} und ${username2} schreiben ..."; + static m50(username, count) => "${username} und ${count} andere schreiben ..."; - static m51(username) => "${username} schreibt ..."; + static m51(username, username2) => "${username} und ${username2} schreiben ..."; - static m52(username) => "${username} hat den Chat verlassen"; + static m52(username) => "${username} schreibt ..."; - static m53(username, type) => "${username} hat ${type} Event gesendet"; + static m53(username) => "${username} hat den Chat verlassen"; + + static m54(username, type) => "${username} hat ${type} Event gesendet"; final messages = _notInlinedMessages(_notInlinedMessages); static _notInlinedMessages(_) => { @@ -218,6 +220,7 @@ class MessageLookup extends MessageLookupByLibrary { "Remove" : MessageLookupByLibrary.simpleMessage("Entfernen"), "Remove exile" : MessageLookupByLibrary.simpleMessage("Verbannung aufheben"), "Remove message" : MessageLookupByLibrary.simpleMessage("Nachricht entfernen"), + "Reply" : MessageLookupByLibrary.simpleMessage("Antworten"), "Revoke all permissions" : MessageLookupByLibrary.simpleMessage("Alle Berechtigungen zurücknehmen"), "Saturday" : MessageLookupByLibrary.simpleMessage("Samstag"), "Search for a chat" : MessageLookupByLibrary.simpleMessage("Durchsuche die Chats"), @@ -288,29 +291,30 @@ class MessageLookup extends MessageLookupByLibrary { "kicked" : m28, "kickedAndBanned" : m29, "loadCountMoreParticipants" : m30, - "play" : m31, - "redactedAnEvent" : m32, - "rejectedTheInvitation" : m33, - "removedBy" : m34, - "seenByUser" : m35, - "seenByUserAndCountOthers" : m36, - "seenByUserAndUser" : m37, - "sentAFile" : m38, - "sentAPicture" : m39, - "sentASticker" : m40, - "sentAVideo" : m41, - "sentAnAudio" : m42, - "sharedTheLocation" : m43, - "timeOfDay" : m44, + "numberSelected" : m31, + "play" : m32, + "redactedAnEvent" : m33, + "rejectedTheInvitation" : m34, + "removedBy" : m35, + "seenByUser" : m36, + "seenByUserAndCountOthers" : m37, + "seenByUserAndUser" : m38, + "sentAFile" : m39, + "sentAPicture" : m40, + "sentASticker" : m41, + "sentAVideo" : m42, + "sentAnAudio" : m43, + "sharedTheLocation" : m44, + "timeOfDay" : m45, "title" : MessageLookupByLibrary.simpleMessage("FluffyChat"), - "unbannedUser" : m45, - "unknownEvent" : m46, - "unreadMessages" : m47, - "unreadMessagesInChats" : m48, - "userAndOthersAreTyping" : m49, - "userAndUserAreTyping" : m50, - "userIsTyping" : m51, - "userLeftTheChat" : m52, - "userSentUnknownEvent" : m53 + "unbannedUser" : m46, + "unknownEvent" : m47, + "unreadMessages" : m48, + "unreadMessagesInChats" : m49, + "userAndOthersAreTyping" : m50, + "userAndUserAreTyping" : m51, + "userIsTyping" : m52, + "userLeftTheChat" : m53, + "userSentUnknownEvent" : m54 }; } diff --git a/lib/i18n/messages_messages.dart b/lib/i18n/messages_messages.dart index 102b3cd..75bdad6 100644 --- a/lib/i18n/messages_messages.dart +++ b/lib/i18n/messages_messages.dart @@ -81,51 +81,53 @@ class MessageLookup extends MessageLookupByLibrary { static m30(count) => "Load ${count} more participants"; - static m31(fileName) => "Play ${fileName}"; + static m31(number) => "${number} selected"; - static m32(username) => "${username} redacted an event"; + static m32(fileName) => "Play ${fileName}"; - static m33(username) => "${username} rejected the invitation"; + static m33(username) => "${username} redacted an event"; - static m34(username) => "Removed by ${username}"; + static m34(username) => "${username} rejected the invitation"; - static m35(username) => "Seen by ${username}"; + static m35(username) => "Removed by ${username}"; - static m36(username, count) => "Seen by ${username} and ${count} others"; + static m36(username) => "Seen by ${username}"; - static m37(username, username2) => "Seen by ${username} and ${username2}"; + static m37(username, count) => "Seen by ${username} and ${count} others"; - static m38(username) => "${username} sent a file"; + static m38(username, username2) => "Seen by ${username} and ${username2}"; - static m39(username) => "${username} sent a picture"; + static m39(username) => "${username} sent a file"; - static m40(username) => "${username} sent a sticker"; + static m40(username) => "${username} sent a picture"; - static m41(username) => "${username} sent a video"; + static m41(username) => "${username} sent a sticker"; - static m42(username) => "${username} sent an audio"; + static m42(username) => "${username} sent a video"; - static m43(username) => "${username} shared the location"; + static m43(username) => "${username} sent an audio"; - static m44(hours12, hours24, minutes, suffix) => "${hours12}:${minutes} ${suffix}"; + static m44(username) => "${username} shared the location"; - static m45(username, targetName) => "${username} unbanned ${targetName}"; + static m45(hours12, hours24, minutes, suffix) => "${hours12}:${minutes} ${suffix}"; - static m46(type) => "Unknown event \'${type}\'"; + static m46(username, targetName) => "${username} unbanned ${targetName}"; - static m47(unreadEvents) => "${unreadEvents} unread messages"; + static m47(type) => "Unknown event \'${type}\'"; - static m48(unreadEvents, unreadChats) => "${unreadEvents} unread messages in ${unreadChats} chats"; + static m48(unreadEvents) => "${unreadEvents} unread messages"; - static m49(username, count) => "${username} and ${count} others are typing..."; + static m49(unreadEvents, unreadChats) => "${unreadEvents} unread messages in ${unreadChats} chats"; - static m50(username, username2) => "${username} and ${username2} are typing..."; + static m50(username, count) => "${username} and ${count} others are typing..."; - static m51(username) => "${username} is typing..."; + static m51(username, username2) => "${username} and ${username2} are typing..."; - static m52(username) => "${username} left the chat"; + static m52(username) => "${username} is typing..."; - static m53(username, type) => "${username} sent a ${type} event"; + static m53(username) => "${username} left the chat"; + + static m54(username, type) => "${username} sent a ${type} event"; final messages = _notInlinedMessages(_notInlinedMessages); static _notInlinedMessages(_) => { @@ -218,6 +220,7 @@ class MessageLookup extends MessageLookupByLibrary { "Remove" : MessageLookupByLibrary.simpleMessage("Remove"), "Remove exile" : MessageLookupByLibrary.simpleMessage("Remove exile"), "Remove message" : MessageLookupByLibrary.simpleMessage("Remove message"), + "Reply" : MessageLookupByLibrary.simpleMessage("Reply"), "Revoke all permissions" : MessageLookupByLibrary.simpleMessage("Revoke all permissions"), "Saturday" : MessageLookupByLibrary.simpleMessage("Saturday"), "Search for a chat" : MessageLookupByLibrary.simpleMessage("Search for a chat"), @@ -288,29 +291,30 @@ class MessageLookup extends MessageLookupByLibrary { "kicked" : m28, "kickedAndBanned" : m29, "loadCountMoreParticipants" : m30, - "play" : m31, - "redactedAnEvent" : m32, - "rejectedTheInvitation" : m33, - "removedBy" : m34, - "seenByUser" : m35, - "seenByUserAndCountOthers" : m36, - "seenByUserAndUser" : m37, - "sentAFile" : m38, - "sentAPicture" : m39, - "sentASticker" : m40, - "sentAVideo" : m41, - "sentAnAudio" : m42, - "sharedTheLocation" : m43, - "timeOfDay" : m44, + "numberSelected" : m31, + "play" : m32, + "redactedAnEvent" : m33, + "rejectedTheInvitation" : m34, + "removedBy" : m35, + "seenByUser" : m36, + "seenByUserAndCountOthers" : m37, + "seenByUserAndUser" : m38, + "sentAFile" : m39, + "sentAPicture" : m40, + "sentASticker" : m41, + "sentAVideo" : m42, + "sentAnAudio" : m43, + "sharedTheLocation" : m44, + "timeOfDay" : m45, "title" : MessageLookupByLibrary.simpleMessage("FluffyChat"), - "unbannedUser" : m45, - "unknownEvent" : m46, - "unreadMessages" : m47, - "unreadMessagesInChats" : m48, - "userAndOthersAreTyping" : m49, - "userAndUserAreTyping" : m50, - "userIsTyping" : m51, - "userLeftTheChat" : m52, - "userSentUnknownEvent" : m53 + "unbannedUser" : m46, + "unknownEvent" : m47, + "unreadMessages" : m48, + "unreadMessagesInChats" : m49, + "userAndOthersAreTyping" : m50, + "userAndUserAreTyping" : m51, + "userIsTyping" : m52, + "userLeftTheChat" : m53, + "userSentUnknownEvent" : m54 }; } diff --git a/lib/main.dart b/lib/main.dart index dfb2ea1..d4e5521 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -46,7 +46,7 @@ class App extends StatelessWidget { color: Colors.white, elevation: 1, textTheme: TextTheme( - title: TextStyle( + headline6: TextStyle( color: Colors.black, ), ), diff --git a/lib/utils/event_extension.dart b/lib/utils/event_extension.dart index 4c0872d..2dc2dc9 100644 --- a/lib/utils/event_extension.dart +++ b/lib/utils/event_extension.dart @@ -13,7 +13,7 @@ extension LocalizedBody on Event { }; getLocalizedBody(BuildContext context, - {bool withSenderNamePrefix = false, hideQuotes = false}) { + {bool withSenderNamePrefix = false, bool hideQuotes = false}) { if (this.redacted) { return I18n.of(context) .removedBy(redactedBecause.sender.calcDisplayname()); diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 159cb2d..0d06afb 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -5,14 +5,17 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/components/adaptive_page_layout.dart'; import 'package:fluffychat/components/chat_settings_popup_menu.dart'; +import 'package:fluffychat/components/dialogs/confirm_dialog.dart'; import 'package:fluffychat/components/list_items/message.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/i18n/i18n.dart'; import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/utils/event_extension.dart'; import 'package:fluffychat/utils/room_extension.dart'; import 'package:fluffychat/views/chat_encryption_settings.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:toast/toast.dart'; import 'package:pedantic/pedantic.dart'; @@ -63,6 +66,12 @@ class _ChatState extends State<_Chat> { Timer typingTimeout; bool currentlyTyping = false; + Set selectedEvents = {}; + + Event replyEvent; + + bool get selectMode => selectedEvents.isNotEmpty; + @override void initState() { _scrollController.addListener(() async { @@ -130,7 +139,26 @@ class _ChatState extends State<_Chat> { void send() { if (sendController.text.isEmpty) return; - room.sendTextEvent(sendController.text); + Map textContent = { + "msgtype": "m.text", + "body": sendController.text, + }; + if (replyEvent != null) { + String replyText = ("<${room.client.userID}> " + replyEvent.body); + List replyTextLines = replyText.split("\n"); + for (int i = 0; i < replyTextLines.length; i++) { + replyTextLines[i] = "> " + replyTextLines[i]; + } + replyText = replyTextLines.join("\n"); + textContent["body"] = replyText + "\n\n${sendController.text}"; + textContent["m.relates_to"] = { + "m.in_reply_to": { + "event_id": replyEvent.eventId, + }, + }; + setState(() => replyEvent = null); + } + room.sendEvent(textContent); sendController.text = ""; } @@ -181,6 +209,69 @@ class _ChatState extends State<_Chat> { ); } + String _getSelectedEventString(BuildContext context) { + String copyString = ""; + for (Event event in selectedEvents) { + if (copyString.isNotEmpty) copyString += "\n\n"; + copyString += event.getLocalizedBody(context, withSenderNamePrefix: true); + } + return copyString; + } + + void copyEventsAction(BuildContext context) { + Clipboard.setData(ClipboardData(text: _getSelectedEventString(context))); + setState(() => selectedEvents.clear()); + } + + void redactEventsAction(BuildContext context) async { + bool confirmed = false; + await showDialog( + context: context, + builder: (context) => ConfirmDialog( + I18n.of(context).messageWillBeRemovedWarning, + I18n.of(context).remove, + (c) => confirmed = true), + ); + if (!confirmed) return; + for (Event event in selectedEvents) { + await Matrix.of(context).tryRequestWithLoadingDialog( + event.status > 0 ? event.redact() : event.remove()); + } + setState(() => selectedEvents.clear()); + } + + bool get canRedactSelectedEvents { + for (Event event in selectedEvents) { + if (event.canRedact == false) return false; + } + return true; + } + + void forwardEventsAction(BuildContext context) async { + if (selectedEvents.length == 1) { + Matrix.of(context).shareContent = selectedEvents.first.content; + } else { + Matrix.of(context).shareContent = { + "msgtype": "m.text", + "body": _getSelectedEventString(context), + }; + } + setState(() => selectedEvents.clear()); + Navigator.of(context).popUntil((r) => r.isFirst); + } + + void sendAgainAction() { + selectedEvents.first.sendAgain(); + setState(() => selectedEvents.clear()); + } + + void replyAction() { + setState(() { + replyEvent = selectedEvents.first; + selectedEvents.clear(); + }); + } + @override Widget build(BuildContext context) { matrix = Matrix.of(context); @@ -224,34 +315,56 @@ class _ChatState extends State<_Chat> { return Scaffold( appBar: AppBar( - title: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(room.getLocalizedDisplayname(context)), - AnimatedContainer( - duration: Duration(milliseconds: 500), - height: typingText.isEmpty ? 0 : 20, - child: Row( + leading: selectMode + ? IconButton( + icon: Icon(Icons.close), + onPressed: () => setState(() => selectedEvents.clear()), + ) + : null, + title: selectedEvents.isEmpty + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, 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, + Text(room.getLocalizedDisplayname(context)), + 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)], + ) + : Text(I18n.of(context) + .numberSelected(selectedEvents.length.toString())), + actions: selectMode + ? [ + IconButton( + icon: Icon(Icons.content_copy), + onPressed: () => copyEventsAction(context), + ), + if (canRedactSelectedEvents) + IconButton( + icon: Icon(Icons.delete), + onPressed: () => redactEventsAction(context), + ), + ] + : [ChatSettingsPopupMenu(room, !room.isDirectChat)], ), body: SafeArea( child: Column( @@ -305,16 +418,71 @@ class _ChatState extends State<_Chat> { ), ) : Message(timeline.events[i - 1], + onSelect: (Event event) => event.redacted + ? null + : selectedEvents.contains(event) + ? setState( + () => selectedEvents.remove(event)) + : setState( + () => selectedEvents.add(event)), + longPressSelect: selectedEvents.isEmpty, + selected: selectedEvents + .contains(timeline.events[i - 1]), nextEvent: i >= 2 ? timeline.events[i - 2] : null); }); }, ), ), + AnimatedContainer( + duration: Duration(milliseconds: 300), + height: replyEvent != null ? 56 : 0, + child: Material( + color: Theme.of(context).secondaryHeaderColor, + child: Row( + children: [ + IconButton( + icon: Icon(Icons.close), + onPressed: () => setState(() => replyEvent = null), + ), + Container( + width: 2, + height: 36, + color: Theme.of(context).primaryColor, + ), + SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + (replyEvent?.sender?.calcDisplayname() ?? "") + ":", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + Text( + replyEvent?.getLocalizedBody(context, + withSenderNamePrefix: false) ?? + "", + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + ), + ], + ), + ), + ), room.canSendDefaultMessages && room.membership == Membership.join ? Container( decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).backgroundColor, boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.2), @@ -326,123 +494,175 @@ class _ChatState extends State<_Chat> { ), 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) => - >[ - PopupMenuItem( - value: "file", - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - child: Icon(Icons.attachment), - ), - title: Text(I18n.of(context).sendFile), - contentPadding: EdgeInsets.all(0), - ), + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: selectMode + ? [ + Container( + height: 56, + child: FlatButton( + onPressed: () => forwardEventsAction(context), + child: Row( + children: [ + Icon(Icons.keyboard_arrow_left), + Text(I18n.of(context).forward), + ], ), - PopupMenuItem( - value: "image", - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - child: Icon(Icons.image), - ), - title: Text(I18n.of(context).sendImage), - contentPadding: EdgeInsets.all(0), - ), - ), - PopupMenuItem( - value: "camera", - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.purple, - foregroundColor: Colors.white, - child: Icon(Icons.camera), - ), - title: Text(I18n.of(context).openCamera), - contentPadding: EdgeInsets.all(0), - ), - ), - ], + ), ), - SizedBox(width: 8), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: TextField( - minLines: 1, - maxLines: kIsWeb ? 1 : 8, - keyboardType: kIsWeb - ? TextInputType.text - : TextInputType.multiline, - onSubmitted: (String text) { - send(); - FocusScope.of(context).requestFocus(inputFocus); - }, - focusNode: inputFocus, - controller: sendController, - decoration: InputDecoration( - hintText: I18n.of(context).writeAMessage, - border: InputBorder.none, - suffixIcon: sendController.text.isEmpty - ? InkWell( - child: Icon(room.encrypted - ? Icons.lock - : Icons.lock_open), - onTap: () => Navigator.of(context).push( - AppRoute.defaultRoute( - context, - ChatEncryptionSettingsView( - widget.id), + selectedEvents.length == 1 + ? selectedEvents.first.status > 0 + ? Container( + height: 56, + child: FlatButton( + onPressed: () => replyAction(), + child: Row( + children: [ + Text(I18n.of(context).reply), + Icon( + Icons.keyboard_arrow_right), + ], + ), + ), + ) + : Container( + height: 56, + child: FlatButton( + onPressed: () => sendAgainAction(), + child: Row( + children: [ + Text(I18n.of(context) + .tryToSendAgain), + SizedBox(width: 4), + Icon(Icons.send, size: 16), + ], + ), + ), + ) + : Container(), + ] + : [ + 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) => + >[ + PopupMenuItem( + value: "file", + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + child: Icon(Icons.attachment), + ), + title: + Text(I18n.of(context).sendFile), + contentPadding: EdgeInsets.all(0), ), ), - ) - : null, + PopupMenuItem( + value: "image", + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + child: Icon(Icons.image), + ), + title: Text( + I18n.of(context).sendImage), + contentPadding: EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: "camera", + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.purple, + foregroundColor: Colors.white, + child: Icon(Icons.camera), + ), + title: Text( + I18n.of(context).openCamera), + contentPadding: EdgeInsets.all(0), + ), + ), + ], + ), + SizedBox(width: 8), + Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 4.0), + child: TextField( + minLines: 1, + maxLines: kIsWeb ? 1 : 8, + keyboardType: kIsWeb + ? TextInputType.text + : TextInputType.multiline, + onSubmitted: (String text) { + send(); + FocusScope.of(context) + .requestFocus(inputFocus); + }, + focusNode: inputFocus, + controller: sendController, + decoration: InputDecoration( + hintText: I18n.of(context).writeAMessage, + border: InputBorder.none, + suffixIcon: sendController.text.isEmpty + ? InkWell( + child: Icon(room.encrypted + ? Icons.lock + : Icons.lock_open), + onTap: () => + Navigator.of(context).push( + AppRoute.defaultRoute( + context, + ChatEncryptionSettingsView( + widget.id), + ), + ), + ) + : null, + ), + 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); + } + }, + ), + ), ), - 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); - } - }, - ), - ), - ), - IconButton( - icon: Icon(Icons.send), - onPressed: () => send(), - ), - ], + IconButton( + icon: Icon(Icons.send), + onPressed: () => send(), + ), + ], ), ) : Container(), diff --git a/pubspec.lock b/pubspec.lock index 67533b2..d079d7f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -239,7 +239,7 @@ packages: name: intl url: "https://pub.dartlang.org" source: hosted - version: "0.16.0" + version: "0.16.1" intl_translation: dependency: "direct main" description: