Implement localized String represantions

This commit is contained in:
Christian Pauly 2020-05-06 10:13:30 +00:00
parent c2bc27abfd
commit 9944844cc3
7 changed files with 427 additions and 14 deletions

View file

@ -14,10 +14,10 @@ coverage:
script: script:
- apt update - apt update
- apt install -y curl gnupg2 git - 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/dart-archive/channels/stable/release/2.7.2/linux_packages/dart_2.7.2-1_amd64.deb > dart.deb
- curl https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list - apt install -y ./dart.deb
- apt update - apt update
- apt install -y dart chromium lcov libolm3 - apt install -y chromium lcov libolm3
- ln -s /usr/lib/dart/bin/pub /usr/bin/ - ln -s /usr/lib/dart/bin/pub /usr/bin/
- useradd -m test - useradd -m test
- chown -R 'test:' '.' - chown -R 'test:' '.'

View file

@ -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_file.dart';
export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart'; export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart';
export 'package:famedlysdk/src/utils/uri_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/open_id_credentials.dart';
export 'package:famedlysdk/src/utils/profile.dart'; export 'package:famedlysdk/src/utils/profile.dart';
export 'package:famedlysdk/src/utils/public_rooms_response.dart'; export 'package:famedlysdk/src/utils/public_rooms_response.dart';

View file

@ -28,6 +28,7 @@ import 'package:famedlysdk/src/utils/receipt.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
import './room.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. /// 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 { class Event {
@ -503,6 +504,236 @@ class Event {
} }
return MatrixFile(bytes: uint8list, path: '/$body'); 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<String, dynamic>
? 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<String, dynamic>
? unsigned['prev_content']['avatar_url'] ?? ''
: '';
final newDisplayname = content['displayname'] ?? '';
final oldDisplayname =
unsigned['prev_content'] is Map<String, dynamic>
? 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<MessageTypes> textOnlyMessageTypes = {
MessageTypes.Text,
MessageTypes.Reply,
MessageTypes.Notice,
MessageTypes.Emote,
MessageTypes.None,
};
} }
enum MessageTypes { enum MessageTypes {

View file

@ -40,6 +40,7 @@ import 'package:olm/olm.dart' as olm;
import './user.dart'; import './user.dart';
import 'timeline.dart'; import 'timeline.dart';
import 'utils/matrix_localizations.dart';
import 'utils/states_map.dart'; import 'utils/states_map.dart';
enum PushRuleState { notify, mentions_only, dont_notify } enum PushRuleState { notify, mentions_only, dont_notify }
@ -279,6 +280,27 @@ class Room {
? states['m.room.name'].content['name'] ? 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. /// The topic of the room if set by a participant.
String get topic => states['m.room.topic'] != null String get topic => states['m.room.topic'] != null
? states['m.room.topic'].content['topic'] ? states['m.room.topic'].content['topic']

View file

@ -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.', '');
}
}
}

View file

@ -133,7 +133,7 @@ packages:
name: coverage name: coverage
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.13.3" version: "0.13.9"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -203,7 +203,7 @@ packages:
name: http name: http
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.0+4" version: "0.12.1"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -296,7 +296,7 @@ packages:
name: mime_type name: mime_type
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.4" version: "0.3.0"
multi_server_socket: multi_server_socket:
dependency: transitive dependency: transitive
description: description:
@ -326,7 +326,7 @@ packages:
name: package_config name: package_config
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.9.3"
package_resolver: package_resolver:
dependency: transitive dependency: transitive
description: description:
@ -424,7 +424,7 @@ packages:
name: source_map_stack_trace name: source_map_stack_trace
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.5" version: "2.0.0"
source_maps: source_maps:
dependency: transitive dependency: transitive
description: description:
@ -480,21 +480,21 @@ packages:
name: test name: test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.11.1" version: "1.14.3"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.13" version: "0.2.15"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.18" version: "0.3.4"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -537,6 +537,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.13" 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: xml:
dependency: transitive dependency: transitive
description: description:

View file

@ -8,10 +8,10 @@ environment:
sdk: ">=2.7.0 <3.0.0" sdk: ">=2.7.0 <3.0.0"
dependencies: dependencies:
http: ^0.12.0+4 http: ^0.12.1
mime_type: ^0.2.4 mime_type: ^0.3.0
canonical_json: ^1.0.0 canonical_json: ^1.0.0
image: ^2.1.4 image: ^2.1.12
olm: olm:
git: git: