371 lines
11 KiB
Dart
371 lines
11 KiB
Dart
import 'package:matrix/matrix.dart';
|
|
import 'package:matrix/matrix_api_lite/generated/internal.dart';
|
|
import 'package:matrix/src/models/timeline_chunk.dart';
|
|
|
|
class Thread {
|
|
final Room room;
|
|
final Event rootEvent;
|
|
Event? lastEvent;
|
|
String? prev_batch;
|
|
bool? currentUserParticipated;
|
|
int? count;
|
|
final Client client;
|
|
|
|
Thread({
|
|
required this.room,
|
|
required this.rootEvent,
|
|
required this.client,
|
|
required this.currentUserParticipated,
|
|
required this.count,
|
|
this.prev_batch,
|
|
this.lastEvent,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
...rootEvent.toJson(),
|
|
'unsigned': {
|
|
'm.thread': {
|
|
'latest_event': lastEvent?.toJson(),
|
|
'count': count,
|
|
'current_user_participated': currentUserParticipated,
|
|
},
|
|
},
|
|
};
|
|
|
|
factory Thread.fromJson(Map<String, dynamic> json, Client client) {
|
|
final room = client.getRoomById(json['room_id']);
|
|
if (room == null) throw Error();
|
|
Event? lastEvent;
|
|
if (json['unsigned']?['m.relations']?['m.thread']?['latest_event'] !=
|
|
null) {
|
|
lastEvent = Event.fromMatrixEvent(
|
|
MatrixEvent.fromJson(
|
|
json['unsigned']?['m.relations']?['m.thread']?['latest_event'],
|
|
),
|
|
room,
|
|
);
|
|
}
|
|
if (json['unsigned']?['m.thread']?['latest_event'] != null) {
|
|
lastEvent = Event.fromMatrixEvent(
|
|
MatrixEvent.fromJson(
|
|
json['unsigned']?['m.thread']?['latest_event'],
|
|
),
|
|
room,
|
|
);
|
|
}
|
|
// Although I was making this part according to specification, it's a bit off
|
|
// I have no clue why
|
|
final thread = Thread(
|
|
room: room,
|
|
client: client,
|
|
rootEvent: Event.fromMatrixEvent(
|
|
MatrixEvent.fromJson(json),
|
|
room,
|
|
),
|
|
lastEvent: lastEvent,
|
|
count: json['unsigned']?['m.relations']?['m.thread']?['count'],
|
|
currentUserParticipated: json['unsigned']?['m.relations']?['m.thread']
|
|
?['current_user_participated'],
|
|
);
|
|
return thread;
|
|
}
|
|
|
|
Future<Event?>? _refreshingLastEvent;
|
|
|
|
Future<Event?> _refreshLastEvent({
|
|
timeout = const Duration(seconds: 30),
|
|
}) async {
|
|
if (room.membership != Membership.join) return null;
|
|
|
|
final result = await client
|
|
.getRelatingEventsWithRelType(
|
|
room.id,
|
|
rootEvent.eventId,
|
|
'm.thread',
|
|
)
|
|
.timeout(timeout);
|
|
final matrixEvent = result.chunk.firstOrNull;
|
|
if (matrixEvent == null) {
|
|
if (lastEvent?.type == EventTypes.refreshingLastEvent) {
|
|
lastEvent = null;
|
|
}
|
|
Logs().d(
|
|
'No last event found for thread ${rootEvent.eventId} in ${rootEvent.roomId}',
|
|
);
|
|
return null;
|
|
}
|
|
var event = Event.fromMatrixEvent(
|
|
matrixEvent,
|
|
room,
|
|
status: EventStatus.synced,
|
|
);
|
|
if (event.type == EventTypes.Encrypted) {
|
|
final encryption = client.encryption;
|
|
if (encryption != null) {
|
|
event = await encryption.decryptRoomEvent(event);
|
|
}
|
|
}
|
|
lastEvent = event;
|
|
|
|
return event;
|
|
}
|
|
|
|
/// When was the last event received.
|
|
DateTime get latestEventReceivedTime {
|
|
final lastEventTime = lastEvent?.originServerTs;
|
|
if (lastEventTime != null) return lastEventTime;
|
|
|
|
if (room.membership == Membership.invite) return DateTime.now();
|
|
|
|
return rootEvent.originServerTs;
|
|
}
|
|
|
|
bool get hasNewMessages {
|
|
// TODO: Implement this
|
|
return false;
|
|
}
|
|
|
|
Future<TimelineChunk?> getEventContext(String eventId) async {
|
|
// TODO: probably find events with relationship
|
|
final resp = await client.getEventContext(
|
|
room.id, eventId,
|
|
limit: Room.defaultHistoryCount,
|
|
// filter: jsonEncode(StateFilter(lazyLoadMembers: true).toJson()),
|
|
);
|
|
|
|
final events = [
|
|
if (resp.eventsAfter != null) ...resp.eventsAfter!.reversed,
|
|
if (resp.event != null) resp.event!,
|
|
if (resp.eventsBefore != null) ...resp.eventsBefore!,
|
|
].map((e) => Event.fromMatrixEvent(e, room)).toList();
|
|
|
|
// Try again to decrypt encrypted events but don't update the database.
|
|
if (room.encrypted && client.encryptionEnabled) {
|
|
for (var i = 0; i < events.length; i++) {
|
|
if (events[i].type == EventTypes.Encrypted &&
|
|
events[i].content['can_request_session'] == true) {
|
|
events[i] = await client.encryption!.decryptRoomEvent(events[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
final chunk = TimelineChunk(
|
|
nextBatch: resp.end ?? '',
|
|
prevBatch: resp.start ?? '',
|
|
events: events,
|
|
);
|
|
|
|
return chunk;
|
|
}
|
|
|
|
Future<ThreadTimeline> getTimeline({
|
|
void Function(int index)? onChange,
|
|
void Function(int index)? onRemove,
|
|
void Function(int insertID)? onInsert,
|
|
void Function()? onNewEvent,
|
|
void Function()? onUpdate,
|
|
String? eventContextId,
|
|
int? limit = Room.defaultHistoryCount,
|
|
}) async {
|
|
// await postLoad();
|
|
|
|
var events = <Event>[];
|
|
|
|
await client.database.transaction(() async {
|
|
events = await client.database.getThreadEventList(
|
|
this,
|
|
limit: limit,
|
|
);
|
|
});
|
|
|
|
var chunk = TimelineChunk(events: events);
|
|
// Load the timeline arround eventContextId if set
|
|
if (eventContextId != null) {
|
|
if (!events.any((Event event) => event.eventId == eventContextId)) {
|
|
chunk =
|
|
await getEventContext(eventContextId) ?? TimelineChunk(events: []);
|
|
}
|
|
}
|
|
|
|
final timeline = ThreadTimeline(
|
|
thread: this,
|
|
chunk: chunk,
|
|
onChange: onChange,
|
|
onRemove: onRemove,
|
|
onInsert: onInsert,
|
|
onNewEvent: onNewEvent,
|
|
onUpdate: onUpdate,
|
|
);
|
|
|
|
// Fetch all users from database we have got here.
|
|
if (eventContextId == null) {
|
|
final userIds = events.map((event) => event.senderId).toSet();
|
|
for (final userId in userIds) {
|
|
if (room.getState(EventTypes.RoomMember, userId) != null) continue;
|
|
final dbUser = await client.database.getUser(userId, room);
|
|
if (dbUser != null) room.setState(dbUser);
|
|
}
|
|
}
|
|
|
|
// Try again to decrypt encrypted events and update the database.
|
|
if (room.encrypted && client.encryptionEnabled) {
|
|
// decrypt messages
|
|
for (var i = 0; i < chunk.events.length; i++) {
|
|
if (chunk.events[i].type == EventTypes.Encrypted) {
|
|
if (eventContextId != null) {
|
|
// for the fragmented timeline, we don't cache the decrypted
|
|
//message in the database
|
|
chunk.events[i] = await client.encryption!.decryptRoomEvent(
|
|
chunk.events[i],
|
|
);
|
|
} else {
|
|
// else, we need the database
|
|
await client.database.transaction(() async {
|
|
for (var i = 0; i < chunk.events.length; i++) {
|
|
if (chunk.events[i].content['can_request_session'] == true) {
|
|
chunk.events[i] = await client.encryption!.decryptRoomEvent(
|
|
chunk.events[i],
|
|
store: !room.isArchived,
|
|
updateType: EventUpdateType.history,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return timeline;
|
|
}
|
|
|
|
Future<String?> sendTextEvent(
|
|
String message, {
|
|
String? txid,
|
|
Event? inReplyTo,
|
|
String? editEventId,
|
|
bool parseMarkdown = true,
|
|
bool parseCommands = true,
|
|
String msgtype = MessageTypes.Text,
|
|
StringBuffer? commandStdout,
|
|
bool addMentions = true,
|
|
|
|
/// Displays an event in the timeline with the transaction ID as the event
|
|
/// ID and a status of SENDING, SENT or ERROR until it gets replaced by
|
|
/// the sync event. Using this can display a different sort order of events
|
|
/// as the sync event does replace but not relocate the pending event.
|
|
bool displayPendingEvent = true,
|
|
}) {
|
|
return room.sendTextEvent(
|
|
message,
|
|
txid: txid,
|
|
inReplyTo: inReplyTo,
|
|
editEventId: editEventId,
|
|
parseCommands: parseCommands,
|
|
parseMarkdown: parseMarkdown,
|
|
msgtype: msgtype,
|
|
commandStdout: commandStdout,
|
|
addMentions: addMentions,
|
|
displayPendingEvent: displayPendingEvent,
|
|
threadLastEventId: lastEvent?.eventId,
|
|
threadRootEventId: rootEvent.eventId,
|
|
);
|
|
}
|
|
|
|
Future<String?> sendLocation(String body, String geoUri, {String? txid}) {
|
|
final event = <String, dynamic>{
|
|
'msgtype': 'm.location',
|
|
'body': body,
|
|
'geo_uri': geoUri,
|
|
};
|
|
return room.sendEvent(
|
|
event,
|
|
txid: txid,
|
|
threadLastEventId: lastEvent?.eventId,
|
|
threadRootEventId: rootEvent.eventId,
|
|
);
|
|
}
|
|
|
|
Future<String?> sendFileEvent(
|
|
MatrixFile file, {
|
|
String? txid,
|
|
Event? inReplyTo,
|
|
String? editEventId,
|
|
int? shrinkImageMaxDimension,
|
|
MatrixImageFile? thumbnail,
|
|
Map<String, dynamic>? extraContent,
|
|
|
|
/// Displays an event in the timeline with the transaction ID as the event
|
|
/// ID and a status of SENDING, SENT or ERROR until it gets replaced by
|
|
/// the sync event. Using this can display a different sort order of events
|
|
/// as the sync event does replace but not relocate the pending event.
|
|
bool displayPendingEvent = true,
|
|
}) async {
|
|
return await room.sendFileEvent(
|
|
file,
|
|
txid: txid,
|
|
inReplyTo: inReplyTo,
|
|
editEventId: editEventId,
|
|
shrinkImageMaxDimension: shrinkImageMaxDimension,
|
|
thumbnail: thumbnail,
|
|
extraContent: extraContent,
|
|
displayPendingEvent: displayPendingEvent,
|
|
threadLastEventId: lastEvent?.eventId,
|
|
threadRootEventId: rootEvent.eventId,
|
|
);
|
|
}
|
|
|
|
Future<void> setReadMarker({String? eventId, bool? public}) async {
|
|
if (eventId == null) return null;
|
|
return await client.postReceipt(
|
|
room.id,
|
|
(public ?? client.receiptsPublicByDefault)
|
|
? ReceiptType.mRead
|
|
: ReceiptType.mReadPrivate,
|
|
eventId,
|
|
threadId: rootEvent.eventId,
|
|
);
|
|
}
|
|
|
|
Future<int> requestHistory({
|
|
int historyCount = Room.defaultHistoryCount,
|
|
void Function()? onHistoryReceived,
|
|
direction = Direction.b,
|
|
StateFilter? filter,
|
|
}) async {
|
|
final prev_batch = this.prev_batch;
|
|
|
|
final storeInDatabase = !room.isArchived;
|
|
|
|
// Ensure stateFilter is not null and set lazyLoadMembers to true if not already set
|
|
filter ??= StateFilter(lazyLoadMembers: true);
|
|
filter.lazyLoadMembers ??= true;
|
|
|
|
if (prev_batch == null) {
|
|
throw 'Tried to request history without a prev_batch token';
|
|
}
|
|
|
|
final resp = await client.getRelatingEventsWithRelType(
|
|
room.id,
|
|
rootEvent.eventId,
|
|
RelationshipTypes.thread,
|
|
from: prev_batch,
|
|
limit: historyCount,
|
|
dir: direction,
|
|
recurse: true,
|
|
);
|
|
|
|
if (onHistoryReceived != null) onHistoryReceived();
|
|
|
|
await client.database.transaction(() async {
|
|
if (storeInDatabase && direction == Direction.b) {
|
|
this.prev_batch = resp.prevBatch;
|
|
await client.database.setThreadPrevBatch(
|
|
resp.prevBatch, room.id, rootEvent.eventId, client);
|
|
}
|
|
});
|
|
|
|
return resp.chunk.length;
|
|
}
|
|
}
|