refactor: make event nullsafe

This commit is contained in:
Nicolas Werner 2021-10-14 00:50:06 +02:00
parent d2ee73f96f
commit 17fd1f22b3
11 changed files with 220 additions and 130 deletions

View File

@ -308,7 +308,7 @@ class Encryption {
await client.database?.storeEventUpdate( await client.database?.storeEventUpdate(
EventUpdate( EventUpdate(
content: event.toJson(), content: event.toJson(),
roomID: event.roomId, roomID: roomId,
type: updateType, type: updateType,
), ),
); );

View File

@ -1,4 +1,3 @@
// @dart=2.9
/* /*
* Famedly Matrix SDK * Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 Famedly GmbH * Copyright (C) 2019, 2020, 2021 Famedly GmbH
@ -39,7 +38,13 @@ abstract class RelationshipTypes {
/// 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 extends MatrixEvent { class Event extends MatrixEvent {
User get sender => room.getUserByMXIDSync(senderId ?? '@unknown:unknown'); User get sender =>
room?.getUserByMXIDSync(senderId) ??
User.fromState(
stateKey: senderId,
typeKey: EventTypes.RoomMember,
originServerTs: DateTime.now(),
);
@Deprecated('Use [originServerTs] instead') @Deprecated('Use [originServerTs] instead')
DateTime get time => originServerTs; DateTime get time => originServerTs;
@ -48,10 +53,10 @@ class Event extends MatrixEvent {
String get typeKey => type; String get typeKey => type;
@Deprecated('Use [sender.calcDisplayname()] instead') @Deprecated('Use [sender.calcDisplayname()] instead')
String get senderName => sender.calcDisplayname(); String? get senderName => sender.calcDisplayname();
/// The room this event belongs to. May be null. /// The room this event belongs to. May be null.
final Room room; final Room? room;
/// The status of this event. /// The status of this event.
EventStatus status; EventStatus status;
@ -59,33 +64,39 @@ class Event extends MatrixEvent {
static const EventStatus defaultStatus = EventStatus.synced; static const EventStatus defaultStatus = EventStatus.synced;
/// Optional. The event that redacted this event, if any. Otherwise null. /// Optional. The event that redacted this event, if any. Otherwise null.
Event get redactedBecause => Event? get redactedBecause {
unsigned != null && unsigned['redacted_because'] is Map final redacted_because = unsigned?['redacted_because'];
? Event.fromJson(unsigned['redacted_because'], room) final room = this.room;
: null; return (redacted_because is Map<String, dynamic>)
? Event.fromJson(redacted_because, room)
: null;
}
bool get redacted => redactedBecause != null; bool get redacted => redactedBecause != null;
User get stateKeyUser => room.getUserByMXIDSync(stateKey); User? get stateKeyUser => room?.getUserByMXIDSync(stateKey);
Event({ Event({
this.status = defaultStatus, this.status = defaultStatus,
Map<String, dynamic> content, required Map<String, dynamic> content,
String type, required String type,
String eventId, required String eventId,
String roomId, String? roomId,
String senderId, required String senderId,
DateTime originServerTs, required DateTime originServerTs,
Map<String, dynamic> unsigned, Map<String, dynamic>? unsigned,
Map<String, dynamic> prevContent, Map<String, dynamic>? prevContent,
String stateKey, String? stateKey,
this.room, this.room,
}) { }) : super(
this.content = content; content: content,
this.type = type; type: type,
eventId: eventId,
senderId: senderId,
originServerTs: originServerTs,
roomId: roomId ?? room?.id,
) {
this.eventId = eventId; this.eventId = eventId;
this.roomId = roomId ?? room?.id;
this.senderId = senderId;
this.unsigned = unsigned; this.unsigned = unsigned;
// synapse unfortunately isn't following the spec and tosses the prev_content // synapse unfortunately isn't following the spec and tosses the prev_content
// into the unsigned block. // into the unsigned block.
@ -103,7 +114,6 @@ class Event extends MatrixEvent {
// A strange bug in dart web makes this crash // A strange bug in dart web makes this crash
} }
this.stateKey = stateKey; this.stateKey = stateKey;
this.originServerTs = originServerTs;
// Mark event as failed to send if status is `sending` and event is older // Mark event as failed to send if status is `sending` and event is older
// than the timeout. This should not happen with the deprecated Moor // than the timeout. This should not happen with the deprecated Moor
@ -113,7 +123,8 @@ class Event extends MatrixEvent {
final age = DateTime.now().millisecondsSinceEpoch - final age = DateTime.now().millisecondsSinceEpoch -
originServerTs.millisecondsSinceEpoch; originServerTs.millisecondsSinceEpoch;
if (age > room.client.sendMessageTimeoutSeconds * 1000) { final room = this.room;
if (room != null && age > room.client.sendMessageTimeoutSeconds * 1000) {
// Update this event in database and open timelines // Update this event in database and open timelines
final json = toJson(); final json = toJson();
json['unsigned'] ??= <String, dynamic>{}; json['unsigned'] ??= <String, dynamic>{};
@ -143,7 +154,7 @@ class Event extends MatrixEvent {
factory Event.fromMatrixEvent( factory Event.fromMatrixEvent(
MatrixEvent matrixEvent, MatrixEvent matrixEvent,
Room room, { Room room, {
EventStatus status, EventStatus status = defaultStatus,
}) => }) =>
Event( Event(
status: status, status: status,
@ -162,7 +173,7 @@ class Event extends MatrixEvent {
/// Get a State event from a table row or from the event stream. /// Get a State event from a table row or from the event stream.
factory Event.fromJson( factory Event.fromJson(
Map<String, dynamic> jsonPayload, Map<String, dynamic> jsonPayload,
Room room, Room? room,
) { ) {
final content = Event.getMapFromPayload(jsonPayload['content']); final content = Event.getMapFromPayload(jsonPayload['content']);
final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']); final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']);
@ -190,7 +201,7 @@ class Event extends MatrixEvent {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final data = <String, dynamic>{}; final data = <String, dynamic>{};
if (stateKey != null) data['state_key'] = stateKey; if (stateKey != null) data['state_key'] = stateKey;
if (prevContent != null && prevContent.isNotEmpty) { if (prevContent?.isNotEmpty == true) {
data['prev_content'] = prevContent; data['prev_content'] = prevContent;
} }
data['content'] = content; data['content'] = content;
@ -199,14 +210,15 @@ class Event extends MatrixEvent {
data['room_id'] = roomId; data['room_id'] = roomId;
data['sender'] = senderId; data['sender'] = senderId;
data['origin_server_ts'] = originServerTs.millisecondsSinceEpoch; data['origin_server_ts'] = originServerTs.millisecondsSinceEpoch;
if (unsigned != null && unsigned.isNotEmpty) { if (unsigned?.isNotEmpty == true) {
data['unsigned'] = unsigned; data['unsigned'] = unsigned;
} }
return data; return data;
} }
User get asUser => User.fromState( User get asUser => User.fromState(
stateKey: stateKey, // state key should always be set for member events
stateKey: stateKey!,
prevContent: prevContent, prevContent: prevContent,
content: content, content: content,
typeKey: type, typeKey: type,
@ -282,8 +294,10 @@ class Event extends MatrixEvent {
/// Returns a list of [Receipt] instances for this event. /// Returns a list of [Receipt] instances for this event.
List<Receipt> get receipts { List<Receipt> get receipts {
if (!(room.roomAccountData.containsKey('m.receipt'))) return []; final room = this.room;
return room.roomAccountData['m.receipt'].content.entries final receipt = room?.roomAccountData['m.receipt'];
if (receipt == null || room == null) return [];
return receipt.content.entries
.where((entry) => entry.value['event_id'] == eventId) .where((entry) => entry.value['event_id'] == eventId)
.map((entry) => Receipt(room.getUserByMXIDSync(entry.key), .map((entry) => Receipt(room.getUserByMXIDSync(entry.key),
DateTime.fromMillisecondsSinceEpoch(entry.value['ts']))) DateTime.fromMillisecondsSinceEpoch(entry.value['ts'])))
@ -294,6 +308,11 @@ class Event extends MatrixEvent {
/// This event will just be removed from the database and the timelines. /// This event will just be removed from the database and the timelines.
/// Returns [false] if not removed. /// Returns [false] if not removed.
Future<bool> remove() async { Future<bool> remove() async {
final room = this.room;
if (room == null) {
return false;
}
if (!status.isSent) { if (!status.isSent) {
await room.client.database?.removeEvent(eventId, room.id); await room.client.database?.removeEvent(eventId, room.id);
@ -311,29 +330,33 @@ class Event extends MatrixEvent {
return false; return false;
} }
/// Try to send this event again. Only works with events of `EventStatus.isError`. /// Try to send this event again. Only works with events of status -1.
Future<String> sendAgain({String txid}) async { Future<String?> sendAgain({String? txid}) async {
if (!status.isError) return null; if (!status.isError) return null;
// we do not remove the event here. It will automatically be updated // we do not remove the event here. It will automatically be updated
// in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2 // in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2
final newEventId = await room.sendEvent( final newEventId = await room?.sendEvent(
content, content,
txid: txid ?? unsigned['transaction_id'] ?? eventId, txid: txid ?? unsigned?['transaction_id'] ?? eventId,
); );
return newEventId; return newEventId;
} }
/// Whether the client is allowed to redact this event. /// Whether the client is allowed to redact this event.
bool get canRedact => senderId == room.client.userID || room.canRedact; bool get canRedact =>
senderId == room?.client.userID || (room?.canRedact ?? false);
/// Redacts this event. Throws `ErrorResponse` on error. /// Redacts this event. Throws `ErrorResponse` on error.
Future<dynamic> redactEvent({String reason, String txid}) => Future<String?> redactEvent({String? reason, String? txid}) async =>
room.redactEvent(eventId, reason: reason, txid: txid); await room?.redactEvent(eventId, reason: reason, txid: txid);
/// Searches for the reply event in the given timeline. /// Searches for the reply event in the given timeline.
Future<Event> getReplyEvent(Timeline timeline) async { Future<Event?> getReplyEvent(Timeline timeline) async {
if (relationshipType != RelationshipTypes.reply) return null; if (relationshipType != RelationshipTypes.reply) return null;
return await timeline.getEventById(relationshipEventId); final relationshipEventId = this.relationshipEventId;
return relationshipEventId == null
? null
: await timeline.getEventById(relationshipEventId);
} }
/// If this event is encrypted and the decryption was not successful because /// If this event is encrypted and the decryption was not successful because
@ -346,7 +369,7 @@ class Event extends MatrixEvent {
content['can_request_session'] != true) { content['can_request_session'] != true) {
throw ('Session key not requestable'); throw ('Session key not requestable');
} }
await room.requestSessionKey(content['session_id'], content['sender_key']); await room?.requestSessionKey(content['session_id'], content['sender_key']);
return; return;
} }
@ -420,7 +443,7 @@ class Event extends MatrixEvent {
/// [minNoThumbSize] is the minimum size that an original image may be to not fetch its thumbnail, defaults to 80k /// [minNoThumbSize] is the minimum size that an original image may be to not fetch its thumbnail, defaults to 80k
/// [useThumbnailMxcUrl] says weather to use the mxc url of the thumbnail, rather than the original attachment. /// [useThumbnailMxcUrl] says weather to use the mxc url of the thumbnail, rather than the original attachment.
/// [animated] says weather the thumbnail is animated /// [animated] says weather the thumbnail is animated
Uri getAttachmentUrl( Uri? getAttachmentUrl(
{bool getThumbnail = false, {bool getThumbnail = false,
bool useThumbnailMxcUrl = false, bool useThumbnailMxcUrl = false,
double width = 800.0, double width = 800.0,
@ -428,6 +451,10 @@ class Event extends MatrixEvent {
ThumbnailMethod method = ThumbnailMethod.scale, ThumbnailMethod method = ThumbnailMethod.scale,
int minNoThumbSize = _minNoThumbSize, int minNoThumbSize = _minNoThumbSize,
bool animated = false}) { bool animated = false}) {
final client = room?.client;
if (client == null) {
return null;
}
if (![EventTypes.Message, EventTypes.Sticker].contains(type) || if (![EventTypes.Message, EventTypes.Sticker].contains(type) ||
!hasAttachment || !hasAttachment ||
isAttachmentEncrypted) { isAttachmentEncrypted) {
@ -449,14 +476,14 @@ class Event extends MatrixEvent {
// now generate the actual URLs // now generate the actual URLs
if (getThumbnail) { if (getThumbnail) {
return Uri.parse(thisMxcUrl).getThumbnail( return Uri.parse(thisMxcUrl).getThumbnail(
room.client, client,
width: width, width: width,
height: height, height: height,
method: method, method: method,
animated: animated, animated: animated,
); );
} else { } else {
return Uri.parse(thisMxcUrl).getDownloadLink(room.client); return Uri.parse(thisMxcUrl).getDownloadLink(client);
} }
} }
@ -472,13 +499,17 @@ class Event extends MatrixEvent {
getThumbnail = mxcUrl != attachmentMxcUrl; getThumbnail = mxcUrl != attachmentMxcUrl;
// Is this file storeable? // Is this file storeable?
final thisInfoMap = getThumbnail ? thumbnailInfoMap : infoMap; final thisInfoMap = getThumbnail ? thumbnailInfoMap : infoMap;
final storeable = room.client.database != null && final database = room?.client.database;
thisInfoMap['size'] is int && if (database == null) {
thisInfoMap['size'] <= room.client.database.maxFileSize; return false;
}
Uint8List uint8list; final storeable = thisInfoMap['size'] is int &&
thisInfoMap['size'] <= database.maxFileSize;
Uint8List? uint8list;
if (storeable) { if (storeable) {
uint8list = await room.client.database.getFile(mxcUrl); uint8list = await database.getFile(mxcUrl);
} }
return uint8list != null; return uint8list != null;
} }
@ -489,10 +520,15 @@ class Event extends MatrixEvent {
/// true to download the thumbnail instead. /// true to download the thumbnail instead.
Future<MatrixFile> downloadAndDecryptAttachment( Future<MatrixFile> downloadAndDecryptAttachment(
{bool getThumbnail = false, {bool getThumbnail = false,
Future<Uint8List> Function(Uri) downloadCallback}) async { Future<Uint8List> Function(Uri)? downloadCallback}) async {
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) { if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
throw ("This event has the type '$type' and so it can't contain an attachment."); throw ("This event has the type '$type' and so it can't contain an attachment.");
} }
final client = room?.client;
final database = room?.client.database;
if (client == null) {
throw 'This event has no valid client.';
}
final mxcUrl = attachmentOrThumbnailMxcUrl(getThumbnail: getThumbnail); final mxcUrl = attachmentOrThumbnailMxcUrl(getThumbnail: getThumbnail);
if (mxcUrl == null) { if (mxcUrl == null) {
throw "This event hasn't any attachment or thumbnail."; throw "This event hasn't any attachment or thumbnail.";
@ -500,33 +536,30 @@ class Event extends MatrixEvent {
getThumbnail = mxcUrl != attachmentMxcUrl; getThumbnail = mxcUrl != attachmentMxcUrl;
final isEncrypted = final isEncrypted =
getThumbnail ? isThumbnailEncrypted : isAttachmentEncrypted; getThumbnail ? isThumbnailEncrypted : isAttachmentEncrypted;
if (isEncrypted && !client.encryptionEnabled) {
if (isEncrypted && !room.client.encryptionEnabled) {
throw ('Encryption is not enabled in your Client.'); throw ('Encryption is not enabled in your Client.');
} }
Uint8List uint8list;
// Is this file storeable? // Is this file storeable?
final thisInfoMap = getThumbnail ? thumbnailInfoMap : infoMap; final thisInfoMap = getThumbnail ? thumbnailInfoMap : infoMap;
var storeable = room.client.database != null && var storeable = database != null &&
thisInfoMap['size'] is int && thisInfoMap['size'] is int &&
thisInfoMap['size'] <= room.client.database.maxFileSize; thisInfoMap['size'] <= database.maxFileSize;
Uint8List? uint8list;
if (storeable) { if (storeable) {
uint8list = await room.client.database.getFile(mxcUrl); uint8list = await client.database.getFile(mxcUrl);
} }
// Download the file // Download the file
if (uint8list == null) { if (uint8list == null) {
downloadCallback ??= (Uri url) async { downloadCallback ??= (Uri url) async => (await http.get(url)).bodyBytes;
return (await http.get(url)).bodyBytes; uint8list = await downloadCallback(mxcUrl.getDownloadLink(client));
}; storeable = database != null &&
uint8list = await downloadCallback(mxcUrl.getDownloadLink(room.client)); storeable &&
storeable = storeable && uint8list.lengthInBytes < database.maxFileSize;
uint8list.lengthInBytes < room.client.database.maxFileSize;
if (storeable) { if (storeable) {
await room.client.database.storeFile( await database.storeFile(
mxcUrl, uint8list, DateTime.now().millisecondsSinceEpoch); mxcUrl, uint8list, DateTime.now().millisecondsSinceEpoch);
} }
} }
@ -544,7 +577,7 @@ class Event extends MatrixEvent {
k: fileMap['key']['k'], k: fileMap['key']['k'],
sha256: fileMap['hashes']['sha256'], sha256: fileMap['hashes']['sha256'],
); );
uint8list = await room.client.runInBackground(decryptFile, encryptedFile); uint8list = await client.runInBackground(decryptFile, encryptedFile);
} }
return MatrixFile(bytes: uint8list, name: body); return MatrixFile(bytes: uint8list, name: body);
} }
@ -565,7 +598,7 @@ class Event extends MatrixEvent {
bool plaintextBody = false, bool plaintextBody = false,
}) { }) {
if (redacted) { if (redacted) {
return i18n.removedBy(redactedBecause.sender.calcDisplayname()); return i18n.removedBy(redactedBecause?.sender?.calcDisplayname() ?? '');
} }
var body = plaintextBody ? this.plaintextBody : this.body; var body = plaintextBody ? this.plaintextBody : this.body;
@ -607,8 +640,9 @@ class Event extends MatrixEvent {
if (withSenderNamePrefix && if (withSenderNamePrefix &&
type == EventTypes.Message && type == EventTypes.Message &&
textOnlyMessageTypes.contains(messageType)) { textOnlyMessageTypes.contains(messageType)) {
final senderNameOrYou = final senderNameOrYou = senderId == room?.client.userID
senderId == room.client.userID ? i18n.you : sender.calcDisplayname(); ? i18n.you
: (sender?.calcDisplayname() ?? '');
localizedBody = '$senderNameOrYou: $localizedBody'; localizedBody = '$senderNameOrYou: $localizedBody';
} }
@ -623,18 +657,18 @@ class Event extends MatrixEvent {
}; };
/// returns if this event matches the passed event or transaction id /// returns if this event matches the passed event or transaction id
bool matchesEventOrTransactionId(String search) { bool matchesEventOrTransactionId(String? search) {
if (search == null) { if (search == null) {
return false; return false;
} }
if (eventId == search) { if (eventId == search) {
return true; return true;
} }
return unsigned != null && unsigned['transaction_id'] == search; return unsigned?['transaction_id'] == search;
} }
/// Get the relationship type of an event. `null` if there is none /// Get the relationship type of an event. `null` if there is none
String get relationshipType { String? get relationshipType {
if (content?.tryGet<Map<String, dynamic>>('m.relates_to') == null) { if (content?.tryGet<Map<String, dynamic>>('m.relates_to') == null) {
return null; return null;
} }
@ -643,11 +677,11 @@ class Event extends MatrixEvent {
} }
return content return content
.tryGet<Map<String, dynamic>>('m.relates_to') .tryGet<Map<String, dynamic>>('m.relates_to')
.tryGet<String>('rel_type'); ?.tryGet<String>('rel_type');
} }
/// Get the event ID that this relationship will reference. `null` if there is none /// Get the event ID that this relationship will reference. `null` if there is none
String get relationshipEventId { String? get relationshipEventId {
if (content == null || !(content['m.relates_to'] is Map)) { if (content == null || !(content['m.relates_to'] is Map)) {
return null; return null;
} }
@ -664,15 +698,12 @@ class Event extends MatrixEvent {
/// Get whether this event has aggregated events from a certain [type] /// Get whether this event has aggregated events from a certain [type]
/// To be able to do that you need to pass a [timeline] /// To be able to do that you need to pass a [timeline]
bool hasAggregatedEvents(Timeline timeline, String type) => bool hasAggregatedEvents(Timeline timeline, String type) =>
timeline.aggregatedEvents.containsKey(eventId) && timeline.aggregatedEvents[eventId]?.containsKey(type) == true;
timeline.aggregatedEvents[eventId].containsKey(type);
/// Get all the aggregated event objects for a given [type]. To be able to do this /// Get all the aggregated event objects for a given [type]. To be able to do this
/// you have to pass a [timeline] /// you have to pass a [timeline]
Set<Event> aggregatedEvents(Timeline timeline, String type) => Set<Event> aggregatedEvents(Timeline timeline, String type) =>
hasAggregatedEvents(timeline, type) timeline.aggregatedEvents[eventId]?[type] ?? <Event>{};
? timeline.aggregatedEvents[eventId][type]
: <Event>{};
/// Fetches the event to be rendered, taking into account all the edits and the like. /// Fetches the event to be rendered, taking into account all the edits and the like.
/// It needs a [timeline] for that. /// It needs a [timeline] for that.

View File

@ -190,9 +190,10 @@ class Timeline {
if (events[i].eventId != null) { if (events[i].eventId != null) {
searchHaystack.add(events[i].eventId); searchHaystack.add(events[i].eventId);
} }
if (events[i].unsigned != null &&
events[i].unsigned['transaction_id'] != null) { final txnid = events[i].unsigned?['transaction_id'];
searchHaystack.add(events[i].unsigned['transaction_id']); if (txnid != null) {
searchHaystack.add(txnid);
} }
if (searchNeedle.intersection(searchHaystack).isNotEmpty) { if (searchNeedle.intersection(searchHaystack).isNotEmpty) {
break; break;
@ -205,19 +206,18 @@ class Timeline {
eventSet.removeWhere((e) => eventSet.removeWhere((e) =>
e.matchesEventOrTransactionId(event.eventId) || e.matchesEventOrTransactionId(event.eventId) ||
(event.unsigned != null && (event.unsigned != null &&
e.matchesEventOrTransactionId(event.unsigned['transaction_id']))); e.matchesEventOrTransactionId(event.unsigned?['transaction_id'])));
} }
void addAggregatedEvent(Event event) { void addAggregatedEvent(Event event) {
// we want to add an event to the aggregation tree // we want to add an event to the aggregation tree
if (event.relationshipType == null || event.relationshipEventId == null) { final relationshipType = event.relationshipType;
final relationshipEventId = event.relationshipEventId;
if (relationshipType == null || relationshipEventId == null) {
return; // nothing to do return; // nothing to do
} }
if (!aggregatedEvents.containsKey(event.relationshipEventId)) { final events = (aggregatedEvents[relationshipEventId] ??=
aggregatedEvents[event.relationshipEventId] = <String, Set<Event>>{}; <String, Set<Event>>{})[relationshipType] ??= <Event>{};
}
final events = (aggregatedEvents[event.relationshipEventId] ??=
<String, Set<Event>>{})[event.relationshipType] ??= <Event>{};
// remove a potential old event // remove a potential old event
_removeEventFromSet(events, event); _removeEventFromSet(events, event);
// add the new one // add the new one
@ -227,7 +227,7 @@ class Timeline {
void removeAggregatedEvent(Event event) { void removeAggregatedEvent(Event event) {
aggregatedEvents.remove(event.eventId); aggregatedEvents.remove(event.eventId);
if (event.unsigned != null) { if (event.unsigned != null) {
aggregatedEvents.remove(event.unsigned['transaction_id']); aggregatedEvents.remove(event.unsigned?['transaction_id']);
} }
for (final types in aggregatedEvents.values) { for (final types in aggregatedEvents.values) {
for (final events in types.values) { for (final events in types.values) {

View File

@ -49,9 +49,9 @@ class User extends Event {
required String stateKey, required String stateKey,
dynamic content, dynamic content,
required String typeKey, required String typeKey,
String? eventId, String eventId = 'fakevent',
String? roomId, String? roomId,
String? senderId, String senderId = 'fakesender',
required DateTime originServerTs, required DateTime originServerTs,
dynamic unsigned, dynamic unsigned,
Room? room}) Room? room})
@ -68,7 +68,7 @@ class User extends Event {
room: room); room: room);
/// The full qualified Matrix ID in the format @username:server.abc. /// The full qualified Matrix ID in the format @username:server.abc.
String get id => stateKey; String get id => stateKey ?? '\@unknown:unknown';
/// The displayname of the user if the user has set one. /// The displayname of the user if the user has set one.
String? get displayName => String? get displayName =>
@ -91,13 +91,16 @@ class User extends Event {
}, orElse: () => Membership.join); }, orElse: () => Membership.join);
/// The avatar if the user has one. /// The avatar if the user has one.
Uri? get avatarUrl => content != null && content.containsKey('avatar_url') Uri? get avatarUrl {
? (content['avatar_url'] is String final prevContent = this.prevContent;
? Uri.tryParse(content['avatar_url']) return content.containsKey('avatar_url')
: null) ? (content['avatar_url'] is String
: (prevContent != null && prevContent['avatar_url'] is String ? Uri.tryParse(content['avatar_url'])
? Uri.tryParse(prevContent['avatar_url']) : null)
: null); : (prevContent != null && prevContent['avatar_url'] is String
? Uri.tryParse(prevContent['avatar_url'])
: null);
}
/// Returns the displayname or the local part of the Matrix ID if the user /// Returns the displayname or the local part of the Matrix ID if the user
/// has no displayname. If [formatLocalpart] is true, then the localpart will /// has no displayname. If [formatLocalpart] is true, then the localpart will
@ -132,37 +135,40 @@ class User extends Event {
} }
/// Call the Matrix API to kick this user from this room. /// Call the Matrix API to kick this user from this room.
Future<void> kick() => room.kick(id); Future<void> kick() async => await room?.kick(id);
/// Call the Matrix API to ban this user from this room. /// Call the Matrix API to ban this user from this room.
Future<void> ban() => room.ban(id); Future<void> ban() async => await room?.ban(id);
/// Call the Matrix API to unban this banned user from this room. /// Call the Matrix API to unban this banned user from this room.
Future<void> unban() => room.unban(id); Future<void> unban() async => await room?.unban(id);
/// Call the Matrix API to change the power level of this user. /// Call the Matrix API to change the power level of this user.
Future<void> setPower(int power) => room.setPower(id, power); Future<void> setPower(int power) async => await room?.setPower(id, power);
/// Returns an existing direct chat ID with this user or creates a new one. /// Returns an existing direct chat ID with this user or creates a new one.
/// Returns null on error. /// Returns null on error.
Future<String?> startDirectChat() => room.client.startDirectChat(id); Future<String?> startDirectChat() async => room?.client.startDirectChat(id);
/// The newest presence of this user if there is any and null if not. /// The newest presence of this user if there is any and null if not.
Presence? get presence => room.client.presences[id]; Presence? get presence => room?.client.presences[id];
/// Whether the client is able to ban/unban this user. /// Whether the client is able to ban/unban this user.
bool get canBan => room.canBan && powerLevel < room.ownPowerLevel; bool get canBan =>
(room?.canBan ?? false) &&
powerLevel < (room?.ownPowerLevel ?? powerLevel);
/// Whether the client is able to kick this user. /// Whether the client is able to kick this user.
bool get canKick => bool get canKick =>
[Membership.join, Membership.invite].contains(membership) && [Membership.join, Membership.invite].contains(membership) &&
room.canKick && (room?.canKick ?? false) &&
powerLevel < room.ownPowerLevel; powerLevel < (room?.ownPowerLevel ?? powerLevel);
/// Whether the client is allowed to change the power level of this user. /// Whether the client is allowed to change the power level of this user.
/// Please be aware that you can only set the power level to at least your own! /// Please be aware that you can only set the power level to at least your own!
bool get canChangePowerLevel => bool get canChangePowerLevel =>
room.canChangePowerLevel && powerLevel < room.ownPowerLevel; (room?.canChangePowerLevel ?? false) &&
powerLevel < (room?.ownPowerLevel ?? powerLevel);
@override @override
bool operator ==(dynamic other) => (other is User && bool operator ==(dynamic other) => (other is User &&
@ -190,7 +196,7 @@ class User extends Event {
: '[$displayName]'); : '[$displayName]');
// get all the users with the same display name // get all the users with the same display name
final allUsersWithSameDisplayname = room.getParticipants(); final allUsersWithSameDisplayname = room?.getParticipants() ?? [];
allUsersWithSameDisplayname.removeWhere((user) => allUsersWithSameDisplayname.removeWhere((user) =>
user.id == id || user.id == id ||
(user.displayName?.isEmpty ?? true) || (user.displayName?.isEmpty ?? true) ||

View File

@ -104,12 +104,11 @@ abstract class EventLocalizations {
}, },
EventTypes.RoomMember: (event, i18n, body) { EventTypes.RoomMember: (event, i18n, body) {
var text = 'Failed to parse member event'; var text = 'Failed to parse member event';
final targetName = event.stateKeyUser.calcDisplayname(); final targetName = event.stateKeyUser?.calcDisplayname() ?? '';
// Has the membership changed? // Has the membership changed?
final newMembership = event.content['membership'] ?? ''; final newMembership = event.content['membership'] ?? '';
final oldMembership = event.prevContent != null final oldMembership = event.prevContent?['membership'] ?? '';
? event.prevContent['membership'] ?? ''
: '';
if (newMembership != oldMembership) { if (newMembership != oldMembership) {
if (oldMembership == 'invite' && newMembership == 'join') { if (oldMembership == 'invite' && newMembership == 'join') {
text = i18n.acceptedTheInvitation(targetName); text = i18n.acceptedTheInvitation(targetName);
@ -146,22 +145,19 @@ abstract class EventLocalizations {
} }
} else if (newMembership == 'join') { } else if (newMembership == 'join') {
final newAvatar = event.content['avatar_url'] ?? ''; final newAvatar = event.content['avatar_url'] ?? '';
final oldAvatar = event.prevContent != null final oldAvatar = event.prevContent?['avatar_url'] ?? '';
? event.prevContent['avatar_url'] ?? ''
: '';
final newDisplayname = event.content['displayname'] ?? ''; final newDisplayname = event.content['displayname'] ?? '';
final oldDisplayname = event.prevContent != null final oldDisplayname = event.prevContent?['displayname'] ?? '';
? event.prevContent['displayname'] ?? '' final stateKey = event.stateKey;
: '';
// Has the user avatar changed? // Has the user avatar changed?
if (newAvatar != oldAvatar) { if (newAvatar != oldAvatar) {
text = i18n.changedTheProfileAvatar(targetName); text = i18n.changedTheProfileAvatar(targetName);
} }
// Has the user avatar changed? // Has the user displayname changed?
else if (newDisplayname != oldDisplayname) { else if (newDisplayname != oldDisplayname && stateKey != null) {
text = i18n.changedTheDisplaynameTo(event.stateKey, newDisplayname); text = i18n.changedTheDisplaynameTo(stateKey, newDisplayname);
} }
} }
return text; return text;
@ -201,7 +197,7 @@ abstract class EventLocalizations {
EventTypes.Encryption: (event, i18n, body) { EventTypes.Encryption: (event, i18n, body) {
var localizedBody = var localizedBody =
i18n.activatedEndToEndEncryption(event.sender.calcDisplayname()); i18n.activatedEndToEndEncryption(event.sender.calcDisplayname());
if (!event.room.client.encryptionEnabled) { if (event.room?.client.encryptionEnabled == false) {
localizedBody += '. ' + i18n.needPantalaimonWarning; localizedBody += '. ' + i18n.needPantalaimonWarning;
} }
return localizedBody; return localizedBody;

View File

@ -79,7 +79,9 @@ extension ImagePackRoomExtension on Room {
for (final entry in allRoomEmotes.entries) { for (final entry in allRoomEmotes.entries) {
addImagePack(entry.value, addImagePack(entry.value,
room: this, room: this,
slug: entry.value.stateKey.isEmpty ? 'room' : entry.value.stateKey); slug: (entry.value.stateKey?.isNotEmpty == true)
? entry.value.stateKey
: 'room');
} }
} }
return packs; return packs;

View File

@ -21,7 +21,7 @@ import 'package:matrix_api_lite/matrix_api_lite.dart';
import '../event.dart'; import '../event.dart';
class SpaceChild { class SpaceChild {
final String roomId; final String? roomId;
final List<String>? via; final List<String>? via;
final String order; final String order;
final bool? suggested; final bool? suggested;
@ -35,7 +35,7 @@ class SpaceChild {
} }
class SpaceParent { class SpaceParent {
final String roomId; final String? roomId;
final List<String>? via; final List<String>? via;
final bool? canonical; final bool? canonical;

View File

@ -55,12 +55,18 @@ void main() {
content: {}, content: {},
room: room, room: room,
stateKey: '', stateKey: '',
eventId: '\$fakeeventid',
originServerTs: DateTime.now(),
senderId: '\@fakeuser:fakeServer.notExisting',
)); ));
room.setState(Event( room.setState(Event(
type: 'm.room.member', type: 'm.room.member',
content: {'membership': 'join'}, content: {'membership': 'join'},
room: room, room: room,
stateKey: client.userID, stateKey: client.userID,
eventId: '\$fakeeventid',
originServerTs: DateTime.now(),
senderId: '\@fakeuser:fakeServer.notExisting',
)); ));
}); });
@ -135,7 +141,18 @@ void main() {
test('react', () async { test('react', () async {
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/react 🦊', await room.sendTextEvent('/react 🦊',
inReplyTo: Event(eventId: '\$event')); inReplyTo: Event(
eventId: '\$event',
type: 'm.room.message',
content: {
'msgtype': 'm.text',
'body': '<b>yay</b>',
'format': 'org.matrix.custom.html',
'formatted_body': '<b>yay</b>',
},
originServerTs: DateTime.now(),
senderId: client.userID,
));
final sent = getLastMessagePayload('m.reaction'); final sent = getLastMessagePayload('m.reaction');
expect(sent, { expect(sent, {
'm.relates_to': { 'm.relates_to': {

View File

@ -72,6 +72,7 @@ void main() {
room: room, room: room,
originServerTs: now, originServerTs: now,
eventId: '\$event', eventId: '\$event',
senderId: client.userID,
); );
final decryptedEvent = final decryptedEvent =
await client.encryption.decryptRoomEvent(roomId, encryptedEvent); await client.encryption.decryptRoomEvent(roomId, encryptedEvent);

View File

@ -36,24 +36,36 @@ void main() {
content: {}, content: {},
room: room, room: room,
stateKey: '', stateKey: '',
senderId: client.userID,
eventId: '\$fakeid1:fakeServer.notExisting',
originServerTs: DateTime.now(),
)); ));
room.setState(Event( room.setState(Event(
type: 'm.room.member', type: 'm.room.member',
content: {'membership': 'join'}, content: {'membership': 'join'},
room: room, room: room,
stateKey: client.userID, stateKey: client.userID,
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid2:fakeServer.notExisting',
originServerTs: DateTime.now(),
)); ));
room2.setState(Event( room2.setState(Event(
type: 'm.room.power_levels', type: 'm.room.power_levels',
content: {}, content: {},
room: room, room: room,
stateKey: '', stateKey: '',
senderId: client.userID,
eventId: '\$fakeid3:fakeServer.notExisting',
originServerTs: DateTime.now(),
)); ));
room2.setState(Event( room2.setState(Event(
type: 'm.room.member', type: 'm.room.member',
content: {'membership': 'join'}, content: {'membership': 'join'},
room: room, room: room,
stateKey: client.userID, stateKey: client.userID,
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid4:fakeServer.notExisting',
originServerTs: DateTime.now(),
)); ));
client.rooms.add(room); client.rooms.add(room);
client.rooms.add(room2); client.rooms.add(room2);
@ -69,6 +81,9 @@ void main() {
}, },
room: room, room: room,
stateKey: '', stateKey: '',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid5:fakeServer.notExisting',
originServerTs: DateTime.now(),
)); ));
final packs = room.getImagePacks(); final packs = room.getImagePacks();
expect(packs.length, 1); expect(packs.length, 1);
@ -95,6 +110,9 @@ void main() {
}, },
room: room, room: room,
stateKey: '', stateKey: '',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid6:fakeServer.notExisting',
originServerTs: DateTime.now(),
)); ));
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon); packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
expect(packsFlat, { expect(packsFlat, {
@ -117,6 +135,9 @@ void main() {
}, },
room: room, room: room,
stateKey: '', stateKey: '',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid7:fakeServer.notExisting',
originServerTs: DateTime.now(),
)); ));
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon); packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
expect(packsFlat, { expect(packsFlat, {
@ -137,6 +158,9 @@ void main() {
}, },
room: room, room: room,
stateKey: 'fox', stateKey: 'fox',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid8:fakeServer.notExisting',
originServerTs: DateTime.now(),
)); ));
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon); packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
expect(packsFlat, { expect(packsFlat, {
@ -177,6 +201,9 @@ void main() {
}, },
room: room2, room: room2,
stateKey: '', stateKey: '',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid9:fakeServer.notExisting',
originServerTs: DateTime.now(),
)); ));
client.accountData['im.ponies.emote_rooms'] = BasicEvent.fromJson({ client.accountData['im.ponies.emote_rooms'] = BasicEvent.fromJson({
'type': 'im.ponies.emote_rooms', 'type': 'im.ponies.emote_rooms',
@ -207,6 +234,9 @@ void main() {
}, },
room: room2, room: room2,
stateKey: 'fox', stateKey: 'fox',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid10:fakeServer.notExisting',
originServerTs: DateTime.now(),
)); ));
client.accountData['im.ponies.emote_rooms'] = BasicEvent.fromJson({ client.accountData['im.ponies.emote_rooms'] = BasicEvent.fromJson({
'type': 'im.ponies.emote_rooms', 'type': 'im.ponies.emote_rooms',

View File

@ -107,6 +107,7 @@ void main() {
room: room, room: room,
eventId: '123', eventId: '123',
content: {'alias': '#testalias:example.com'}, content: {'alias': '#testalias:example.com'},
originServerTs: DateTime.now(),
stateKey: ''), stateKey: ''),
); );
expect(room.displayname, 'testalias'); expect(room.displayname, 'testalias');
@ -120,6 +121,7 @@ void main() {
room: room, room: room,
eventId: '123', eventId: '123',
content: {'name': 'testname'}, content: {'name': 'testname'},
originServerTs: DateTime.now(),
stateKey: ''), stateKey: ''),
); );
expect(room.displayname, 'testname'); expect(room.displayname, 'testname');
@ -133,6 +135,7 @@ void main() {
room: room, room: room,
eventId: '123', eventId: '123',
content: {'topic': 'testtopic'}, content: {'topic': 'testtopic'},
originServerTs: DateTime.now(),
stateKey: ''), stateKey: ''),
); );
expect(room.topic, 'testtopic'); expect(room.topic, 'testtopic');
@ -146,6 +149,7 @@ void main() {
room: room, room: room,
eventId: '123', eventId: '123',
content: {'url': 'mxc://testurl'}, content: {'url': 'mxc://testurl'},
originServerTs: DateTime.now(),
stateKey: ''), stateKey: ''),
); );
expect(room.avatar.toString(), 'mxc://testurl'); expect(room.avatar.toString(), 'mxc://testurl');
@ -161,6 +165,7 @@ void main() {
content: { content: {
'pinned': ['1234'] 'pinned': ['1234']
}, },
originServerTs: DateTime.now(),
stateKey: ''), stateKey: ''),
); );
expect(room.pinnedEventIds.first, '1234'); expect(room.pinnedEventIds.first, '1234');
@ -358,6 +363,7 @@ void main() {
'users': {'@test:fakeServer.notExisting': 100}, 'users': {'@test:fakeServer.notExisting': 100},
'users_default': 10 'users_default': 10
}, },
originServerTs: DateTime.now(),
stateKey: ''), stateKey: ''),
); );
expect(room.ownPowerLevel, 100); expect(room.ownPowerLevel, 100);
@ -396,6 +402,7 @@ void main() {
'users': {}, 'users': {},
'users_default': 0 'users_default': 0
}, },
originServerTs: DateTime.now(),
stateKey: '', stateKey: '',
), ),
); );