663 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Dart
		
	
	
	
			
		
		
	
	
			663 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Dart
		
	
	
	
| /*
 | |
|  *   Famedly Matrix SDK
 | |
|  *   Copyright (C) 2019, 2020 Famedly GmbH
 | |
|  *
 | |
|  *   This program is free software: you can redistribute it and/or modify
 | |
|  *   it under the terms of the GNU Affero General Public License as
 | |
|  *   published by the Free Software Foundation, either version 3 of the
 | |
|  *   License, or (at your option) any later version.
 | |
|  *
 | |
|  *   This program is distributed in the hope that it will be useful,
 | |
|  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
|  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | |
|  *   GNU Affero General Public License for more details.
 | |
|  *
 | |
|  *   You should have received a copy of the GNU Affero General Public License
 | |
|  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | |
|  */
 | |
| 
 | |
| import 'dart:convert';
 | |
| import 'dart:typed_data';
 | |
| import 'package:famedlysdk/famedlysdk.dart';
 | |
| import 'package:famedlysdk/encryption.dart';
 | |
| import 'package:famedlysdk/src/utils/receipt.dart';
 | |
| import 'package:http/http.dart' as http;
 | |
| import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
 | |
| import '../matrix_api.dart';
 | |
| import './room.dart';
 | |
| import 'utils/matrix_localizations.dart';
 | |
| import './database/database.dart' show DbRoomState, DbEvent;
 | |
| 
 | |
| /// 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 extends MatrixEvent {
 | |
|   User get sender => room.getUserByMXIDSync(senderId ?? '@unknown');
 | |
| 
 | |
|   @Deprecated('Use [originServerTs] instead')
 | |
|   DateTime get time => originServerTs;
 | |
| 
 | |
|   @Deprecated('Use [type] instead')
 | |
|   String get typeKey => type;
 | |
| 
 | |
|   /// The room this event belongs to. May be null.
 | |
|   final Room room;
 | |
| 
 | |
|   /// The status of this event.
 | |
|   /// -1=ERROR
 | |
|   ///  0=SENDING
 | |
|   ///  1=SENT
 | |
|   ///  2=TIMELINE
 | |
|   ///  3=ROOM_STATE
 | |
|   int status;
 | |
| 
 | |
|   static const int defaultStatus = 2;
 | |
|   static const Map<String, int> STATUS_TYPE = {
 | |
|     'ERROR': -1,
 | |
|     'SENDING': 0,
 | |
|     'SENT': 1,
 | |
|     'TIMELINE': 2,
 | |
|     'ROOM_STATE': 3,
 | |
|   };
 | |
| 
 | |
|   /// Optional. The event that redacted this event, if any. Otherwise null.
 | |
|   Event get redactedBecause =>
 | |
|       unsigned != null && unsigned.containsKey('redacted_because')
 | |
|           ? Event.fromJson(unsigned['redacted_because'], room)
 | |
|           : null;
 | |
| 
 | |
|   bool get redacted => redactedBecause != null;
 | |
| 
 | |
|   User get stateKeyUser => room.getUserByMXIDSync(stateKey);
 | |
| 
 | |
|   double sortOrder;
 | |
| 
 | |
|   Event(
 | |
|       {this.status = defaultStatus,
 | |
|       Map<String, dynamic> content,
 | |
|       String type,
 | |
|       String eventId,
 | |
|       String roomId,
 | |
|       String senderId,
 | |
|       DateTime originServerTs,
 | |
|       Map<String, dynamic> unsigned,
 | |
|       Map<String, dynamic> prevContent,
 | |
|       String stateKey,
 | |
|       this.room,
 | |
|       this.sortOrder = 0.0}) {
 | |
|     this.content = content;
 | |
|     this.type = type;
 | |
|     this.eventId = eventId;
 | |
|     this.roomId = roomId ?? room?.id;
 | |
|     this.senderId = senderId;
 | |
|     this.unsigned = unsigned;
 | |
|     this.prevContent = prevContent;
 | |
|     this.stateKey = stateKey;
 | |
|     this.originServerTs = originServerTs;
 | |
|   }
 | |
