From 9944844cc3606f74626356874560979750c0cae1 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 6 May 2020 10:13:30 +0000 Subject: [PATCH] Implement localized String represantions --- .gitlab-ci.yml | 6 +- lib/famedlysdk.dart | 1 + lib/src/event.dart | 231 ++++++++++++++++++++++++ lib/src/room.dart | 22 +++ lib/src/utils/matrix_localizations.dart | 152 ++++++++++++++++ pubspec.lock | 23 ++- pubspec.yaml | 6 +- 7 files changed, 427 insertions(+), 14 deletions(-) create mode 100644 lib/src/utils/matrix_localizations.dart diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6deeb88..875daec 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,10 +14,10 @@ coverage: script: - apt update - apt install -y curl gnupg2 git - - curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - - - curl https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list + - curl https://storage.googleapis.com/dart-archive/channels/stable/release/2.7.2/linux_packages/dart_2.7.2-1_amd64.deb > dart.deb + - apt install -y ./dart.deb - apt update - - apt install -y dart chromium lcov libolm3 + - apt install -y chromium lcov libolm3 - ln -s /usr/lib/dart/bin/pub /usr/bin/ - useradd -m test - chown -R 'test:' '.' diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart index a3cd2a1..ae5cf13 100644 --- a/lib/famedlysdk.dart +++ b/lib/famedlysdk.dart @@ -31,6 +31,7 @@ export 'package:famedlysdk/src/utils/matrix_exception.dart'; export 'package:famedlysdk/src/utils/matrix_file.dart'; export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart'; export 'package:famedlysdk/src/utils/uri_extension.dart'; +export 'package:famedlysdk/src/utils/matrix_localizations.dart'; export 'package:famedlysdk/src/utils/open_id_credentials.dart'; export 'package:famedlysdk/src/utils/profile.dart'; export 'package:famedlysdk/src/utils/public_rooms_response.dart'; diff --git a/lib/src/event.dart b/lib/src/event.dart index 3157af9..d5faf7e 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -28,6 +28,7 @@ import 'package:famedlysdk/src/utils/receipt.dart'; import 'package:http/http.dart' as http; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import './room.dart'; +import 'utils/matrix_localizations.dart'; /// All data exchanged over Matrix is expressed as an "event". Typically each client action (e.g. sending a message) correlates with exactly one event. class Event { @@ -503,6 +504,236 @@ class Event { } return MatrixFile(bytes: uint8list, path: '/$body'); } + + /// Returns a localized String representation of this event. For a + /// room list you may find [withSenderNamePrefix] useful. Set [hideReply] to + /// crop all lines starting with '>'. + String getLocalizedBody(MatrixLocalizations i18n, + {bool withSenderNamePrefix = false, bool hideReply = false}) { + if (redacted) { + return i18n.removedBy(redactedBecause.sender.calcDisplayname()); + } + var localizedBody = body; + final senderName = sender.calcDisplayname(); + switch (type) { + case EventTypes.Sticker: + localizedBody = i18n.sentASticker(senderName); + break; + case EventTypes.Redaction: + localizedBody = i18n.redactedAnEvent(senderName); + break; + case EventTypes.RoomAliases: + localizedBody = i18n.changedTheRoomAliases(senderName); + break; + case EventTypes.RoomCanonicalAlias: + localizedBody = i18n.changedTheRoomInvitationLink(senderName); + break; + case EventTypes.RoomCreate: + localizedBody = i18n.createdTheChat(senderName); + break; + case EventTypes.RoomJoinRules: + var joinRules = JoinRules.values.firstWhere( + (r) => + r.toString().replaceAll('JoinRules.', '') == + content['join_rule'], + orElse: () => null); + if (joinRules == null) { + localizedBody = i18n.changedTheJoinRules(senderName); + } else { + localizedBody = i18n.changedTheJoinRulesTo( + senderName, joinRules.getLocalizedString(i18n)); + } + break; + case EventTypes.RoomMember: + var text = 'Failed to parse member event'; + final targetName = stateKeyUser.calcDisplayname(); + // Has the membership changed? + final newMembership = content['membership'] ?? ''; + final oldMembership = unsigned['prev_content'] is Map + ? unsigned['prev_content']['membership'] ?? '' + : ''; + if (newMembership != oldMembership) { + if (oldMembership == 'invite' && newMembership == 'join') { + text = i18n.acceptedTheInvitation(targetName); + } else if (oldMembership == 'invite' && newMembership == 'leave') { + if (stateKey == senderId) { + text = i18n.rejectedTheInvitation(targetName); + } else { + text = i18n.hasWithdrawnTheInvitationFor(senderName, targetName); + } + } else if (oldMembership == 'leave' && newMembership == 'join') { + text = i18n.joinedTheChat(targetName); + } else if (oldMembership == 'join' && newMembership == 'ban') { + text = i18n.kickedAndBanned(senderName, targetName); + } else if (oldMembership == 'join' && + newMembership == 'leave' && + stateKey != senderId) { + text = i18n.kicked(senderName, targetName); + } else if (oldMembership == 'join' && + newMembership == 'leave' && + stateKey == senderId) { + text = i18n.userLeftTheChat(targetName); + } else if (oldMembership == 'invite' && newMembership == 'ban') { + text = i18n.bannedUser(senderName, targetName); + } else if (oldMembership == 'leave' && newMembership == 'ban') { + text = i18n.bannedUser(senderName, targetName); + } else if (oldMembership == 'ban' && newMembership == 'leave') { + text = i18n.unbannedUser(senderName, targetName); + } else if (newMembership == 'invite') { + text = i18n.invitedUser(senderName, targetName); + } else if (newMembership == 'join') { + text = i18n.joinedTheChat(targetName); + } + } else if (newMembership == 'join') { + final newAvatar = content['avatar_url'] ?? ''; + final oldAvatar = unsigned['prev_content'] is Map + ? unsigned['prev_content']['avatar_url'] ?? '' + : ''; + + final newDisplayname = content['displayname'] ?? ''; + final oldDisplayname = + unsigned['prev_content'] is Map + ? unsigned['prev_content']['displayname'] ?? '' + : ''; + + // Has the user avatar changed? + if (newAvatar != oldAvatar) { + text = i18n.changedTheProfileAvatar(targetName); + } + // Has the user avatar changed? + else if (newDisplayname != oldDisplayname) { + text = i18n.changedTheDisplaynameTo(targetName, newDisplayname); + } + } + localizedBody = text; + break; + case EventTypes.RoomPowerLevels: + localizedBody = i18n.changedTheChatPermissions(senderName); + break; + case EventTypes.RoomName: + localizedBody = i18n.changedTheChatNameTo(senderName, content['name']); + break; + case EventTypes.RoomTopic: + localizedBody = + i18n.changedTheChatDescriptionTo(senderName, content['topic']); + break; + case EventTypes.RoomAvatar: + localizedBody = i18n.changedTheChatAvatar(senderName); + break; + case EventTypes.GuestAccess: + var guestAccess = GuestAccess.values.firstWhere( + (r) => + r.toString().replaceAll('GuestAccess.', '') == + content['guest_access'], + orElse: () => null); + if (guestAccess == null) { + localizedBody = i18n.changedTheGuestAccessRules(senderName); + } else { + localizedBody = i18n.changedTheGuestAccessRulesTo( + senderName, guestAccess.getLocalizedString(i18n)); + } + break; + case EventTypes.HistoryVisibility: + var historyVisibility = HistoryVisibility.values.firstWhere( + (r) => + r.toString().replaceAll('HistoryVisibility.', '') == + content['history_visibility'], + orElse: () => null); + if (historyVisibility == null) { + localizedBody = i18n.changedTheHistoryVisibility(senderName); + } else { + localizedBody = i18n.changedTheHistoryVisibilityTo( + senderName, historyVisibility.getLocalizedString(i18n)); + } + break; + case EventTypes.Encryption: + localizedBody = i18n.activatedEndToEndEncryption(senderName); + if (!room.client.encryptionEnabled) { + localizedBody += '. ' + i18n.needPantalaimonWarning; + } + break; + case EventTypes.Encrypted: + case EventTypes.Message: + switch (messageType) { + case MessageTypes.Image: + localizedBody = i18n.sentAPicture(senderName); + break; + case MessageTypes.File: + localizedBody = i18n.sentAFile(senderName); + break; + case MessageTypes.Audio: + localizedBody = i18n.sentAnAudio(senderName); + break; + case MessageTypes.Video: + localizedBody = i18n.sentAVideo(senderName); + break; + case MessageTypes.Location: + localizedBody = i18n.sharedTheLocation(senderName); + break; + case MessageTypes.Sticker: + localizedBody = i18n.sentASticker(senderName); + break; + case MessageTypes.Emote: + localizedBody = '* $body'; + break; + case MessageTypes.BadEncrypted: + String errorText; + switch (body) { + case DecryptError.CHANNEL_CORRUPTED: + errorText = i18n.channelCorruptedDecryptError + '.'; + break; + case DecryptError.NOT_ENABLED: + errorText = i18n.encryptionNotEnabled + '.'; + break; + case DecryptError.UNKNOWN_ALGORITHM: + errorText = i18n.unknownEncryptionAlgorithm + '.'; + break; + case DecryptError.UNKNOWN_SESSION: + errorText = i18n.noPermission + '.'; + break; + default: + errorText = body; + break; + } + localizedBody = i18n.couldNotDecryptMessage(errorText); + break; + case MessageTypes.Text: + case MessageTypes.Notice: + case MessageTypes.None: + case MessageTypes.Reply: + localizedBody = body; + break; + } + break; + default: + localizedBody = i18n.unknownEvent(typeKey); + } + + // Hide reply fallback + if (hideReply) { + localizedBody = localizedBody.replaceFirst( + RegExp(r'^>( \*)? <[^>]+>[^\n\r]+\r?\n(> [^\n]*\r?\n)*\r?\n'), ''); + } + + // Add the sender name prefix + if (withSenderNamePrefix && + type == EventTypes.Message && + textOnlyMessageTypes.contains(messageType)) { + final senderNameOrYou = + senderId == room.client.userID ? i18n.you : senderName; + localizedBody = '$senderNameOrYou: $localizedBody'; + } + + return localizedBody; + } + + static const Set textOnlyMessageTypes = { + MessageTypes.Text, + MessageTypes.Reply, + MessageTypes.Notice, + MessageTypes.Emote, + MessageTypes.None, + }; } enum MessageTypes { diff --git a/lib/src/room.dart b/lib/src/room.dart index d45cd15..b91664d 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -40,6 +40,7 @@ import 'package:olm/olm.dart' as olm; import './user.dart'; import 'timeline.dart'; +import 'utils/matrix_localizations.dart'; import 'utils/states_map.dart'; enum PushRuleState { notify, mentions_only, dont_notify } @@ -279,6 +280,27 @@ class Room { ? states['m.room.name'].content['name'] : ''; + /// Returns a localized displayname for this server. If the room is a groupchat + /// without a name, then it will return the localized version of 'Group with Alice' instead + /// of just 'Alice' to make it different to a direct chat. + /// Empty chats will become the localized version of 'Empty Chat'. + /// This method requires a localization class which implements [MatrixLocalizations] + String getLocalizedDisplayname(MatrixLocalizations i18n) { + if ((name?.isEmpty ?? true) && + (canonicalAlias?.isEmpty ?? true) && + !isDirectChat && + (mHeroes != null && mHeroes.isNotEmpty)) { + return i18n.groupWith(displayname); + } + if ((name?.isEmpty ?? true) && + (canonicalAlias?.isEmpty ?? true) && + !isDirectChat && + (mHeroes?.isEmpty ?? true)) { + return i18n.emptyChat; + } + return displayname; + } + /// The topic of the room if set by a participant. String get topic => states['m.room.topic'] != null ? states['m.room.topic'].content['topic'] diff --git a/lib/src/utils/matrix_localizations.dart b/lib/src/utils/matrix_localizations.dart new file mode 100644 index 0000000..bffe88d --- /dev/null +++ b/lib/src/utils/matrix_localizations.dart @@ -0,0 +1,152 @@ +import '../room.dart'; + +abstract class MatrixLocalizations { + String get emptyChat; + + String get invitedUsersOnly; + + String get fromTheInvitation; + + String get fromJoining; + + String get visibleForAllParticipants; + + String get visibleForEveryone; + + String get guestsCanJoin; + + String get guestsAreForbidden; + + String get anyoneCanJoin; + + String get needPantalaimonWarning; + + String get channelCorruptedDecryptError; + + String get encryptionNotEnabled; + + String get unknownEncryptionAlgorithm; + + String get noPermission; + + String get you; + + String groupWith(String displayname); + + String removedBy(String calcDisplayname); + + String sentASticker(String senderName); + + String redactedAnEvent(String senderName); + + String changedTheRoomAliases(String senderName); + + String changedTheRoomInvitationLink(String senderName); + + String createdTheChat(String senderName); + + String changedTheJoinRules(String senderName); + + String changedTheJoinRulesTo(String senderName, String localizedString); + + String acceptedTheInvitation(String targetName); + + String rejectedTheInvitation(String targetName); + + String hasWithdrawnTheInvitationFor(String senderName, String targetName); + + String joinedTheChat(String targetName); + + String kickedAndBanned(String senderName, String targetName); + + String kicked(String senderName, String targetName); + + String userLeftTheChat(String targetName); + + String bannedUser(String senderName, String targetName); + + String unbannedUser(String senderName, String targetName); + + String invitedUser(String senderName, String targetName); + + String changedTheProfileAvatar(String targetName); + + String changedTheDisplaynameTo(String targetName, String newDisplayname); + + String changedTheChatPermissions(String senderName); + + String changedTheChatNameTo(String senderName, String content); + + String changedTheChatDescriptionTo(String senderName, String content); + + String changedTheChatAvatar(String senderName); + + String changedTheGuestAccessRules(String senderName); + + String changedTheGuestAccessRulesTo( + String senderName, String localizedString); + + String changedTheHistoryVisibility(String senderName); + + String changedTheHistoryVisibilityTo( + String senderName, String localizedString); + + String activatedEndToEndEncryption(String senderName); + + String sentAPicture(String senderName); + + String sentAFile(String senderName); + + String sentAnAudio(String senderName); + + String sentAVideo(String senderName); + + String sharedTheLocation(String senderName); + + String couldNotDecryptMessage(String errorText); + + String unknownEvent(String typeKey); +} + +extension HistoryVisibilityDisplayString on HistoryVisibility { + String getLocalizedString(MatrixLocalizations i18n) { + switch (this) { + case HistoryVisibility.invited: + return i18n.fromTheInvitation; + case HistoryVisibility.joined: + return i18n.fromJoining; + case HistoryVisibility.shared: + return i18n.visibleForAllParticipants; + case HistoryVisibility.world_readable: + return i18n.visibleForEveryone; + default: + return toString().replaceAll('HistoryVisibility.', ''); + } + } +} + +extension GuestAccessDisplayString on GuestAccess { + String getLocalizedString(MatrixLocalizations i18n) { + switch (this) { + case GuestAccess.can_join: + return i18n.guestsCanJoin; + case GuestAccess.forbidden: + return i18n.guestsAreForbidden; + default: + return toString().replaceAll('GuestAccess.', ''); + } + } +} + +extension JoinRulesDisplayString on JoinRules { + String getLocalizedString(MatrixLocalizations i18n) { + switch (this) { + case JoinRules.public: + return i18n.anyoneCanJoin; + case JoinRules.invite: + return i18n.invitedUsersOnly; + default: + return toString().replaceAll('JoinRules.', ''); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 9c3fd39..bcd7fd7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,7 +133,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "0.13.3" + version: "0.13.9" crypto: dependency: transitive description: @@ -203,7 +203,7 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.0+4" + version: "0.12.1" http_multi_server: dependency: transitive description: @@ -296,7 +296,7 @@ packages: name: mime_type url: "https://pub.dartlang.org" source: hosted - version: "0.2.4" + version: "0.3.0" multi_server_socket: dependency: transitive description: @@ -326,7 +326,7 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.9.3" package_resolver: dependency: transitive description: @@ -424,7 +424,7 @@ packages: name: source_map_stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.1.5" + version: "2.0.0" source_maps: dependency: transitive description: @@ -480,21 +480,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.11.1" + version: "1.14.3" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.13" + version: "0.2.15" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.2.18" + version: "0.3.4" timing: dependency: transitive description: @@ -537,6 +537,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.13" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.2" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bb00d5f..445067d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,10 +8,10 @@ environment: sdk: ">=2.7.0 <3.0.0" dependencies: - http: ^0.12.0+4 - mime_type: ^0.2.4 + http: ^0.12.1 + mime_type: ^0.3.0 canonical_json: ^1.0.0 - image: ^2.1.4 + image: ^2.1.12 olm: git: