[Lists] Add RoomList List type.

This commit is contained in:
Christian Pauly 2019-06-21 13:30:39 +02:00
parent 1b1abf7190
commit 66fce65dee
5 changed files with 388 additions and 5 deletions

View file

@ -25,6 +25,8 @@ import 'dart:async';
import 'dart:core'; import 'dart:core';
import 'responses/ErrorResponse.dart'; import 'responses/ErrorResponse.dart';
import 'Connection.dart'; import 'Connection.dart';
import 'RoomList.dart';
import 'Room.dart';
import 'Store.dart'; import 'Store.dart';
import 'User.dart'; import 'User.dart';
import 'responses/PushrulesResponse.dart'; import 'responses/PushrulesResponse.dart';
@ -189,6 +191,28 @@ class Client {
await connection.clear(); await connection.clear();
} }
/// Loads the Rooms from the [store] and creates a new [RoomList] object.
Future<RoomList> getRoomList(
{bool onlyLeft = false,
bool onlyDirect = false,
bool onlyGroups = false,
onUpdateCallback onUpdate,
onInsertCallback,
onInsert,
onRemoveCallback onRemove}) async {
List<Room> 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 /// Creates a new group chat and invites the given Users and returns the new
/// created room ID. /// created room ID.
Future<String> createGroup(List<User> users) async { Future<String> createGroup(List<User> users) async {

165
lib/src/RoomList.dart Normal file
View file

@ -0,0 +1,165 @@
/*
* Copyright (c) 2019 Zender & Kurtz GbR.
*
* Authors:
* Christian Pauly <krille@famedly.com>
* Marcel Radzio <mtrnord@famedly.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Room> 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<EventUpdate> eventSub;
StreamSubscription<RoomUpdate> 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);

View file

@ -25,7 +25,6 @@ import 'dart:async';
import 'Event.dart'; import 'Event.dart';
import 'Room.dart'; import 'Room.dart';
import 'User.dart'; import 'User.dart';
import 'utils/ChatTime.dart';
import 'sync/EventUpdate.dart'; import 'sync/EventUpdate.dart';
/// Represents the timeline of a room. The callbacks [onUpdate], [onDelete], /// Represents the timeline of a room. The callbacks [onUpdate], [onDelete],
@ -63,12 +62,18 @@ class Timeline {
events.insert(0, newEvent); events.insert(0, newEvent);
onInsert(0); onInsert(0);
} }
onUpdate(); sortAndUpdate();
} catch (e) { } catch (e) {
print("[WARNING] ${e.toString()}"); print("[WARNING] ${e.toString()}");
sub.cancel(); sub.cancel();
} }
} }
sortAndUpdate() {
events
?.sort((a, b) => b.time.toTimeStamp().compareTo(a.time.toTimeStamp()));
onUpdate();
}
} }
typedef onUpdateCallback = void Function(); typedef onUpdateCallback = void Function();

175
test/RoomList_test.dart Normal file
View file

@ -0,0 +1,175 @@
/*
* Copyright (c) 2019 Zender & Kurtz GbR.
*
* Authors:
* Christian Pauly <krille@famedly.com>
* Marcel Radzio <mtrnord@famedly.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<int> insertList = [];
List<int> 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<int> insertList = [];
List<int> 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);
});
});
}

View file

@ -64,18 +64,32 @@ void main() {
"origin_server_ts": testTimeStamp "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); expect(timeline.sub != null, true);
await new Future.delayed(new Duration(milliseconds: 50)); await new Future.delayed(new Duration(milliseconds: 50));
expect(updateCount, 1); expect(updateCount, 2);
expect(insertList, [0]); expect(insertList, [0, 0]);
expect(timeline.events.length, 1); expect(timeline.events.length, 2);
expect(timeline.events[0].id, "1"); expect(timeline.events[0].id, "1");
expect(timeline.events[0].sender.id, "@alice:example.com"); expect(timeline.events[0].sender.id, "@alice:example.com");
expect(timeline.events[0].time.toTimeStamp(), testTimeStamp); expect(timeline.events[0].time.toTimeStamp(), testTimeStamp);
expect(timeline.events[0].environment, "m.room.message"); expect(timeline.events[0].environment, "m.room.message");
expect(timeline.events[0].getBody(), "Testcase"); expect(timeline.events[0].getBody(), "Testcase");
expect(timeline.events[0].time > timeline.events[1].time, true);
}); });
}); });
} }