diff --git a/lib/components/avatar.dart b/lib/components/avatar.dart index 58b16ce..c7d4603 100644 --- a/lib/components/avatar.dart +++ b/lib/components/avatar.dart @@ -42,7 +42,9 @@ class Avatar extends StatelessWidget { src, ) : null, - backgroundColor: name?.color ?? Theme.of(context).secondaryHeaderColor, + backgroundColor: mxContent.mxc.isEmpty + ? name?.color ?? Theme.of(context).secondaryHeaderColor + : Theme.of(context).secondaryHeaderColor, child: mxContent.mxc.isEmpty ? Text(fallbackLetters, style: TextStyle(color: Colors.white)) : null, diff --git a/lib/components/content_banner.dart b/lib/components/content_banner.dart index 33b65e0..deebec9 100644 --- a/lib/components/content_banner.dart +++ b/lib/components/content_banner.dart @@ -12,11 +12,13 @@ class ContentBanner extends StatelessWidget { final double height; final IconData defaultIcon; final bool loading; + final Function onEdit; const ContentBanner(this.mxContent, {this.height = 400, this.defaultIcon = Icons.people_outline, this.loading = false, + this.onEdit, Key key}) : super(key: key); @@ -42,25 +44,46 @@ class ContentBanner extends StatelessWidget { : null, child: Container( height: 200, - color: Theme.of(context).secondaryHeaderColor, - child: !loading - ? mxContent.mxc?.isNotEmpty ?? false - ? kIsWeb - ? Image.network( - src, - height: 200, - fit: BoxFit.cover, - ) - : CachedNetworkImage( - imageUrl: src, - height: 200, - fit: BoxFit.cover, - placeholder: (c, s) => - Center(child: CircularProgressIndicator()), - errorWidget: (c, s, o) => Icon(Icons.error, size: 200), - ) - : Icon(defaultIcon, size: 200) - : Icon(defaultIcon, size: 200), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).secondaryHeaderColor, + ), + child: Stack( + children: [ + Positioned( + left: 0, + right: 0, + top: 0, + bottom: 0, + child: !loading + ? mxContent.mxc?.isNotEmpty ?? false + ? kIsWeb + ? Image.network( + src, + height: 200, + fit: BoxFit.cover, + ) + : CachedNetworkImage( + imageUrl: src, + height: 200, + fit: BoxFit.cover, + ) + : Icon(defaultIcon, size: 200) + : Icon(defaultIcon, size: 200), + ), + if (this.onEdit != null) + Container( + margin: EdgeInsets.all(8), + alignment: Alignment.bottomRight, + child: FloatingActionButton( + mini: true, + backgroundColor: Theme.of(context).primaryColor, + child: Icon(Icons.file_upload), + onPressed: onEdit, + ), + ), + ], + ), ), ); } diff --git a/lib/components/list_items/chat_list_item.dart b/lib/components/list_items/chat_list_item.dart index c8e061d..966810e 100644 --- a/lib/components/list_items/chat_list_item.dart +++ b/lib/components/list_items/chat_list_item.dart @@ -1,8 +1,8 @@ import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/components/message_content.dart'; +import 'package:fluffychat/utils/event_extension.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/app_route.dart'; -import 'package:fluffychat/utils/room_name_calculator.dart'; +import 'package:fluffychat/utils/room_extension.dart'; import 'package:fluffychat/views/chat.dart'; import 'package:flutter/material.dart'; import 'package:toast/toast.dart'; @@ -91,7 +91,7 @@ class ChatListItem extends StatelessWidget { children: [ Expanded( child: Text( - RoomNameCalculator(room).name, + room.getLocalizedDisplayname(context), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -117,9 +117,16 @@ class ChatListItem extends StatelessWidget { color: Theme.of(context).primaryColor, ), ) - : MessageContent( - room.lastEvent, - textOnly: true, + : Text( + room.lastEvent.getLocalizedBody(context, + withSenderNamePrefix: true, hideQuotes: true), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + decoration: room.lastEvent.redacted + ? TextDecoration.lineThrough + : null, + ), ), ), SizedBox(width: 8), diff --git a/lib/components/list_items/state_message.dart b/lib/components/list_items/state_message.dart index e82da39..e0db5e0 100644 --- a/lib/components/list_items/state_message.dart +++ b/lib/components/list_items/state_message.dart @@ -1,8 +1,8 @@ import 'package:bubble/bubble.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:flutter/material.dart'; - -import '../message_content.dart'; +import 'package:fluffychat/utils/event_extension.dart'; +import 'package:link_text/link_text.dart'; class StateMessage extends StatelessWidget { final Event event; @@ -23,7 +23,13 @@ class StateMessage extends StatelessWidget { color: Colors.black, elevation: 0, alignment: Alignment.center, - child: MessageContent(event, textColor: Colors.white), + child: LinkText( + text: event.getLocalizedBody(context), + textStyle: TextStyle( + color: Colors.white, + decoration: event.redacted ? TextDecoration.lineThrough : null, + ), + ), ), ), ); diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 126346b..743c813 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -4,7 +4,8 @@ import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/utils/app_route.dart'; -import 'package:fluffychat/utils/room_name_calculator.dart'; +import 'package:fluffychat/utils/event_extension.dart'; +import 'package:fluffychat/utils/room_extension.dart'; import 'package:fluffychat/utils/sqflite_store.dart'; import 'package:fluffychat/views/chat.dart'; import 'package:flutter/foundation.dart'; @@ -264,28 +265,12 @@ class MatrixState extends State { : "$unreadEvents unread messages"; // Calculate the body - String body; - switch (event.messageType) { - case MessageTypes.Image: - body = "${event.sender.calcDisplayname()} sent a picture"; - break; - case MessageTypes.File: - body = "${event.sender.calcDisplayname()} sent a file"; - break; - case MessageTypes.Audio: - body = "${event.sender.calcDisplayname()} sent an audio"; - break; - case MessageTypes.Video: - body = "${event.sender.calcDisplayname()} sent a video"; - break; - default: - body = "${event.sender.calcDisplayname()}: ${event.body}"; - break; - } + final String body = event.getLocalizedBody(context, + withSenderNamePrefix: true, hideQuotes: true); // The person object for the android message style notification final person = Person( - name: RoomNameCalculator(room).name, + name: room.getLocalizedDisplayname(context), icon: room.avatar.mxc.isEmpty ? null : await downloadAndSaveContent( @@ -320,7 +305,10 @@ class MatrixState extends State { var platformChannelSpecifics = NotificationDetails( androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); await _flutterLocalNotificationsPlugin.show( - 0, RoomNameCalculator(room).name, body, platformChannelSpecifics, + 0, + room.getLocalizedDisplayname(context), + body, + platformChannelSpecifics, payload: roomId); } catch (exception) { print("[Push] Error while processing notification: " + @@ -356,7 +344,7 @@ class MatrixState extends State { @override void initState() { if (widget.client == null) { - client = Client(widget.clientName, debug: false); + client = Client(widget.clientName, debug: true); if (!kIsWeb) { _initWithStore(); } else { diff --git a/lib/components/message_content.dart b/lib/components/message_content.dart index 7831584..c787a8d 100644 --- a/lib/components/message_content.dart +++ b/lib/components/message_content.dart @@ -2,6 +2,7 @@ import 'package:bubble/bubble.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/utils/event_extension.dart'; import 'package:fluffychat/views/content_web_view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -13,41 +14,17 @@ import 'matrix.dart'; class MessageContent extends StatelessWidget { final Event event; final Color textColor; - final bool textOnly; - const MessageContent(this.event, {this.textColor, this.textOnly = false}); + const MessageContent(this.event, {this.textColor}); @override Widget build(BuildContext context) { - final int maxLines = textOnly ? 1 : null; - - final Widget unknown = Text( - "${event.sender.calcDisplayname()} sent a ${event.typeKey} event", - maxLines: maxLines, - overflow: textOnly ? TextOverflow.ellipsis : null, - style: TextStyle( - color: textColor, - decoration: event.redacted ? TextDecoration.lineThrough : null, - ), - ); - switch (event.type) { case EventTypes.Message: case EventTypes.Sticker: switch (event.messageType) { case MessageTypes.Image: case MessageTypes.Sticker: - if (textOnly) { - return Text( - "${event.sender.calcDisplayname()} sent a picture", - maxLines: maxLines, - style: TextStyle( - color: textColor, - decoration: - event.redacted ? TextDecoration.lineThrough : null, - ), - ); - } final int size = 400; final String src = MxContent(event.content["url"]).getThumbnail( Matrix.of(context).client, @@ -78,17 +55,6 @@ class MessageContent extends StatelessWidget { ), ); case MessageTypes.Audio: - if (textOnly) { - return Text( - "${event.sender.calcDisplayname()} sent an audio message", - maxLines: maxLines, - style: TextStyle( - color: textColor, - decoration: - event.redacted ? TextDecoration.lineThrough : null, - ), - ); - } return Container( width: 200, child: RaisedButton( @@ -114,17 +80,6 @@ class MessageContent extends StatelessWidget { ), ); case MessageTypes.Video: - if (textOnly) { - return Text( - "${event.sender.calcDisplayname()} sent a video message", - maxLines: maxLines, - style: TextStyle( - color: textColor, - decoration: - event.redacted ? TextDecoration.lineThrough : null, - ), - ); - } return Container( width: 200, child: RaisedButton( @@ -150,17 +105,6 @@ class MessageContent extends StatelessWidget { ), ); case MessageTypes.File: - 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( @@ -183,217 +127,29 @@ class MessageContent extends StatelessWidget { case MessageTypes.Location: case MessageTypes.None: case MessageTypes.Notice: - final String senderPrefix = - textOnly && event.senderId != event.room.directChatMatrixID - ? event.senderId == Matrix.of(context).client.userID - ? "You: " - : "${event.sender.calcDisplayname()}: " - : ""; - final String body = event.redacted - ? "Redacted by ${event.redactedBecause.sender.calcDisplayname()}" - : senderPrefix + event.body; - if (textOnly) { - return Text( - body, - maxLines: maxLines, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: textColor, - decoration: - event.redacted ? TextDecoration.lineThrough : null, - ), - ); - } - return LinkText( - text: body, - textStyle: TextStyle( - color: textColor, - decoration: event.redacted ? TextDecoration.lineThrough : null, - ), - ); - case MessageTypes.Emote: - if (textOnly) { - return Text( - "* " + event.body, - maxLines: maxLines, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: textColor, - fontStyle: FontStyle.italic, - decoration: - event.redacted ? TextDecoration.lineThrough : null, - ), - ); - } return LinkText( - text: "* " + event.body, + text: event.getLocalizedBody(context), textStyle: TextStyle( color: textColor, - fontStyle: FontStyle.italic, decoration: event.redacted ? TextDecoration.lineThrough : null, ), ); } - return unknown; - case EventTypes.RoomCreate: return Text( - "${event.sender.calcDisplayname()} has created the chat", - maxLines: maxLines, - overflow: textOnly ? TextOverflow.ellipsis : null, - style: TextStyle( - color: textColor, - ), - ); - case EventTypes.RoomAvatar: - return Text( - "${event.sender.calcDisplayname()} has changed the chat avatar", - maxLines: maxLines, - overflow: textOnly ? TextOverflow.ellipsis : null, - style: TextStyle( - color: textColor, - ), - ); - case EventTypes.RoomName: - return Text( - "${event.sender.calcDisplayname()} has changed the chat name to '${event.content['name']}'", - maxLines: maxLines, - overflow: textOnly ? TextOverflow.ellipsis : null, - style: TextStyle( - color: textColor, - ), - ); - case EventTypes.RoomMember: // Display what has changed - String text = "Failed to parse member event"; - // Has the membership changed? - final String newMembership = event.content["membership"] ?? ""; - final String oldMembership = - event.unsigned["prev_content"] is Map - ? event.unsigned["prev_content"]["membership"] ?? "" - : ""; - if (newMembership != oldMembership) { - if (oldMembership == "invite" && newMembership == "join") { - text = - "${event.stateKeyUser.calcDisplayname()} has accepted the invitation"; - } else if (oldMembership == "leave" && newMembership == "join") { - text = - "${event.stateKeyUser.calcDisplayname()} has joined the chat"; - } else if (oldMembership == "join" && newMembership == "ban") { - text = - "${event.sender.calcDisplayname()} has kicked and banned ${event.stateKeyUser.calcDisplayname()}"; - } else if (oldMembership == "join" && - newMembership == "leave" && - event.stateKey != event.senderId) { - text = - "${event.sender.calcDisplayname()} has kicked ${event.stateKeyUser.calcDisplayname()}"; - } else if (oldMembership == "join" && - newMembership == "leave" && - event.stateKey == event.senderId) { - text = "${event.stateKeyUser.calcDisplayname()} has left the room"; - } else if (oldMembership == "invite" && newMembership == "ban") { - text = - "${event.sender.calcDisplayname()} has banned ${event.stateKeyUser.calcDisplayname()}"; - } else if (oldMembership == "leave" && newMembership == "ban") { - text = - "${event.sender.calcDisplayname()} has banned ${event.stateKeyUser.calcDisplayname()}"; - } else if (oldMembership == "ban" && newMembership == "leave") { - text = - "${event.sender.calcDisplayname()} has unbanned ${event.stateKeyUser.calcDisplayname()}"; - } else if (newMembership == "invite") { - text = - "${event.sender.calcDisplayname()} has invited ${event.stateKeyUser.calcDisplayname()}"; - } else if (newMembership == "join") { - text = "${event.stateKeyUser.calcDisplayname()} has joined"; - } - } else if (newMembership == "join") { - final String newAvatar = event.content["avatar_url"] ?? ""; - final String oldAvatar = - event.unsigned["prev_content"] is Map - ? event.unsigned["prev_content"]["avatar_url"] ?? "" - : ""; - - final String newDisplayname = event.content["displayname"] ?? ""; - final String oldDisplayname = - event.unsigned["prev_content"] is Map - ? event.unsigned["prev_content"]["displayname"] ?? "" - : ""; - - // Has the user avatar changed? - if (newAvatar != oldAvatar) { - text = - "${event.stateKeyUser.calcDisplayname()} has changed the profile avatar"; - } - // Has the user avatar changed? - else if (newDisplayname != oldDisplayname) { - text = - "${event.stateKeyUser.calcDisplayname()} has changed the displayname to '$newDisplayname'"; - } - } - - return Text( - text, - maxLines: maxLines, - overflow: textOnly ? TextOverflow.ellipsis : null, - style: TextStyle( - color: textColor, - ), - ); - case EventTypes.RoomTopic: - return Text( - "${event.sender.calcDisplayname()} has changed the chat topic to '${event.content['topic']}'", - maxLines: maxLines, - overflow: textOnly ? TextOverflow.ellipsis : null, - style: TextStyle( - color: textColor, - ), - ); - case EventTypes.RoomPowerLevels: - return Text( - "${event.sender.calcDisplayname()} has changed the power levels of the chat", - maxLines: maxLines, - overflow: textOnly ? TextOverflow.ellipsis : null, - style: TextStyle( - color: textColor, - ), - ); - case EventTypes.HistoryVisibility: - return Text( - "${event.sender.calcDisplayname()} has changed the history visibility of the chat to '${event.content['history_visibility']}'", - maxLines: maxLines, - overflow: textOnly ? TextOverflow.ellipsis : null, - style: TextStyle( - color: textColor, - ), - ); - case EventTypes.RoomJoinRules: - return Text( - "${event.sender.calcDisplayname()} has changed the join rules of the chat to '${event.content['join_rule']}'", - maxLines: maxLines, - overflow: textOnly ? TextOverflow.ellipsis : null, - style: TextStyle( - color: textColor, - ), - ); - case EventTypes.RoomCanonicalAlias: - if (event.content['canonical_alias']?.isEmpty ?? true) { - return Text( - "${event.sender.calcDisplayname()} has removed the canonical alias.", - maxLines: maxLines, - style: TextStyle( - color: textColor, - ), - ); - } - return Text( - "${event.sender.calcDisplayname()} has changed the canonical alias to: ${event.content['canonical_alias']}", - maxLines: maxLines, - overflow: textOnly ? TextOverflow.ellipsis : null, + event.getLocalizedBody(context), style: TextStyle( color: textColor, ), ); default: - return unknown; + return Text( + "${event.sender.calcDisplayname()} sent a ${event.typeKey} event", + style: TextStyle( + color: textColor, + decoration: event.redacted ? TextDecoration.lineThrough : null, + ), + ); } } } diff --git a/lib/utils/event_extension.dart b/lib/utils/event_extension.dart new file mode 100644 index 0000000..90c4ab8 --- /dev/null +++ b/lib/utils/event_extension.dart @@ -0,0 +1,224 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; +import 'room_state_enums_extensions.dart'; + +extension LocalizedBody on Event { + static Set textOnlyMessageTypes = { + MessageTypes.Text, + MessageTypes.Reply, + MessageTypes.Notice, + MessageTypes.Emote, + MessageTypes.None, + }; + + getLocalizedBody(BuildContext context, + {bool withSenderNamePrefix = false, hideQuotes = false}) { + if (this.redacted) { + return "Redacted by ${this.redactedBecause.sender.calcDisplayname()}"; + } + String localizedBody = body; + final String senderName = this.sender.calcDisplayname(); + switch (this.type) { + case EventTypes.Sticker: + localizedBody = "$senderName sent a sticker"; + break; + case EventTypes.Redaction: + localizedBody = "$senderName redacted an event"; + break; + case EventTypes.RoomAliases: + localizedBody = "$senderName changed the room aliases"; + break; + case EventTypes.RoomCanonicalAlias: + localizedBody = "$senderName changed the room invite link"; + break; + case EventTypes.RoomCreate: + localizedBody = "$senderName created the room"; + break; + case EventTypes.RoomJoinRules: + JoinRules joinRules = JoinRules.values.firstWhere( + (r) => + r.toString().replaceAll("JoinRules.", "") == + content["join_rule"], + orElse: () => null); + if (joinRules == null) { + localizedBody = "$senderName changed the join rules"; + } else { + localizedBody = + "$senderName changed the join rules to: ${joinRules.getLocalizedString(context)}"; + } + break; + case EventTypes.RoomMember: + String text = "Failed to parse member event"; + final String targetName = this.stateKeyUser.calcDisplayname(); + // Has the membership changed? + final String newMembership = this.content["membership"] ?? ""; + final String oldMembership = + this.unsigned["prev_content"] is Map + ? this.unsigned["prev_content"]["membership"] ?? "" + : ""; + if (newMembership != oldMembership) { + if (oldMembership == "invite" && newMembership == "join") { + text = "$targetName has accepted the invitation"; + } else if (oldMembership == "leave" && newMembership == "join") { + text = "$targetName has joined the chat"; + } else if (oldMembership == "join" && newMembership == "ban") { + text = "$senderName has kicked and banned $targetName"; + } else if (oldMembership == "join" && + newMembership == "leave" && + this.stateKey != this.senderId) { + text = "$senderName has kicked $targetName"; + } else if (oldMembership == "join" && + newMembership == "leave" && + this.stateKey == this.senderId) { + text = "$senderName has left the room"; + } else if (oldMembership == "invite" && newMembership == "ban") { + text = "$senderName has banned $targetName"; + } else if (oldMembership == "leave" && newMembership == "ban") { + text = "$senderName has banned $targetName"; + } else if (oldMembership == "ban" && newMembership == "leave") { + text = "$senderName has unbanned $targetName"; + } else if (newMembership == "invite") { + text = "$senderName has invited $targetName"; + } else if (newMembership == "join") { + text = "$targetName has joined"; + } + } else if (newMembership == "join") { + final String newAvatar = this.content["avatar_url"] ?? ""; + final String oldAvatar = + this.unsigned["prev_content"] is Map + ? this.unsigned["prev_content"]["avatar_url"] ?? "" + : ""; + + final String newDisplayname = this.content["displayname"] ?? ""; + final String oldDisplayname = + this.unsigned["prev_content"] is Map + ? this.unsigned["prev_content"]["displayname"] ?? "" + : ""; + + // Has the user avatar changed? + if (newAvatar != oldAvatar) { + text = "$targetName has changed the profile avatar"; + } + // Has the user avatar changed? + else if (newDisplayname != oldDisplayname) { + text = + "${this.stateKeyUser.id} has changed the displayname to '$newDisplayname'"; + } + } + localizedBody = text; + break; + case EventTypes.RoomPowerLevels: + localizedBody = "$senderName changed the group permissions"; + break; + case EventTypes.RoomName: + localizedBody = + "$senderName changed the group name to: '${content["name"]}'"; + break; + case EventTypes.RoomTopic: + localizedBody = + "$senderName changed the group name to: '${content["topic"]}'"; + break; + case EventTypes.RoomAvatar: + localizedBody = "$senderName changed the group avatar"; + break; + case EventTypes.GuestAccess: + GuestAccess guestAccess = GuestAccess.values.firstWhere( + (r) => + r.toString().replaceAll("GuestAccess.", "") == + content["guest_access"], + orElse: () => null); + if (guestAccess == null) { + localizedBody = "$senderName changed the guest access rules"; + } else { + localizedBody = + "$senderName changed the guest access rules to: ${guestAccess.getLocalizedString(context)}"; + } + break; + case EventTypes.HistoryVisibility: + HistoryVisibility historyVisibility = HistoryVisibility.values + .firstWhere( + (r) => + r.toString().replaceAll("HistoryVisibility.", "") == + content["history_visibility"], + orElse: () => null); + if (historyVisibility == null) { + localizedBody = "$senderName changed the history visibility"; + } else { + localizedBody = + "$senderName changed the history visibility to: ${historyVisibility.getLocalizedString(context)}"; + } + break; + case EventTypes.Encryption: + localizedBody = "$senderName activated end to end encryption"; + break; + case EventTypes.Encrypted: + localizedBody = "Could not decrypt message"; + break; + case EventTypes.CallInvite: + localizedBody = body; + break; + case EventTypes.CallAnswer: + localizedBody = body; + break; + case EventTypes.CallCandidates: + localizedBody = body; + break; + case EventTypes.CallHangup: + localizedBody = body; + break; + case EventTypes.Unknown: + localizedBody = body; + break; + case EventTypes.Message: + switch (this.messageType) { + case MessageTypes.Image: + localizedBody = "$senderName sent a picture"; + break; + case MessageTypes.File: + localizedBody = "$senderName sent a file"; + break; + case MessageTypes.Audio: + localizedBody = "$senderName sent an audio"; + break; + case MessageTypes.Video: + localizedBody = "$senderName sent a video"; + break; + case MessageTypes.Location: + localizedBody = "$senderName shared the location"; + break; + case MessageTypes.Sticker: + localizedBody = "$senderName sent a sticker"; + break; + case MessageTypes.Emote: + localizedBody = "* $body"; + break; + case MessageTypes.Text: + case MessageTypes.Notice: + case MessageTypes.None: + case MessageTypes.Reply: + localizedBody = body; + break; + } + break; + default: + localizedBody = + "Unknown event '${this.type.toString().replaceAll("EventTypes.", "")}'"; + } + + // Add the sender name prefix + if (withSenderNamePrefix && + this.type == EventTypes.Message && + textOnlyMessageTypes.contains(this.messageType)) { + localizedBody = "$senderName: $localizedBody"; + } + + // Hide quotes + if (hideQuotes) { + List lines = localizedBody.split("\n"); + lines.removeWhere((s) => s.startsWith("> ")); + localizedBody = lines.join("\n"); + } + + return localizedBody; + } +} diff --git a/lib/utils/room_extension.dart b/lib/utils/room_extension.dart new file mode 100644 index 0000000..c415c78 --- /dev/null +++ b/lib/utils/room_extension.dart @@ -0,0 +1,13 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; + +extension LocalizedRoomDisplayname on Room { + String getLocalizedDisplayname(BuildContext context) { + if ((this.name?.isEmpty ?? true) && + (this.canonicalAlias?.isEmpty ?? true) && + !this.isDirectChat) { + return "Group with ${this.displayname}"; + } + return this.displayname; + } +} diff --git a/lib/utils/room_name_calculator.dart b/lib/utils/room_name_calculator.dart deleted file mode 100644 index 88dce92..0000000 --- a/lib/utils/room_name_calculator.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:famedlysdk/famedlysdk.dart'; - -class RoomNameCalculator { - final Room room; - - const RoomNameCalculator(this.room); - - String get name { - if ((room.name?.isEmpty ?? true) && - (room.canonicalAlias?.isEmpty ?? true) && - !room.isDirectChat) { - return "Group with ${room.displayname}"; - } - return room.displayname; - } -} diff --git a/lib/utils/room_state_enums_extensions.dart b/lib/utils/room_state_enums_extensions.dart new file mode 100644 index 0000000..d3163a2 --- /dev/null +++ b/lib/utils/room_state_enums_extensions.dart @@ -0,0 +1,45 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; + +extension HistoryVisibilityDisplayString on HistoryVisibility { + String getLocalizedString(BuildContext context) { + switch (this) { + case HistoryVisibility.invited: + return "From the invitation"; + case HistoryVisibility.joined: + return "From joining"; + case HistoryVisibility.shared: + return "Visible for all participants"; + case HistoryVisibility.world_readable: + return "Visible for everyone"; + default: + return this.toString().replaceAll("HistoryVisibility.", ""); + } + } +} + +extension GuestAccessDisplayString on GuestAccess { + String getLocalizedString(BuildContext context) { + switch (this) { + case GuestAccess.can_join: + return "Guests can join"; + case GuestAccess.forbidden: + return "Guests are forbidden"; + default: + return this.toString().replaceAll("GuestAccess.", ""); + } + } +} + +extension JoinRulesDisplayString on JoinRules { + String getLocalizedString(BuildContext context) { + switch (this) { + case JoinRules.public: + return "Anyone can join"; + case JoinRules.invite: + return "Invited users only"; + default: + return this.toString().replaceAll("JoinRules.", ""); + } + } +} diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 6a0beae..92c8d5c 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -7,7 +7,7 @@ import 'package:fluffychat/components/adaptive_page_layout.dart'; import 'package:fluffychat/components/chat_settings_popup_menu.dart'; import 'package:fluffychat/components/list_items/message.dart'; import 'package:fluffychat/components/matrix.dart'; -import 'package:fluffychat/utils/room_name_calculator.dart'; +import 'package:fluffychat/utils/room_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -187,7 +187,7 @@ class _ChatState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(RoomNameCalculator(room).name), + Text(room.getLocalizedDisplayname(context)), AnimatedContainer( duration: Duration(milliseconds: 500), height: typingText.isEmpty ? 0 : 20, @@ -237,14 +237,14 @@ class _ChatState extends State { controller: _scrollController, itemBuilder: (BuildContext context, int i) => i == 0 ? AnimatedContainer( - height: seenByText.isEmpty ? 0 : 36, + height: seenByText.isEmpty ? 0 : 24, duration: seenByText.isEmpty ? Duration(milliseconds: 0) : Duration(milliseconds: 500), alignment: timeline.events.first.senderId == client.userID - ? Alignment.centerRight - : Alignment.centerLeft, + ? Alignment.topRight + : Alignment.topLeft, child: Text( seenByText, maxLines: 1, @@ -253,7 +253,11 @@ class _ChatState extends State { color: Theme.of(context).primaryColor, ), ), - padding: EdgeInsets.all(8), + padding: EdgeInsets.only( + left: 8, + right: 8, + bottom: 8, + ), ) : Message(timeline.events[i - 1], nextEvent: diff --git a/lib/views/chat_details.dart b/lib/views/chat_details.dart index 3311d39..de87889 100644 --- a/lib/views/chat_details.dart +++ b/lib/views/chat_details.dart @@ -8,13 +8,14 @@ import 'package:fluffychat/components/content_banner.dart'; import 'package:fluffychat/components/list_items/participant_list_item.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/utils/app_route.dart'; -import 'package:fluffychat/utils/room_name_calculator.dart'; +import 'package:fluffychat/utils/room_extension.dart'; +import 'package:fluffychat/utils/room_state_enums_extensions.dart'; import 'package:fluffychat/views/chat_list.dart'; import 'package:fluffychat/views/invitation_selection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:link_text/link_text.dart'; +import 'package:share/share.dart'; import 'package:toast/toast.dart'; class ChatDetails extends StatefulWidget { @@ -43,6 +44,40 @@ class _ChatDetailsState extends State { } } + void setCanonicalAliasAction(context, s) async { + final String domain = widget.room.client.userID.split(":")[1]; + final String canonicalAlias = "%23" + s + "%3A" + domain; + final Event aliasEvent = widget.room.getState("m.room.aliases", domain); + final List aliases = + aliasEvent != null ? aliasEvent.content["aliases"] ?? [] : []; + if (aliases.indexWhere((s) => s == canonicalAlias) == -1) { + List newAliases = List.from(aliases); + newAliases.add(canonicalAlias); + final response = await Matrix.of(context).tryRequestWithLoadingDialog( + widget.room.client.jsonRequest( + type: HTTPType.GET, + action: "/client/r0/directory/room/$canonicalAlias", + ), + ); + if (response == false) { + final success = await Matrix.of(context).tryRequestWithLoadingDialog( + widget.room.client.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/directory/room/$canonicalAlias", + data: {"room_id": widget.room.id}), + ); + if (success == false) return; + } + } + await Matrix.of(context).tryRequestWithLoadingDialog( + widget.room.client.jsonRequest( + type: HTTPType.PUT, + action: + "/client/r0/rooms/${widget.room.id}/state/m.room.canonical_alias", + data: {"alias": "#$s:$domain"}), + ); + } + void setTopicAction(BuildContext context, String displayname) async { setState(() => topicEditMode = false); final MatrixState matrix = Matrix.of(context); @@ -117,8 +152,16 @@ class _ChatDetailsState extends State { ), secondScaffold: Scaffold( appBar: AppBar( - title: Text(RoomNameCalculator(widget.room).name), - actions: [ChatSettingsPopupMenu(widget.room, false)], + title: Text(widget.room.getLocalizedDisplayname(context)), + actions: [ + if (widget.room.canonicalAlias?.isNotEmpty ?? false) + IconButton( + icon: Icon(Icons.share), + onPressed: () => Share.share( + "https://matrix.to/#/${widget.room.canonicalAlias}"), + ), + ChatSettingsPopupMenu(widget.room, false) + ], ), body: ListView.builder( itemCount: members.length + 1 + (canRequestMoreMembers ? 1 : 0), @@ -126,28 +169,11 @@ class _ChatDetailsState extends State { ? Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ContentBanner(widget.room.avatar), + ContentBanner(widget.room.avatar, + onEdit: widget.room.canSendEvent("m.room.avatar") + ? () => setAvatarAction(context) + : null), Divider(height: 1), - if (widget.room.canSendEvent("m.room.avatar") && !kIsWeb) - ListTile( - title: Text("Upload group avatar"), - leading: Icon(Icons.camera), - onTap: () => setAvatarAction(context), - ), - if (widget.room.canSendEvent("m.room.name")) - ListTile( - leading: Icon(Icons.edit), - title: TextField( - textInputAction: TextInputAction.done, - onSubmitted: (s) => setDisplaynameAction(context, s), - decoration: InputDecoration( - border: InputBorder.none, - labelText: "Set group name", - labelStyle: TextStyle(color: Colors.black), - hintText: (RoomNameCalculator(widget.room).name), - ), - ), - ), topicEditMode ? ListTile( title: TextField( @@ -168,6 +194,13 @@ class _ChatDetailsState extends State { ), ) : ListTile( + leading: widget.room.canSendEvent("m.room.topic") + ? CircleAvatar( + backgroundColor: Colors.white, + foregroundColor: Colors.grey, + child: Icon(Icons.edit), + ) + : null, title: Text("Group description:", style: TextStyle( color: Theme.of(context).primaryColor, @@ -185,6 +218,173 @@ class _ChatDetailsState extends State { ? () => setState(() => topicEditMode = true) : null, ), + Divider(thickness: 8), + ListTile( + title: Text( + "Settings", + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ), + if (widget.room.canSendEvent("m.room.name")) + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.white, + foregroundColor: Colors.grey, + child: Icon(Icons.people), + ), + title: TextField( + textInputAction: TextInputAction.done, + onSubmitted: (s) => setDisplaynameAction(context, s), + decoration: InputDecoration( + border: InputBorder.none, + labelText: "Change the name of the group", + labelStyle: TextStyle(color: Colors.black), + hintText: + widget.room.getLocalizedDisplayname(context), + ), + ), + ), + if (widget.room.canSendEvent("m.room.canonical_alias") && + widget.room.joinRules == JoinRules.public) + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.white, + foregroundColor: Colors.grey, + child: Icon(Icons.link), + ), + title: TextField( + textInputAction: TextInputAction.done, + onSubmitted: (s) => + setCanonicalAliasAction(context, s), + decoration: InputDecoration( + border: InputBorder.none, + labelText: "Set invitation link", + labelStyle: TextStyle(color: Colors.black), + hintText: widget.room.canonicalAlias + ?.replaceAll("#", "") ?? + "alias", + prefixText: "#", + suffixText: widget.room.client.userID.split(":")[1], + ), + ), + ), + PopupMenuButton( + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.white, + foregroundColor: Colors.grey, + child: Icon(Icons.public)), + title: Text("Who is allowed to join this group"), + subtitle: Text( + widget.room.joinRules.getLocalizedString(context), + ), + ), + onSelected: (JoinRules joinRule) => + Matrix.of(context).tryRequestWithLoadingDialog( + widget.room.setJoinRules(joinRule), + ), + itemBuilder: (BuildContext context) => + >[ + if (widget.room.canChangeJoinRules) + PopupMenuItem( + value: JoinRules.public, + child: Text( + JoinRules.public.getLocalizedString(context)), + ), + if (widget.room.canChangeJoinRules) + PopupMenuItem( + value: JoinRules.invite, + child: Text( + JoinRules.invite.getLocalizedString(context)), + ), + ], + ), + PopupMenuButton( + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.white, + foregroundColor: Colors.grey, + child: Icon(Icons.visibility), + ), + title: Text("Visibility of the chat history"), + subtitle: Text( + widget.room.historyVisibility + .getLocalizedString(context), + ), + ), + onSelected: (HistoryVisibility historyVisibility) => + Matrix.of(context).tryRequestWithLoadingDialog( + widget.room.setHistoryVisibility(historyVisibility), + ), + itemBuilder: (BuildContext context) => + >[ + if (widget.room.canChangeHistoryVisibility) + PopupMenuItem( + value: HistoryVisibility.invited, + child: Text(HistoryVisibility.invited + .getLocalizedString(context)), + ), + if (widget.room.canChangeHistoryVisibility) + PopupMenuItem( + value: HistoryVisibility.joined, + child: Text(HistoryVisibility.joined + .getLocalizedString(context)), + ), + if (widget.room.canChangeHistoryVisibility) + PopupMenuItem( + value: HistoryVisibility.shared, + child: Text(HistoryVisibility.shared + .getLocalizedString(context)), + ), + if (widget.room.canChangeHistoryVisibility) + PopupMenuItem( + value: HistoryVisibility.world_readable, + child: Text(HistoryVisibility.world_readable + .getLocalizedString(context)), + ), + ], + ), + if (widget.room.joinRules == JoinRules.public) + PopupMenuButton( + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.white, + foregroundColor: Colors.grey, + child: Icon(Icons.info_outline), + ), + title: Text("Are guest users allowed to join"), + subtitle: Text( + widget.room.guestAccess.getLocalizedString(context), + ), + ), + onSelected: (GuestAccess guestAccess) => + Matrix.of(context).tryRequestWithLoadingDialog( + widget.room.setGuestAccess(guestAccess), + ), + itemBuilder: (BuildContext context) => + >[ + if (widget.room.canChangeGuestAccess) + PopupMenuItem( + value: GuestAccess.can_join, + child: Text( + GuestAccess.can_join + .getLocalizedString(context), + ), + ), + if (widget.room.canChangeGuestAccess) + PopupMenuItem( + value: GuestAccess.forbidden, + child: Text( + GuestAccess.forbidden + .getLocalizedString(context), + ), + ), + ], + ), + Divider(thickness: 8), ListTile( title: Text( "$actualMembersCount participant" + diff --git a/lib/views/settings.dart b/lib/views/settings.dart index 3064b25..701fce4 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -7,7 +7,6 @@ import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/utils/app_route.dart'; import 'package:fluffychat/views/chat_list.dart'; import 'package:fluffychat/views/sign_up.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:toast/toast.dart'; @@ -44,11 +43,7 @@ class _SettingsState extends State { final MatrixState matrix = Matrix.of(context); final Map success = await matrix.tryRequestWithLoadingDialog( - matrix.client.jsonRequest( - type: HTTPType.PUT, - action: "/client/r0/profile/${matrix.client.userID}/displayname", - data: {"displayname": displayname}, - ), + matrix.client.setDisplayname(displayname), ); if (success != null && success.isEmpty) { Toast.show( @@ -110,23 +105,8 @@ class _SettingsState extends State { profile?.avatarUrl ?? MxContent(""), defaultIcon: Icons.account_circle, loading: profile == null, + onEdit: () => setAvatarAction(context), ), - ListTile( - title: Text( - "Profile", - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - ), - ), - ), - kIsWeb - ? Container() - : ListTile( - title: Text("Upload avatar"), - leading: Icon(Icons.camera), - onTap: () => setAvatarAction(context), - ), ListTile( leading: Icon(Icons.edit), title: TextField( @@ -141,6 +121,7 @@ class _SettingsState extends State { ), ), ), + Divider(thickness: 8), ListTile( title: Text( "About", @@ -170,6 +151,7 @@ class _SettingsState extends State { title: Text("Source code"), onTap: () => launch( "https://gitlab.com/ChristianPauly/fluffychat-flutter")), + Divider(thickness: 8), ListTile( title: Text( "Logout", diff --git a/pubspec.lock b/pubspec.lock index dd1ea0a..3915206 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -82,8 +82,8 @@ packages: dependency: "direct main" description: path: "." - ref: c8633111e5f016cc3dd95f644a4e8767be5559f6 - resolved-ref: c8633111e5f016cc3dd95f644a4e8767be5559f6 + ref: "2545995bbe96a1d96fe176ab666f4dd03d591aa6" + resolved-ref: "2545995bbe96a1d96fe176ab666f4dd03d591aa6" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" @@ -263,6 +263,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + share: + dependency: "direct main" + description: + name: share + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3+5" sky_engine: dependency: transitive description: flutter @@ -402,5 +409,5 @@ packages: source: hosted version: "2.2.0" sdks: - dart: ">=2.6.0 <3.0.0" + dart: ">=2.7.0 <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 709f787..2e1d835 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: c8633111e5f016cc3dd95f644a4e8767be5559f6 + ref: 2545995bbe96a1d96fe176ab666f4dd03d591aa6 localstorage: ^3.0.1+4 bubble: ^1.1.9+1 @@ -44,6 +44,7 @@ dependencies: link_text: ^0.1.1 path_provider: ^1.5.1 webview_flutter: ^0.3.19+4 + share: ^0.6.3+5 dev_dependencies: flutter_test: