diff --git a/lib/components/dialogs/presence_dialog.dart b/lib/components/dialogs/presence_dialog.dart new file mode 100644 index 0000000..cb54773 --- /dev/null +++ b/lib/components/dialogs/presence_dialog.dart @@ -0,0 +1,72 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/views/chat.dart'; +import 'package:flutter/material.dart'; +import 'package:fluffychat/utils/presence_extension.dart'; + +import '../avatar.dart'; +import '../matrix.dart'; + +class PresenceDialog extends StatelessWidget { + final Uri avatarUrl; + final String displayname; + final Presence presence; + + const PresenceDialog( + this.presence, { + this.avatarUrl, + this.displayname, + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: ListTile( + contentPadding: EdgeInsets.zero, + leading: Avatar(avatarUrl, displayname), + title: Text(displayname), + subtitle: Text(presence.sender), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(presence.getLocalizedStatusMessage(context)), + if (presence.presence != null) + Text( + presence.presence.toString().split('.').last, + style: TextStyle( + color: presence.currentlyActive == true + ? Colors.green + : Theme.of(context).primaryColor, + ), + ) + ], + ), + actions: [ + if (presence.sender != Matrix.of(context).client.userID) + FlatButton( + child: Text(L10n.of(context).sendAMessage), + onPressed: () async { + final roomId = await User( + presence.sender, + room: Room(id: '', client: Matrix.of(context).client), + ).startDirectChat(); + await Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute( + context, + ChatView(roomId), + ), + (Route r) => r.isFirst); + }, + ), + FlatButton( + child: Text(L10n.of(context).close), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + } +} diff --git a/lib/components/list_items/message.dart b/lib/components/list_items/message.dart index 2d6e255..7935e49 100644 --- a/lib/components/list_items/message.dart +++ b/lib/components/list_items/message.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/utils/event_extension.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:flutter/material.dart'; +import '../adaptive_page_layout.dart'; import '../avatar.dart'; import '../matrix.dart'; import 'state_message.dart'; @@ -74,74 +75,78 @@ class Message extends StatelessWidget { margin: BubbleEdges.symmetric(horizontal: 4), color: color, nip: nip, - child: Stack( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (event.isReply) - FutureBuilder( - future: event.getReplyEvent(timeline), - builder: (BuildContext context, snapshot) { - final replyEvent = snapshot.hasData - ? snapshot.data - : Event( - eventId: event.content['m.relates_to'] - ['m.in_reply_to']['event_id'], - content: {'msgtype': 'm.text', 'body': '...'}, - senderId: event.senderId, - typeKey: 'm.room.message', - room: event.room, - roomId: event.roomId, - status: 1, - time: DateTime.now(), - ); - return Container( - margin: EdgeInsets.symmetric(vertical: 4.0), - child: - ReplyContent(replyEvent, lightText: ownMessage), - ); - }, - ), - MessageContent( - event, - textColor: textColor, - ), - if (event.type == EventTypes.Encrypted && - event.messageType == MessageTypes.BadEncrypted && - event.content['body'] == DecryptError.UNKNOWN_SESSION) - RaisedButton( - color: color.withAlpha(100), - child: Text( - L10n.of(context).requestPermission, - style: TextStyle(color: textColor), + child: Container( + constraints: + BoxConstraints(maxWidth: AdaptivePageLayout.defaultMinWidth), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (event.isReply) + FutureBuilder( + future: event.getReplyEvent(timeline), + builder: (BuildContext context, snapshot) { + final replyEvent = snapshot.hasData + ? snapshot.data + : Event( + eventId: event.content['m.relates_to'] + ['m.in_reply_to']['event_id'], + content: {'msgtype': 'm.text', 'body': '...'}, + senderId: event.senderId, + typeKey: 'm.room.message', + room: event.room, + roomId: event.roomId, + status: 1, + time: DateTime.now(), + ); + return Container( + margin: EdgeInsets.symmetric(vertical: 4.0), + child: + ReplyContent(replyEvent, lightText: ownMessage), + ); + }, ), - onPressed: () => SimpleDialogs(context) - .tryRequestWithLoadingDialog(event.requestKey()), - ), - SizedBox(height: 4), - Opacity( - opacity: 0, - child: _MetaRow( + MessageContent( event, - ownMessage, - textColor, + textColor: textColor, ), - ), - ], - ), - Positioned( - bottom: 0, - right: ownMessage ? 0 : null, - left: !ownMessage ? 0 : null, - child: _MetaRow( - event, - ownMessage, - textColor, + if (event.type == EventTypes.Encrypted && + event.messageType == MessageTypes.BadEncrypted && + event.content['body'] == DecryptError.UNKNOWN_SESSION) + RaisedButton( + color: color.withAlpha(100), + child: Text( + L10n.of(context).requestPermission, + style: TextStyle(color: textColor), + ), + onPressed: () => SimpleDialogs(context) + .tryRequestWithLoadingDialog(event.requestKey()), + ), + SizedBox(height: 4), + Opacity( + opacity: 0, + child: _MetaRow( + event, + ownMessage, + textColor, + ), + ), + ], ), - ), - ], + Positioned( + bottom: 0, + right: ownMessage ? 0 : null, + left: !ownMessage ? 0 : null, + child: _MetaRow( + event, + ownMessage, + textColor, + ), + ), + ], + ), ), ), ), diff --git a/lib/components/list_items/presence_list_item.dart b/lib/components/list_items/presence_list_item.dart index 1082734..b7c9a23 100644 --- a/lib/components/list_items/presence_list_item.dart +++ b/lib/components/list_items/presence_list_item.dart @@ -1,12 +1,9 @@ import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/utils/app_route.dart'; -import 'package:fluffychat/views/chat.dart'; +import 'package:fluffychat/components/dialogs/presence_dialog.dart'; import 'package:flutter/material.dart'; import '../avatar.dart'; import '../matrix.dart'; -import 'package:fluffychat/utils/presence_extension.dart'; class PresenceListItem extends StatelessWidget { final Presence presence; @@ -36,68 +33,27 @@ class PresenceListItem extends StatelessWidget { return InkWell( onTap: () => showDialog( context: context, - builder: (c) => AlertDialog( - title: ListTile( - contentPadding: EdgeInsets.zero, - leading: Avatar(avatarUrl, displayname), - title: Text(displayname), - subtitle: Text(presence.sender), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + builder: (c) => PresenceDialog( + presence, + avatarUrl: avatarUrl, + displayname: displayname, + ), + child: Container( + width: 80, + child: Column( children: [ - Text(presence.getLocalizedStatusMessage(context)), - if (presence.presence != null) - Text( - presence.presence.toString().split('.').last, - style: TextStyle( - color: presence.currentlyActive == true - ? Colors.green - : Theme.of(context).primaryColor, - ), - ) + SizedBox(height: 9), + Avatar(avatarUrl, displayname), + Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + displayname, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), ], ), - actions: [ - if (presence.sender != Matrix.of(context).client.userID) - FlatButton( - child: Text(L10n.of(context).sendAMessage), - onPressed: () async { - final roomId = await User( - presence.sender, - room: Room(id: '', client: Matrix.of(context).client), - ).startDirectChat(); - await Navigator.of(context).pushAndRemoveUntil( - AppRoute.defaultRoute( - context, - ChatView(roomId), - ), - (Route r) => r.isFirst); - }, - ), - FlatButton( - child: Text(L10n.of(context).close), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ), - child: Container( - width: 80, - child: Column( - children: [ - SizedBox(height: 9), - Avatar(avatarUrl, displayname), - Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - displayname, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ], ), ), ); diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index a905f7c..8abe67c 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -354,6 +354,11 @@ "type": "text", "placeholders": {} }, + "Currenlty active": "Jetzt gerade online", + "@Currenlty active": { + "type": "text", + "placeholders": {} + }, "dateAndTimeOfDay": "{date}, {timeOfDay}", "@dateAndTimeOfDay": { "type": "text", @@ -687,7 +692,7 @@ "username": {} } }, - "lastActiveAgo": "Zuletzt aktiv: {localizedTimeShort}", + "lastActiveAgo": "Zuletzt gesehen: {localizedTimeShort}", "@lastActiveAgo": { "type": "text", "placeholders": { @@ -997,6 +1002,11 @@ "username": {} } }, + "Seen a long time ago": "Vor sehr langer Zeit gesehen", + "@Seen a long time ago": { + "type": "text", + "placeholders": {} + }, "seenByUserAndUser": "Gelesen von {username} und {username2}", "@seenByUserAndUser": { "type": "text", diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 38365e5..010e949 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2020-05-12T08:42:24.358124", + "@@last_modified": "2020-05-15T15:34:50.065646", "About": "About", "@About": { "type": "text", @@ -354,6 +354,11 @@ "type": "text", "placeholders": {} }, + "Currenlty active": "Currenlty active", + "@Currenlty active": { + "type": "text", + "placeholders": {} + }, "dateAndTimeOfDay": "{date}, {timeOfDay}", "@dateAndTimeOfDay": { "type": "text", @@ -995,6 +1000,11 @@ "type": "text", "placeholders": {} }, + "Seen a long time ago": "Seen a long time ago", + "@Seen a long time ago": { + "type": "text", + "placeholders": {} + }, "seenByUser": "Seen by {username}", "@seenByUser": { "type": "text", diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 3863ad1..14e83b8 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -261,6 +261,8 @@ class L10n extends MatrixLocalizations { String get createNewGroup => Intl.message("Create new group"); + String get currentlyActive => Intl.message('Currenlty active'); + String dateAndTimeOfDay(String date, String timeOfDay) => Intl.message( "$date, $timeOfDay", name: "dateAndTimeOfDay", @@ -599,6 +601,8 @@ class L10n extends MatrixLocalizations { String get searchForAChat => Intl.message("Search for a chat"); + String get lastSeenLongTimeAgo => Intl.message('Seen a long time ago'); + String seenByUser(String username) => Intl.message( "Seen by $username", name: "seenByUser", diff --git a/lib/l10n/messages_de.dart b/lib/l10n/messages_de.dart index 8afeea9..e2ec61f 100644 --- a/lib/l10n/messages_de.dart +++ b/lib/l10n/messages_de.dart @@ -83,7 +83,7 @@ class MessageLookup extends MessageLookupByLibrary { static m31(username, targetName) => "${username} hat ${targetName} hinausgeworfen und verbannt"; - static m32(localizedTimeShort) => "Zuletzt aktiv: ${localizedTimeShort}"; + static m32(localizedTimeShort) => "Zuletzt gesehen: ${localizedTimeShort}"; static m33(count) => "${count} weitere Teilnehmer laden"; @@ -181,6 +181,7 @@ class MessageLookup extends MessageLookupByLibrary { "Create" : MessageLookupByLibrary.simpleMessage("Create"), "Create account now" : MessageLookupByLibrary.simpleMessage("Account jetzt erstellen"), "Create new group" : MessageLookupByLibrary.simpleMessage("Neue Gruppe"), + "Currenlty active" : MessageLookupByLibrary.simpleMessage("Jetzt gerade online"), "Dark" : MessageLookupByLibrary.simpleMessage("Dunkel"), "Delete" : MessageLookupByLibrary.simpleMessage("Löschen"), "Delete message" : MessageLookupByLibrary.simpleMessage("Nachricht löschen"), @@ -273,6 +274,7 @@ class MessageLookup extends MessageLookupByLibrary { "Revoke all permissions" : MessageLookupByLibrary.simpleMessage("Alle Berechtigungen zurücknehmen"), "Saturday" : MessageLookupByLibrary.simpleMessage("Samstag"), "Search for a chat" : MessageLookupByLibrary.simpleMessage("Durchsuche die Chats"), + "Seen a long time ago" : MessageLookupByLibrary.simpleMessage("Vor sehr langer Zeit gesehen"), "Send" : MessageLookupByLibrary.simpleMessage("Senden"), "Send a message" : MessageLookupByLibrary.simpleMessage("Nachricht schreiben"), "Send file" : MessageLookupByLibrary.simpleMessage("Datei senden"), diff --git a/lib/l10n/messages_messages.dart b/lib/l10n/messages_messages.dart index 082d826..cc21e51 100644 --- a/lib/l10n/messages_messages.dart +++ b/lib/l10n/messages_messages.dart @@ -181,6 +181,7 @@ class MessageLookup extends MessageLookupByLibrary { "Create" : MessageLookupByLibrary.simpleMessage("Create"), "Create account now" : MessageLookupByLibrary.simpleMessage("Create account now"), "Create new group" : MessageLookupByLibrary.simpleMessage("Create new group"), + "Currenlty active" : MessageLookupByLibrary.simpleMessage("Currenlty active"), "Dark" : MessageLookupByLibrary.simpleMessage("Dark"), "Delete" : MessageLookupByLibrary.simpleMessage("Delete"), "Delete message" : MessageLookupByLibrary.simpleMessage("Delete message"), @@ -275,6 +276,7 @@ class MessageLookup extends MessageLookupByLibrary { "Revoke all permissions" : MessageLookupByLibrary.simpleMessage("Revoke all permissions"), "Saturday" : MessageLookupByLibrary.simpleMessage("Saturday"), "Search for a chat" : MessageLookupByLibrary.simpleMessage("Search for a chat"), + "Seen a long time ago" : MessageLookupByLibrary.simpleMessage("Seen a long time ago"), "Send" : MessageLookupByLibrary.simpleMessage("Send"), "Send a message" : MessageLookupByLibrary.simpleMessage("Send a message"), "Send file" : MessageLookupByLibrary.simpleMessage("Send file"), diff --git a/lib/utils/room_status_extension.dart b/lib/utils/room_status_extension.dart new file mode 100644 index 0000000..4b53f94 --- /dev/null +++ b/lib/utils/room_status_extension.dart @@ -0,0 +1,23 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:flutter/widgets.dart'; + +import 'date_time_extension.dart'; + +extension RoomStatusExtension on Room { + Presence get directChatPresence => client.presences[directChatMatrixID]; + + String getLocalizedStatus(BuildContext context) { + if (isDirectChat) { + if (directChatPresence != null) { + if (directChatPresence.currentlyActive == true) { + return L10n.of(context).currentlyActive; + } + return L10n.of(context) + .lastActiveAgo(directChatPresence.time.localizedTimeShort(context)); + } + return L10n.of(context).lastSeenLongTimeAgo; + } + return L10n.of(context).countParticipants(mJoinedMemberCount.toString()); + } +} diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 47f7c90..977ad9b 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/avatar.dart'; import 'package:fluffychat/components/chat_settings_popup_menu.dart'; +import 'package:fluffychat/components/dialogs/presence_dialog.dart'; import 'package:fluffychat/components/dialogs/recording_dialog.dart'; import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; import 'package:fluffychat/components/encryption_button.dart'; @@ -12,6 +15,8 @@ import 'package:fluffychat/components/list_items/message.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/components/reply_content.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/utils/room_status_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -19,6 +24,7 @@ import 'package:bot_toast/bot_toast.dart'; import 'package:image_picker/image_picker.dart'; import 'package:pedantic/pedantic.dart'; +import 'chat_details.dart'; import 'chat_list.dart'; import '../components/input_bar.dart'; @@ -359,38 +365,59 @@ class _ChatState extends State<_Chat> { onPressed: () => setState(() => selectedEvents.clear()), ) : null, + titleSpacing: 0, title: selectedEvents.isEmpty - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: !kIsWeb && Platform.isIOS - ? CrossAxisAlignment.center - : CrossAxisAlignment.start, - children: [ - Text(room.getLocalizedDisplayname(L10n.of(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: 13), - SizedBox(width: 4), - Text( - typingText, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontStyle: FontStyle.italic, - fontSize: 16, + ? StreamBuilder( + stream: Matrix.of(context) + .client + .onPresence + .stream + .where((p) => p.sender == room.directChatMatrixID), + builder: (context, snapshot) { + return ListTile( + leading: Avatar(room.avatar, room.displayname), + contentPadding: EdgeInsets.zero, + onTap: () => + room.isDirectChat && room.directChatPresence == null + ? null + : room.isDirectChat + ? showDialog( + context: context, + builder: (c) => PresenceDialog( + room.directChatPresence, + avatarUrl: room.avatar, + displayname: room.displayname, + ), + ) + : Navigator.of(context).push( + AppRoute.defaultRoute( + context, + ChatDetails(room), + ), + ), + title: Text(room.getLocalizedDisplayname(L10n.of(context))), + subtitle: typingText.isEmpty + ? Text( + room.getLocalizedStatus(context), + ) + : Row( + children: [ + Icon(Icons.edit, + color: Theme.of(context).primaryColor, + size: 13), + SizedBox(width: 4), + Text( + typingText, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontStyle: FontStyle.italic, + fontSize: 16, + ), + ), + ], ), - ), - ], - ), - ), - ], - ) + ); + }) : Text(L10n.of(context) .numberSelected(selectedEvents.length.toString())), actions: selectMode @@ -456,6 +483,14 @@ class _ChatState extends State<_Chat> { if (timeline.events.isEmpty) return Container(); return ListView.builder( + padding: EdgeInsets.symmetric( + horizontal: max( + 0, + (MediaQuery.of(context).size.width - + AdaptivePageLayout.defaultMinWidth * + 2) / + 2), + ), reverse: true, itemCount: timeline.events.length + 2, controller: _scrollController,