| 
 | |
|   static Map<String, dynamic> getMapFromPayload(dynamic payload) {
 | |
|     if (payload is String) {
 | |
|       try {
 | |
|         return json.decode(payload);
 | |
|       } catch (e) {
 | |
|         return {};
 | |
|       }
 | |
|     }
 | |
|     if (payload is Map<String, dynamic>) return payload;
 | |
|     return {};
 | |
|   }
 | |
| 
 | |
|   factory Event.fromMatrixEvent(
 | |
|     MatrixEvent matrixEvent,
 | |
|     Room room, {
 | |
|     double sortOrder,
 | |
|     int status,
 | |
|   }) =>
 | |
|       Event(
 | |
|         status: status,
 | |
|         content: matrixEvent.content,
 | |
|         type: matrixEvent.type,
 | |
|         eventId: matrixEvent.eventId,
 | |
|         roomId: room.id,
 | |
|         senderId: matrixEvent.senderId,
 | |
|         originServerTs: matrixEvent.originServerTs,
 | |
|         unsigned: matrixEvent.unsigned,
 | |
|         prevContent: matrixEvent.prevContent,
 | |
|         stateKey: matrixEvent.stateKey,
 | |
|         room: room,
 | |
|         sortOrder: sortOrder,
 | |
|       );
 | |
| 
 | |
|   /// Get a State event from a table row or from the event stream.
 | |
