From 66fce65dee250d8882f3217526ebd9be8055046e Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Fri, 21 Jun 2019 13:30:39 +0200 Subject: [PATCH] [Lists] Add RoomList List type. --- lib/src/Client.dart | 24 ++++++ lib/src/RoomList.dart | 165 +++++++++++++++++++++++++++++++++++++ lib/src/Timeline.dart | 9 ++- test/RoomList_test.dart | 175 ++++++++++++++++++++++++++++++++++++++++ test/Timeline_test.dart | 20 ++++- 5 files changed, 388 insertions(+), 5 deletions(-) create mode 100644 lib/src/RoomList.dart create mode 100644 test/RoomList_test.dart diff --git a/lib/src/Client.dart b/lib/src/Client.dart index c83fad2e..266e00f3 100644 --- a/lib/src/Client.dart +++ b/lib/src/Client.dart @@ -25,6 +25,8 @@ import 'dart:async'; import 'dart:core'; import 'responses/ErrorResponse.dart'; import 'Connection.dart'; +import 'RoomList.dart'; +import 'Room.dart'; import 'Store.dart'; import 'User.dart'; import 'responses/PushrulesResponse.dart'; @@ -189,6 +191,28 @@ class Client { await connection.clear(); } + /// Loads the Rooms from the [store] and creates a new [RoomList] object. + Future getRoomList( + {bool onlyLeft = false, + bool onlyDirect = false, + bool onlyGroups = false, + onUpdateCallback onUpdate, + onInsertCallback, + onInsert, + onRemoveCallback onRemove}) async { + List rooms = await store.getRoomList( + onlyLeft: onlyLeft, onlyGroups: onlyGroups, onlyDirect: onlyDirect); + return RoomList( + client: this, + onlyLeft: onlyLeft, + onlyDirect: onlyDirect, + onlyGroups: onlyGroups, + onUpdate: onUpdate, + onInsert: onInsert, + onRemove: onRemove, + rooms: rooms); + } + /// Creates a new group chat and invites the given Users and returns the new /// created room ID. Future createGroup(List users) async { diff --git a/lib/src/RoomList.dart b/lib/src/RoomList.dart new file mode 100644 index 00000000..0faeb9ff --- /dev/null +++ b/lib/src/RoomList.dart @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2019 Zender & Kurtz GbR. + * + * Authors: + * Christian Pauly + * Marcel Radzio + * + * This file is part of famedlysdk. + * + * famedlysdk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * famedlysdk 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with famedlysdk. If not, see . + */ + +import 'dart:async'; +import 'dart:core'; +import 'Client.dart'; +import 'Event.dart'; +import 'Room.dart'; +import 'User.dart'; +import 'utils/ChatTime.dart'; +import 'utils/MxContent.dart'; +import 'sync/EventUpdate.dart'; +import 'sync/RoomUpdate.dart'; + +/// Represents a list of rooms for this client, which will automatically update +/// itself and call the [onUpdate], [onInsert] and [onDelete] callbacks. To get +/// the initial room list, use the store or create a RoomList instance by using +/// [client.getRoomList]. +class RoomList { + final Client client; + List rooms = []; + + final bool onlyLeft; + final bool onlyDirect; + final bool onlyGroups; + + /// Will be called, when the room list has changed. Can be used e.g. to update + /// the state of a StatefulWidget. + final onUpdateCallback onUpdate; + + /// Will be called, when a new room is added to the list. + final onInsertCallback onInsert; + + /// Will be called, when a room has been removed from the list. + final onRemoveCallback onRemove; + + StreamSubscription eventSub; + StreamSubscription roomSub; + + RoomList( + {this.client, + this.rooms, + this.onUpdate, + this.onInsert, + this.onRemove, + this.onlyLeft = false, + this.onlyDirect = false, + this.onlyGroups = false}) { + eventSub ??= client.connection.onEvent.stream.listen(_handleEventUpdate); + roomSub ??= client.connection.onRoomUpdate.stream.listen(_handleRoomUpdate); + } + + void _handleRoomUpdate(RoomUpdate chatUpdate) async { + // Update the chat list item. + // Search the room in the rooms + num j = 0; + for (j = 0; j < rooms.length; j++) { + if (rooms[j].id == chatUpdate.id) break; + } + final bool found = (j < rooms.length - 1 && rooms[j].id == chatUpdate.id); + + // Does the chat already exist in the list rooms? + if (!found && chatUpdate.membership != "leave") { + num position = chatUpdate.membership == "invite" ? 0 : j; + ChatTime timestamp = + chatUpdate.membership == "invite" ? ChatTime.now() : ChatTime(0); + // Add the new chat to the list + Room newRoom = Room( + id: chatUpdate.id, + name: "", + membership: chatUpdate.membership, + prev_batch: chatUpdate.prev_batch, + highlightCount: chatUpdate.highlight_count, + notificationCount: chatUpdate.notification_count); + rooms.insert(position, newRoom); + onInsert(position); + } + // If the membership is "leave" then remove the item and stop here + else if (found && chatUpdate.membership == "leave") { + final Room removed = rooms.removeAt(j); + onRemove(j); + } + // Update notification and highlight count + else if (found && + chatUpdate.membership != "leave" && + (rooms[j].notificationCount != chatUpdate.notification_count || + rooms[j].highlightCount != chatUpdate.highlight_count)) { + rooms[j].notificationCount = chatUpdate.notification_count; + rooms[j].highlightCount = chatUpdate.highlight_count; + } + sortAndUpdate(); + } + + void _handleEventUpdate(EventUpdate eventUpdate) { + // Is the event necessary for the chat list? If not, then return + if (!(eventUpdate.type == "timeline" || + eventUpdate.eventType == "m.room.avatar" || + eventUpdate.eventType == "m.room.name")) return; + + // Search the room in the rooms + num j = 0; + for (j = 0; j < rooms.length; j++) { + if (rooms[j].id == eventUpdate.roomID) break; + } + final bool found = (j < rooms.length && rooms[j].id == eventUpdate.roomID); + if (!found) return; + + // Is this an old timeline event? Then stop here... + /*if (eventUpdate.type == "timeline" && + ChatTime(eventUpdate.content["origin_server_ts"]) <= + rooms[j].timeCreated) return;*/ + + if (eventUpdate.type == "timeline") { + // Update the last message preview + String body = eventUpdate.content["content"]["body"] ?? ""; + rooms[j].lastEvent = Event( + eventUpdate.content["id"], + User(eventUpdate.content["sender"]), + ChatTime(eventUpdate.content["origin_server_ts"]), + room: rooms[j], + content: eventUpdate.content["content"], + environment: "timeline", + status: 2, + ); + } + if (eventUpdate.eventType == "m.room.name") { + // Update the room name + rooms[j].name = eventUpdate.content["content"]["name"]; + } else if (eventUpdate.eventType == "m.room.avatar") { + // Update the room avatar + rooms[j].avatar = MxContent(eventUpdate.content["content"]["url"]); + } + sortAndUpdate(); + } + + sortAndUpdate() { + rooms?.sort((a, b) => + b.timeCreated.toTimeStamp().compareTo(a.timeCreated.toTimeStamp())); + onUpdate(); + } +} + +typedef onUpdateCallback = void Function(); +typedef onInsertCallback = void Function(int insertID); +typedef onRemoveCallback = void Function(int insertID); diff --git a/lib/src/Timeline.dart b/lib/src/Timeline.dart index 65dc665b..04a23b59 100644 --- a/lib/src/Timeline.dart +++ b/lib/src/Timeline.dart @@ -25,7 +25,6 @@ import 'dart:async'; import 'Event.dart'; import 'Room.dart'; import 'User.dart'; -import 'utils/ChatTime.dart'; import 'sync/EventUpdate.dart'; /// Represents the timeline of a room. The callbacks [onUpdate], [onDelete], @@ -63,12 +62,18 @@ class Timeline { events.insert(0, newEvent); onInsert(0); } - onUpdate(); + sortAndUpdate(); } catch (e) { print("[WARNING] ${e.toString()}"); sub.cancel(); } } + + sortAndUpdate() { + events + ?.sort((a, b) => b.time.toTimeStamp().compareTo(a.time.toTimeStamp())); + onUpdate(); + } } typedef onUpdateCallback = void Function(); diff --git a/test/RoomList_test.dart b/test/RoomList_test.dart new file mode 100644 index 00000000..abd1c4f8 --- /dev/null +++ b/test/RoomList_test.dart @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2019 Zender & Kurtz GbR. + * + * Authors: + * Christian Pauly + * Marcel Radzio + * + * This file is part of famedlysdk. + * + * famedlysdk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * famedlysdk 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with famedlysdk. If not, see . + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:famedlysdk/src/Client.dart'; +import 'package:famedlysdk/src/Event.dart'; +import 'package:famedlysdk/src/Room.dart'; +import 'package:famedlysdk/src/RoomList.dart'; +import 'package:famedlysdk/src/User.dart'; +import 'package:famedlysdk/src/sync/EventUpdate.dart'; +import 'package:famedlysdk/src/sync/RoomUpdate.dart'; +import 'package:famedlysdk/src/utils/ChatTime.dart'; + +void main() { + /// All Tests related to the MxContent + group("RoomList", () { + final roomID = "!1:example.com"; + + test("Create and insert one room", () async { + final Client client = Client("testclient"); + client.homeserver = "https://testserver.abc"; + + int updateCount = 0; + List insertList = []; + List removeList = []; + + RoomList roomList = RoomList( + client: client, + rooms: [], + onUpdate: () { + updateCount++; + }, + onInsert: (int insertID) { + insertList.add(insertID); + }, + onRemove: (int removeID) { + insertList.add(removeID); + }); + + expect(roomList.eventSub != null, true); + expect(roomList.roomSub != null, true); + + client.connection.onRoomUpdate.add(RoomUpdate( + id: roomID, + membership: "join", + notification_count: 2, + highlight_count: 1, + limitedTimeline: false, + prev_batch: "1234", + )); + + await new Future.delayed(new Duration(milliseconds: 50)); + + expect(updateCount, 1); + expect(insertList, [0]); + expect(removeList, []); + + expect(roomList.rooms.length, 1); + expect(roomList.rooms[0].id, roomID); + expect(roomList.rooms[0].membership, "join"); + expect(roomList.rooms[0].notificationCount, 2); + expect(roomList.rooms[0].highlightCount, 1); + expect(roomList.rooms[0].prev_batch, "1234"); + expect(roomList.rooms[0].timeCreated, ChatTime.now()); + }); + + test("Restort", () async { + final Client client = Client("testclient"); + client.homeserver = "https://testserver.abc"; + + int updateCount = 0; + List insertList = []; + List removeList = []; + + RoomList roomList = RoomList( + client: client, + rooms: [], + onUpdate: () { + updateCount++; + }, + onInsert: (int insertID) { + insertList.add(insertID); + }, + onRemove: (int removeID) { + insertList.add(removeID); + }); + + client.connection.onRoomUpdate.add(RoomUpdate( + id: "1", + membership: "join", + notification_count: 2, + highlight_count: 1, + limitedTimeline: false, + prev_batch: "1234", + )); + client.connection.onRoomUpdate.add(RoomUpdate( + id: "2", + membership: "join", + notification_count: 2, + highlight_count: 1, + limitedTimeline: false, + prev_batch: "1234", + )); + + await new Future.delayed(new Duration(milliseconds: 50)); + + expect(roomList.eventSub != null, true); + expect(roomList.roomSub != null, true); + expect(roomList.rooms[0].id, "1"); + expect(roomList.rooms[1].id, "2"); + + ChatTime now = ChatTime.now(); + + client.connection.onEvent.add(EventUpdate( + type: "timeline", + roomID: "1", + eventType: "m.room.message", + content: { + "type": "m.room.message", + "content": {"msgtype": "m.text", "body": "Testcase"}, + "sender": "@alice:example.com", + "status": 2, + "id": "1", + "origin_server_ts": now.toTimeStamp() - 1000 + })); + + client.connection.onEvent.add(EventUpdate( + type: "timeline", + roomID: "2", + eventType: "m.room.message", + content: { + "type": "m.room.message", + "content": {"msgtype": "m.text", "body": "Testcase 2"}, + "sender": "@alice:example.com", + "status": 2, + "id": "2", + "origin_server_ts": now.toTimeStamp() + })); + + await new Future.delayed(new Duration(milliseconds: 50)); + + expect(updateCount, 4); + expect(insertList, [0, 1]); + expect(removeList, []); + + expect(roomList.rooms.length, 2); + expect( + roomList.rooms[0].timeCreated > roomList.rooms[1].timeCreated, true); + expect(roomList.rooms[0].id, "2"); + expect(roomList.rooms[1].id, "1"); + expect(roomList.rooms[0].lastMessage, "Testcase 2"); + expect(roomList.rooms[0].timeCreated, now); + }); + }); +} diff --git a/test/Timeline_test.dart b/test/Timeline_test.dart index 14f7c993..2576aca1 100644 --- a/test/Timeline_test.dart +++ b/test/Timeline_test.dart @@ -64,18 +64,32 @@ void main() { "origin_server_ts": testTimeStamp })); + client.connection.onEvent.add(EventUpdate( + type: "timeline", + roomID: roomID, + eventType: "m.room.message", + content: { + "type": "m.room.message", + "content": {"msgtype": "m.text", "body": "Testcase"}, + "sender": "@alice:example.com", + "status": 2, + "id": "2", + "origin_server_ts": testTimeStamp - 1000 + })); + expect(timeline.sub != null, true); await new Future.delayed(new Duration(milliseconds: 50)); - expect(updateCount, 1); - expect(insertList, [0]); - expect(timeline.events.length, 1); + expect(updateCount, 2); + expect(insertList, [0, 0]); + expect(timeline.events.length, 2); expect(timeline.events[0].id, "1"); expect(timeline.events[0].sender.id, "@alice:example.com"); expect(timeline.events[0].time.toTimeStamp(), testTimeStamp); expect(timeline.events[0].environment, "m.room.message"); expect(timeline.events[0].getBody(), "Testcase"); + expect(timeline.events[0].time > timeline.events[1].time, true); }); }); }