From 32141618b6a3d07be84b8f882404cf802d0db273 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Sun, 20 Oct 2019 09:44:14 +0000 Subject: [PATCH] [Ephemerals] Add new features --- lib/src/Connection.dart | 66 ++++++++++++++++++++++-------------- lib/src/Event.dart | 17 ++++++++++ lib/src/Room.dart | 14 ++++++++ lib/src/RoomAccountData.dart | 1 + lib/src/RoomList.dart | 23 ++++++++----- lib/src/utils/Receipt.dart | 11 ++++++ test/Client_test.dart | 44 ++++++++++++++++++------ test/FakeMatrixApi.dart | 11 ++++++ test/Timeline_test.dart | 19 +++++++++++ 9 files changed, 162 insertions(+), 44 deletions(-) create mode 100644 lib/src/utils/Receipt.dart diff --git a/lib/src/Connection.dart b/lib/src/Connection.dart index e90d2b7..39b0edb 100644 --- a/lib/src/Connection.dart +++ b/lib/src/Connection.dart @@ -422,9 +422,9 @@ class Connection { room["timeline"]["events"] is List) _handleRoomEvents(id, room["timeline"]["events"], "timeline"); - if (room["ephemetal"] is Map && - room["ephemetal"]["events"] is List) - _handleEphemerals(id, room["ephemetal"]["events"]); + if (room["ephemeral"] is Map && + room["ephemeral"]["events"] is List) + _handleEphemerals(id, room["ephemeral"]["events"]); if (room["account_data"] is Map && room["account_data"]["events"] is List) @@ -434,32 +434,48 @@ class Connection { void _handleEphemerals(String id, List events) { for (num i = 0; i < events.length; i++) { - if (!(events[i]["type"] is String && - events[i]["content"] is Map)) continue; + _handleEvent(events[i], id, "ephemeral"); + + // Receipt events are deltas between two states. We will create a + // fake room account data event for this and store the difference + // there. if (events[i]["type"] == "m.receipt") { - events[i]["content"].forEach((String e, dynamic value) { - if (!(events[i]["content"][e] is Map && - events[i]["content"][e]["m.read"] is Map)) - return; - events[i]["content"][e]["m.read"] - .forEach((String user, dynamic value) async { - if (!(events[i]["content"][e]["m.read"]["user"] - is Map && - events[i]["content"][e]["m.read"]["ts"] is num)) return; + Room room = client.roomList.getRoomById(id); + if (room == null) room = Room(id: id); - _handleEvent(events[i], id, "ephemeral"); - }); - }); - } else if (events[i]["type"] == "m.typing") { - if (!(events[i]["content"]["user_ids"] is List)) continue; + Map receiptStateContent = + room.roomAccountData["m.receipt"]?.content ?? {}; + for (var eventEntry in events[i]["content"].entries) { + final String eventID = eventEntry.key; + if (events[i]["content"][eventID]["m.read"] != null) { + final Map userTimestampMap = + events[i]["content"][eventID]["m.read"]; + for (var userTimestampMapEntry in userTimestampMap.entries) { + final String mxid = userTimestampMapEntry.key; - List user_ids = events[i]["content"]["user_ids"]; + // Remove previous receipt event from this user + for (var entry in receiptStateContent.entries) { + if (entry.value["m.read"] is Map && + entry.value["m.read"].containsKey(mxid)) { + entry.value["m.read"].remove(mxid); + break; + } + } - /// If the user is typing, remove his id from the list of typing users - var ownTyping = user_ids.indexOf(client.userID); - if (ownTyping != -1) user_ids.removeAt(1); - - _handleEvent(events[i], id, "ephemeral"); + if (userTimestampMap[mxid]["ts"] is int) { + if (receiptStateContent[eventID] == null) + receiptStateContent[eventID] = {"m.read": {}}; + else if (receiptStateContent[eventID]["m.read"]) + receiptStateContent[eventID]["m.read"] = {}; + receiptStateContent[eventID]["m.read"][mxid] = { + "ts": userTimestampMap[mxid]["ts"], + }; + } + } + } + } + events[i]["content"] = receiptStateContent; + _handleEvent(events[i], id, "account_data"); } } } diff --git a/lib/src/Event.dart b/lib/src/Event.dart index b03c371..7a73b06 100644 --- a/lib/src/Event.dart +++ b/lib/src/Event.dart @@ -24,8 +24,10 @@ import 'package:famedlysdk/src/RoomState.dart'; import 'package:famedlysdk/src/sync/EventUpdate.dart'; import 'package:famedlysdk/src/utils/ChatTime.dart'; +import 'package:famedlysdk/src/utils/Receipt.dart'; import './Room.dart'; +import 'User.dart'; /// Defines a timeline event for a room. class Event extends RoomState { @@ -97,6 +99,21 @@ class Event extends RoomState { return "$type"; } + /// Returns a list of [Receipt] instances for this event. + List get receipts { + if (!(room.roomAccountData.containsKey("m.receipt") && + room.roomAccountData["m.receipt"].content.containsKey(eventId))) + return []; + List receiptsList = []; + for (var entry in room + .roomAccountData["m.receipt"].content[eventId]["m.read"].entries) { + receiptsList.add(Receipt( + room.states[entry.key]?.asUser ?? User(entry.key), + ChatTime(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 remove() async { diff --git a/lib/src/Room.dart b/lib/src/Room.dart index e52235f..df14e58 100644 --- a/lib/src/Room.dart +++ b/lib/src/Room.dart @@ -69,6 +69,9 @@ class Room { /// Key-Value store for room states. Map states = {}; + /// Key-Value store for ephemerals. + Map ephemerals = {}; + /// Key-Value store for private account data only visible for this user. Map roomAccountData = {}; @@ -161,6 +164,17 @@ class Room { return lastEvent; } + /// Returns a list of all current typing users. + List get typingUsers { + if (!ephemerals.containsKey("m.typing")) return []; + List typingMxid = ephemerals["m.typing"].content["user_ids"]; + List typingUsers = []; + for (int i = 0; i < typingMxid.length; i++) + typingUsers.add( + states[typingMxid[i]]?.asUser ?? User(typingMxid[i], room: this)); + return typingUsers; + } + /// Your current client instance. final Client client; diff --git a/lib/src/RoomAccountData.dart b/lib/src/RoomAccountData.dart index 919dcfa..e139a6a 100644 --- a/lib/src/RoomAccountData.dart +++ b/lib/src/RoomAccountData.dart @@ -25,6 +25,7 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/src/AccountData.dart'; import 'package:famedlysdk/src/RoomState.dart'; +/// Stripped down events for account data and ephemrals of a room. class RoomAccountData extends AccountData { /// The user who has sent this event if it is not a global account data event. final String roomId; diff --git a/lib/src/RoomList.dart b/lib/src/RoomList.dart index 0803ea4..9f38dc3 100644 --- a/lib/src/RoomList.dart +++ b/lib/src/RoomList.dart @@ -24,6 +24,7 @@ import 'dart:async'; import 'dart:core'; +import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/src/RoomState.dart'; import 'Client.dart'; @@ -147,9 +148,6 @@ class RoomList { } void _handleEventUpdate(EventUpdate eventUpdate) { - if (eventUpdate.type != "timeline" && - eventUpdate.type != "state" && - eventUpdate.type != "invite_state") return; // Search the room in the rooms num j = 0; for (j = 0; j < rooms.length; j++) { @@ -157,11 +155,20 @@ class RoomList { } final bool found = (j < rooms.length && rooms[j].id == eventUpdate.roomID); if (!found) return; - - RoomState stateEvent = RoomState.fromJson(eventUpdate.content, rooms[j]); - if (rooms[j].states[stateEvent.key] != null && - rooms[j].states[stateEvent.key].time > stateEvent.time) return; - rooms[j].states[stateEvent.key] = stateEvent; + if (eventUpdate.type == "timeline" || + eventUpdate.type == "state" || + eventUpdate.type == "invite_state") { + RoomState stateEvent = RoomState.fromJson(eventUpdate.content, rooms[j]); + if (rooms[j].states[stateEvent.key] != null && + rooms[j].states[stateEvent.key].time > stateEvent.time) return; + rooms[j].states[stateEvent.key] = stateEvent; + } else if (eventUpdate.type == "account_data") { + rooms[j].roomAccountData[eventUpdate.eventType] = + RoomAccountData.fromJson(eventUpdate.content, rooms[j]); + } else if (eventUpdate.type == "ephemeral") { + rooms[j].ephemerals[eventUpdate.eventType] = + RoomAccountData.fromJson(eventUpdate.content, rooms[j]); + } if (rooms[j].onUpdate != null) rooms[j].onUpdate(); sortAndUpdate(); } diff --git a/lib/src/utils/Receipt.dart b/lib/src/utils/Receipt.dart new file mode 100644 index 0000000..6115530 --- /dev/null +++ b/lib/src/utils/Receipt.dart @@ -0,0 +1,11 @@ +import 'package:famedlysdk/src/utils/ChatTime.dart'; +import '../User.dart'; + +/// Represents a receipt. +/// This [user] has read an event at the given [time]. +class Receipt { + final User user; + final ChatTime time; + + const Receipt(this.user, this.time); +} diff --git a/test/Client_test.dart b/test/Client_test.dart index f82ee88..bb2de3f 100644 --- a/test/Client_test.dart +++ b/test/Client_test.dart @@ -103,6 +103,7 @@ void main() { newDeviceID: resp["device_id"], newMatrixVersions: matrix.matrixVersions, newLazyLoadMembers: matrix.lazyLoadMembers); + await new Future.delayed(new Duration(milliseconds: 50)); expect(matrix.accessToken == resp["access_token"], true); expect(matrix.deviceName == "Text Matrix Client", true); @@ -123,6 +124,15 @@ void main() { expect(matrix.roomList.rooms[1].directChatMatrixID, "@bob:example.com"); expect(matrix.directChats, matrix.accountData["m.direct"].content); expect(matrix.presences.length, 1); + expect(matrix.roomList.rooms[1].ephemerals.length, 2); + expect(matrix.roomList.rooms[1].typingUsers.length, 1); + expect(matrix.roomList.rooms[1].typingUsers[0].id, "@alice:example.com"); + expect(matrix.roomList.rooms[1].roomAccountData.length, 3); + expect( + matrix.roomList.rooms[1].roomAccountData["m.receipt"] + .content["7365636s6r6432:example.com"]["m.read"] + ["@alice:example.com"]["ts"], + 1436451550453); expect(matrix.roomList.rooms.length, 2); expect(matrix.roomList.rooms[1].canonicalAlias, "#famedlyContactDiscovery:${matrix.userID.split(":")[1]}"); @@ -223,7 +233,7 @@ void main() { List eventUpdateList = await eventUpdateListFuture; - expect(eventUpdateList.length, 9); + expect(eventUpdateList.length, 12); expect(eventUpdateList[0].eventType, "m.room.member"); expect(eventUpdateList[0].roomID, "!726s6s6q:example.com"); @@ -241,21 +251,33 @@ void main() { expect(eventUpdateList[3].roomID, "!726s6s6q:example.com"); expect(eventUpdateList[3].type, "timeline"); - expect(eventUpdateList[4].eventType, "m.tag"); + expect(eventUpdateList[4].eventType, "m.typing"); expect(eventUpdateList[4].roomID, "!726s6s6q:example.com"); - expect(eventUpdateList[4].type, "account_data"); + expect(eventUpdateList[4].type, "ephemeral"); - expect(eventUpdateList[5].eventType, "org.example.custom.room.config"); + expect(eventUpdateList[5].eventType, "m.receipt"); expect(eventUpdateList[5].roomID, "!726s6s6q:example.com"); - expect(eventUpdateList[5].type, "account_data"); + expect(eventUpdateList[5].type, "ephemeral"); - expect(eventUpdateList[6].eventType, "m.room.name"); - expect(eventUpdateList[6].roomID, "!696r7674:example.com"); - expect(eventUpdateList[6].type, "invite_state"); + expect(eventUpdateList[6].eventType, "m.receipt"); + expect(eventUpdateList[6].roomID, "!726s6s6q:example.com"); + expect(eventUpdateList[6].type, "account_data"); - expect(eventUpdateList[7].eventType, "m.room.member"); - expect(eventUpdateList[7].roomID, "!696r7674:example.com"); - expect(eventUpdateList[7].type, "invite_state"); + expect(eventUpdateList[7].eventType, "m.tag"); + expect(eventUpdateList[7].roomID, "!726s6s6q:example.com"); + expect(eventUpdateList[7].type, "account_data"); + + expect(eventUpdateList[8].eventType, "org.example.custom.room.config"); + expect(eventUpdateList[8].roomID, "!726s6s6q:example.com"); + expect(eventUpdateList[8].type, "account_data"); + + expect(eventUpdateList[9].eventType, "m.room.name"); + expect(eventUpdateList[9].roomID, "!696r7674:example.com"); + expect(eventUpdateList[9].type, "invite_state"); + + expect(eventUpdateList[10].eventType, "m.room.member"); + expect(eventUpdateList[10].roomID, "!696r7674:example.com"); + expect(eventUpdateList[10].type, "invite_state"); }); test('User Update Test', () async { diff --git a/test/FakeMatrixApi.dart b/test/FakeMatrixApi.dart index 7169f00..ead161f 100644 --- a/test/FakeMatrixApi.dart +++ b/test/FakeMatrixApi.dart @@ -443,6 +443,17 @@ class FakeMatrixApi extends MockClient { "content": { "user_ids": ["@alice:example.com"] } + }, + { + "content": { + "7365636s6r6432:example.com": { + "m.read": { + "@alice:example.com": {"ts": 1436451550453} + } + } + }, + "room_id": "!726s6s6q:example.com", + "type": "m.receipt" } ] }, diff --git a/test/Timeline_test.dart b/test/Timeline_test.dart index 9d061c7..970a8b1 100644 --- a/test/Timeline_test.dart +++ b/test/Timeline_test.dart @@ -21,6 +21,7 @@ * along with famedlysdk. If not, see . */ +import 'package:famedlysdk/src/RoomAccountData.dart'; import 'package:test/test.dart'; import 'package:famedlysdk/src/Client.dart'; import 'package:famedlysdk/src/Room.dart'; @@ -97,6 +98,24 @@ void main() { expect(timeline.events[0].time.toTimeStamp(), testTimeStamp); expect(timeline.events[0].getBody(), "Testcase"); expect(timeline.events[0].time > timeline.events[1].time, true); + expect(timeline.events[0].receipts, []); + + room.roomAccountData["m.receipt"] = RoomAccountData.fromJson({ + "type": "m.receipt", + "content": { + "1": { + "m.read": { + "@alice:example.com": {"ts": 1436451550453} + } + } + }, + "room_id": roomID, + }, room); + + await new Future.delayed(new Duration(milliseconds: 50)); + + expect(timeline.events[0].receipts.length, 1); + expect(timeline.events[0].receipts[0].user.id, "@alice:example.com"); }); test("Send message", () async {