|   factory Event.fromJson(Map<String, dynamic> jsonPayload, Room room,
 | |
|       [double sortOrder]) {
 | |
|     final content = Event.getMapFromPayload(jsonPayload['content']);
 | |
|     final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']);
 | |
|     final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']);
 | |
|     return Event(
 | |
|       status: jsonPayload['status'] ?? defaultStatus,
 | |
|       stateKey: jsonPayload['state_key'],
 | |
|       prevContent: prevContent,
 | |
|       content: content,
 | |
|       type: jsonPayload['type'],
 | |
|       eventId: jsonPayload['event_id'],
 | |
|       roomId: jsonPayload['room_id'],
 | |
|       senderId: jsonPayload['sender'],
 | |
|       originServerTs: jsonPayload.containsKey('origin_server_ts')
 | |
|           ? DateTime.fromMillisecondsSinceEpoch(jsonPayload['origin_server_ts'])
 | |
|           : DateTime.now(),
 | |
|       unsigned: unsigned,
 | |
|       room: room,
 | |
|       sortOrder: sortOrder ?? 0.0,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /// Get an event from either DbRoomState or DbEvent
 | |
|   factory Event.fromDb(dynamic dbEntry, Room room) {
 | |
|     if (!(dbEntry is DbRoomState || dbEntry is DbEvent)) {
 | |
|       throw ('Unknown db type');
 | |
|     }
 | |
|     final content = Event.getMapFromPayload(dbEntry.content);
 | |
|     final unsigned = Event.getMapFromPayload(dbEntry.unsigned);
 | |
|     final prevContent = Event.getMapFromPayload(dbEntry.prevContent);
 | |
|     return Event(
 | |
|       status: (dbEntry is DbEvent ? dbEntry.status : null) ?? defaultStatus,
 | |
|       stateKey: dbEntry.stateKey,
 | |
|       prevContent: prevContent,
 | |
|       content: content,
 | |
|       type: dbEntry.type,
 | |
|       eventId: dbEntry.eventId,
 | |
|       roomId: dbEntry.roomId,
 | |
|       senderId: dbEntry.sender,
 | |
|       originServerTs: dbEntry.originServerTs ?? DateTime.now(),
 | |
|       unsigned: unsigned,
 | |
|       room: room,
 | |
|       sortOrder: dbEntry.sortOrder ?? 0.0,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Map<String, dynamic> toJson() {
 | |
|     final data = <String, dynamic>{};
 | |
|     if (stateKey != null) data['state_key'] = stateKey;
 | |
|     if (prevContent != null && prevContent.isNotEmpty) {
 | |
|       data['prev_content'] = prevContent;
 | |
|     }
 | |
|     data['content'] = content;
 | |
|     data['type'] = type;
 | |
|     data['event_id'] = eventId;
 | |
|     data['room_id'] = roomId;
 | |
|     data['sender'] = senderId;
 | |
|     data['origin_server_ts'] = originServerTs.millisecondsSinceEpoch;
 | |
|     if (unsigned != null && unsigned.isNotEmpty) {
 | |
|       data['unsigned'] = unsigned;
 | |
|     }
 | |
|     return data;
 | |
|   }
 | |
| 
 | |
|   User get asUser => User.fromState(
 | |
|       stateKey: stateKey,
 | |
|       prevContent: prevContent,
 | |
|       content: content,
 | |
|       typeKey: type,
 | |
|       eventId: eventId,
 | |
|       roomId: roomId,
 | |
|       senderId: senderId,
 | |
|       originServerTs: originServerTs,
 | |
|       unsigned: unsigned,
 | |
|       room: room);
 | |
| 
 | |
|   String get messageType => (content.containsKey('m.relates_to') &&
 | |
|           content['m.relates_to']['m.in_reply_to'] != null)
 | |
|       ? MessageTypes.Reply
 | |
|       : content['msgtype'] ?? MessageTypes.Text;
 | |
| 
 | |
|   void setRedactionEvent(Event redactedBecause) {
 | |
|     unsigned = {
 | |
|       'redacted_because': redactedBecause.toJson(),
 | |
|     };
 | |
|     prevContent = null;
 | |
|     var contentKeyWhiteList = <String>[];
 | |
|     switch (type) {
 | |
|       case EventTypes.RoomMember:
 | |
|         contentKeyWhiteList.add('membership');
 | |
|         break;
 | |
|       case EventTypes.RoomCreate:
 | |
|         contentKeyWhiteList.add('creator');
 | |
|         break;
 | |
|       case EventTypes.RoomJoinRules:
 | |
|         contentKeyWhiteList.add('join_rule');
 | |
|         break;
 | |
|       case EventTypes.RoomPowerLevels:
 | |
|         contentKeyWhiteList.add('ban');
 | |
|         contentKeyWhiteList.add('events');
 | |
|         contentKeyWhiteList.add('events_default');
 | |
|         contentKeyWhiteList.add('kick');
 | |
|         contentKeyWhiteList.add('redact');
 | |
|         contentKeyWhiteList.add('state_default');
 | |
|         contentKeyWhiteList.add('users');
 | |
|         contentKeyWhiteList.add('users_default');
 | |
|         break;
 | |
|       case EventTypes.RoomAliases:
 | |
|         contentKeyWhiteList.add('aliases');
 | |
|         break;
 | |
|       case EventTypes.HistoryVisibility:
 | |
|         contentKeyWhiteList.add('history_visibility');
 | |
|         break;
 | |
|       default:
 | |
|         break;
 | |
|     }
 | |
|     var toRemoveList = <String>[];
 | |
|     for (var entry in content.entries) {
 | |
|       if (!contentKeyWhiteList.contains(entry.key)) {
 | |
|         toRemoveList.add(entry.key);
 | |
|       }
 | |
|     }
 | |
|     toRemoveList.forEach((s) => content.remove(s));
 | |
|   }
 | |
| 
 | |
|   /// Returns the body of this event if it has a body.
 | |
|   String get text => content['body'] ?? '';
 | |
| 
 | |
|   /// Returns the formatted boy of this event if it has a formatted body.
 | |
|   String get formattedText => content['formatted_body'] ?? '';
 | |
| 
 | |
|   /// Use this to get the body.
 | |
|   String get body {
 | |
|     if (redacted) return 'Redacted';
 | |
|     if (text != '') return text;
 | |
|     if (formattedText != '') return formattedText;
 | |
|     return '$type';
 | |
|   }
 | |
| 
 | |
|   /// Returns a list of [Receipt] instances for this event.
 | |
|   List<Receipt> get receipts {
 | |
|     if (!(room.roomAccountData.containsKey('m.receipt'))) return [];
 | |
|     var receiptsList = <Receipt>[];
 | |
|     for (var entry in room.roomAccountData['m.receipt'].content.entries) {
 | |
|       if (entry.value['event_id'] == eventId) {
 | |
|         receiptsList.add(Receipt(room.getUserByMXIDSync(entry.key),
 | |
|             DateTime.fromMillisecondsSinceEpoch(entry.value['ts'])));
 | |
|       }
 | |
|     }
 | |
|     return receiptsList;
 | |
|   }
 | |
| 
 | |
|   /// Removes this event if the status is < 1. This event will just be removed
 | |
|   /// from the database and the timelines. Returns false if not removed.
 | |
|   Future<bool> remove() async {
 | |
|     if (status < 1) {
 | |
|       await room.client.database?.removeEvent(room.client.id, eventId, room.id);
 | |
| 
 | |
|       room.client.onEvent.add(EventUpdate(
 | |
|           roomID: room.id,
 | |
|           type: 'timeline',
 | |
|           eventType: type,
 | |
|           content: {
 | |
|             'event_id': eventId,
 | |
|             'status': -2,
 | |
|             'content': {'body': 'Removed...'}
 | |
|           },
 | |
|           sortOrder: sortOrder));
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /// Try to send this event again. Only works with events of status -1.
 | |
|   Future<String> sendAgain({String txid}) async {
 | |
|     if (status != -1) return null;
 | |
|     await remove();
 | |
|     final eventID = await room.sendEvent(
 | |
|       content,
 | |
|       txid: txid ?? unsigned['transaction_id'],
 | |
|     );
 | |
|     return eventID;
 | |
|   }
 | |
| 
 | |
|   /// Whether the client is allowed to redact this event.
 | |
|   bool get canRedact => senderId == room.client.userID || room.canRedact;
 | |
| 
 | |
|   /// Redacts this event. Returns [ErrorResponse] on error.
 | |
|   Future<dynamic> redact({String reason, String txid}) =>
 | |
|       room.redactEvent(eventId, reason: reason, txid: txid);
 | |
| 
 | |
|   /// Whether this event is in reply to another event.
 | |
|   bool get isReply =>
 | |
|       content['m.relates_to'] is Map<String, dynamic> &&
 | |
|       content['m.relates_to']['m.in_reply_to'] is Map<String, dynamic> &&
 | |
|       content['m.relates_to']['m.in_reply_to']['event_id'] is String &&
 | |
|       (content['m.relates_to']['m.in_reply_to']['event_id'] as String)
 | |
|           .isNotEmpty;
 | |
| 
 | |
|   /// Searches for the reply event in the given timeline.
 | |
|   Future<Event> getReplyEvent(Timeline timeline) async {
 | |
|     if (!isReply) return null;
 | |
|     final String replyEventId =
 | |
|         content['m.relates_to']['m.in_reply_to']['event_id'];
 | |
|     return await timeline.getEventById(replyEventId);
 | |
|   }
 | |
| 
 | |
|   /// If this event is encrypted and the decryption was not successful because
 | |
|   /// the session is unknown, this requests the session key from other devices
 | |
|   /// in the room. If the event is not encrypted or the decryption failed because
 | |
|   /// of a different error, this throws an exception.
 | |
|   Future<void> requestKey() async {
 | |
|     if (type != EventTypes.Encrypted ||
 | |
|         messageType != MessageTypes.BadEncrypted ||
 | |
|         content['can_request_session'] != true) {
 | |
|       throw ('Session key not requestable');
 | |
|     }
 | |
|     await room.requestSessionKey(content['session_id'], content['sender_key']);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   bool get hasThumbnail =>
 | |
|       content['info'] is Map<String, dynamic> &&
 | |
|       (content['info'].containsKey('thumbnail_url') ||
 | |
|           content['info'].containsKey('thumbnail_file'));
 | |
| 
 | |
|   /// Downloads (and decryptes if necessary) the attachment of this
 | |
|   /// event and returns it as a [MatrixFile]. If this event doesn't
 | |
|   /// contain an attachment, this throws an error. Set [getThumbnail] to
 | |
|   /// true to download the thumbnail instead.
 | |
|   Future<MatrixFile> downloadAndDecryptAttachment(
 | |
|       {bool getThumbnail = false}) async {
 | |
|     if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
 | |
|       throw ("This event has the type '$type' and so it can't contain an attachment.");
 | |
|     }
 | |
|     if (!getThumbnail &&
 | |
|         !content.containsKey('url') &&
 | |
|         !content.containsKey('file')) {
 | |
|       throw ("This event hasn't any attachment.");
 | |
|     }
 | |
|     if (getThumbnail && !hasThumbnail) {
 | |
|       throw ("This event hasn't any thumbnail.");
 | |
|     }
 | |
|     final isEncrypted = getThumbnail
 | |
|         ? !content['info'].containsKey('thumbnail_url')
 | |
|         : !content.containsKey('url');
 | |
| 
 | |
|     if (isEncrypted && !room.client.encryptionEnabled) {
 | |
|       throw ('Encryption is not enabled in your Client.');
 | |
|     }
 | |
|     var mxContent = getThumbnail
 | |
|         ? Uri.parse(isEncrypted
 | |
|             ? content['info']['thumbnail_file']['url']
 | |
|             : content['info']['thumbnail_url'])
 | |
|         : Uri.parse(isEncrypted ? content['file']['url'] : content['url']);
 | |
| 
 | |
|     Uint8List uint8list;
 | |
| 
 | |
|     // Is this file storeable?
 | |
|     final infoMap =
 | |
|         getThumbnail ? content['info']['thumbnail_info'] : content['info'];
 | |
|     final storeable = room.client.database != null &&
 | |
|         infoMap is Map<String, dynamic> &&
 | |
|         infoMap['size'] is int &&
 | |
|         infoMap['size'] <= room.client.database.maxFileSize;
 | |
| 
 | |
|     if (storeable) {
 | |
|       uint8list = await room.client.database.getFile(mxContent.toString());
 | |
|     }
 | |
| 
 | |
|     // Download the file
 | |
|     if (uint8list == null) {
 | |
|       uint8list =
 | |
|           (await http.get(mxContent.getDownloadLink(room.client))).bodyBytes;
 | |
|       if (storeable) {
 | |
|         await room.client.database
 | |
|             .storeFile(mxContent.toString(), uint8list, DateTime.now());
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Decrypt the file
 | |
|     if (isEncrypted) {
 | |
|       final fileMap =
 | |
|           getThumbnail ? content['info']['thumbnail_file'] : content['file'];
 | |
|       if (!fileMap['key']['key_ops'].contains('decrypt')) {
 | |
|         throw ("Missing 'decrypt' in 'key_ops'.");
 | |
|       }
 | |
|       final encryptedFile = EncryptedFile();
 | |
|       encryptedFile.data = uint8list;
 | |
|       encryptedFile.iv = fileMap['iv'];
 | |
|       encryptedFile.k = fileMap['key']['k'];
 | |
|       encryptedFile.sha256 = fileMap['hashes']['sha256'];
 | |
|       uint8list = await decryptFile(encryptedFile);
 | |
|     }
 | |
|     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.RoomTombstone:
 | |
|         localizedBody = i18n.roomHasBeenUpgraded;
 | |
|         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(type);
 | |
|     }
 | |
| 
 | |
|     // 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<String> textOnlyMessageTypes = {
 | |
|     MessageTypes.Text,
 | |
|     MessageTypes.Reply,
 | |
|     MessageTypes.Notice,
 | |
|     MessageTypes.Emote,
 | |
|     MessageTypes.None,
 | |
|   };
 | |
| }
 |