diff --git a/lib/src/Event.dart b/lib/src/Event.dart index 37851ae..ad85437 100644 --- a/lib/src/Event.dart +++ b/lib/src/Event.dart @@ -126,13 +126,16 @@ class Event { /// Generate a new Event object from a json string, mostly a table row. static Event fromJson(Map jsonObj, Room room) { - Map content; - try { - content = json.decode(jsonObj["content_json"]); - } catch (e) { - print("jsonObj decode of event content failed: ${e.toString()}"); - content = {}; - } + Map content = jsonObj["content"]; + + if (content == null) + try { + content = json.decode(jsonObj["content_json"]); + } catch (e) { + print("jsonObj decode of event content failed: ${e.toString()}"); + content = {}; + } + return Event( jsonObj["id"], User.fromJson(jsonObj, room), diff --git a/lib/src/Room.dart b/lib/src/Room.dart index 0ff0420..c34b542 100644 --- a/lib/src/Room.dart +++ b/lib/src/Room.dart @@ -28,6 +28,7 @@ import 'package:famedlysdk/src/responses/ErrorResponse.dart'; import 'package:famedlysdk/src/sync/EventUpdate.dart'; import 'package:famedlysdk/src/Event.dart'; import './User.dart'; +import 'Timeline.dart'; /// Represents a Matrix room. class Room { @@ -83,14 +84,7 @@ class Room { /// The needed power levels for all actions. Map powerLevels = {}; - /// The list of events in this room. If the room is created by the - /// [getRoomList()] of the [Store], this will contain only the last event. - List events = []; - - /// The list of participants in this room. If the room is created by the - /// [getRoomList()] of the [Store], this will contain only the sender of the - /// last event. - List participants = []; + Event lastEvent; /// Your current client instance. final Client client; @@ -123,23 +117,22 @@ class Room { this.historyVisibility, this.joinRules, this.powerLevels, - this.events, - this.participants, + this.lastEvent, this.client, }); /// The last message sent to this room. String get lastMessage { - if (events != null && events.length > 0) - return events[0].getBody(); + if (lastEvent != null) + return lastEvent.getBody(); else return ""; } /// When the last message received. ChatTime get timeCreated { - if (events?.length > 0) - return events[0].time; + if (lastEvent != null) + return lastEvent.time; else return ChatTime.now(); } @@ -165,12 +158,6 @@ class Room { return res; } - @Deprecated("Use the client.connection streams instead!") - Stream> get eventsStream { - return Stream>.fromIterable(Iterable>.generate( - this.events.length, (int index) => this.events)).asBroadcastStream(); - } - /// Call the Matrix API to send a simple text message. Future sendText(String message, {String txid = null}) async { if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}"; @@ -394,9 +381,8 @@ class Room { "power_event_name": row["power_event_name"], "power_event_power_levels": row["power_event_power_levels"], }, + lastEvent: Event.fromJson(row, null), client: matrix, - events: [Event.fromJson(row, null)], - participants: [], ); } @@ -413,26 +399,25 @@ class Room { return room; } + Future getTimeline({onUpdate, onInsert}) async { + List events = await loadEvents(); + return Timeline( + room: this, + events: events, + onUpdate: onUpdate, + onInsert: onInsert, + ); + } + /// Load all events for a given room from the store. This includes all /// senders of those events, who will be added to the participants list. Future> loadEvents() async { - this.events = await client.store.getEventList(this); - - Map participantMap = {}; - for (num i = 0; i < events.length; i++) { - if (!participantMap.containsKey(events[i].sender.mxid)) { - participants.add(events[i].sender); - participantMap[events[i].sender.mxid] = true; - } - } - - return this.events; + return await client.store.getEventList(this); } /// Load all participants for a given room from the store. Future> loadParticipants() async { - this.participants = await client.store.loadParticipants(this); - return this.participants; + return await client.store.loadParticipants(this); } /// Request the full list of participants from the server. The local list @@ -454,8 +439,6 @@ class Room { if (newUser.membership != "leave") participants.add(newUser); } - this.participants = participants; - - return this.participants; + return participants; } } diff --git a/lib/src/Timeline.dart b/lib/src/Timeline.dart new file mode 100644 index 0000000..65dc665 --- /dev/null +++ b/lib/src/Timeline.dart @@ -0,0 +1,75 @@ +/* + * 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 '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], +/// [onInsert] and [onResort] will be triggered automatically. The initial +/// event list will be retreived when created by the [room.getTimeline] method. +class Timeline { + final Room room; + List events = []; + + final onUpdateCallback onUpdate; + final onInsertCallback onInsert; + + StreamSubscription sub; + + Timeline({this.room, this.events, this.onUpdate, this.onInsert}) { + sub ??= room.client.connection.onEvent.stream.listen(_handleEventUpdate); + } + + void _handleEventUpdate(EventUpdate eventUpdate) async { + try { + if (eventUpdate.roomID != room.id) return; + if (eventUpdate.type == "timeline" || eventUpdate.type == "history") { + if (!eventUpdate.content.containsKey("id")) + eventUpdate.content["id"] = eventUpdate.content["event_id"]; + + User user = await room.client.store + ?.getUser(matrixID: eventUpdate.content["sender"], room: room); + if (user != null) { + eventUpdate.content["displayname"] = user.displayName; + eventUpdate.content["avatar_url"] = user.avatarUrl.mxc; + } + + Event newEvent = Event.fromJson(eventUpdate.content, room); + + events.insert(0, newEvent); + onInsert(0); + } + onUpdate(); + } catch (e) { + print("[WARNING] ${e.toString()}"); + sub.cancel(); + } + } +} + +typedef onUpdateCallback = void Function(); +typedef onInsertCallback = void Function(int insertID); diff --git a/lib/src/User.dart b/lib/src/User.dart index aff9122..9836727 100644 --- a/lib/src/User.dart +++ b/lib/src/User.dart @@ -79,7 +79,7 @@ class User { /// Creates a new User object from a json string like a row from the database. static User fromJson(Map json, Room room) { - return User(json['matrix_id'], + return User(json['matrix_id'] ?? json['sender'], displayName: json['displayname'], avatarUrl: MxContent(json['avatar_url']), membership: json['membership'], diff --git a/lib/src/sync/EventUpdate.dart b/lib/src/sync/EventUpdate.dart index 41c40b0..725a711 100644 --- a/lib/src/sync/EventUpdate.dart +++ b/lib/src/sync/EventUpdate.dart @@ -25,7 +25,7 @@ /// already known event. class EventUpdate { /// Usually 'timeline', 'state' or whatever. - final String eventType; + final String type; /// Most events belong to a room. If not, this equals to eventType. final String roomID; @@ -33,7 +33,7 @@ class EventUpdate { /// See (Matrix Room Events)[https://matrix.org/docs/spec/client_server/r0.4.0.html#room-events] /// and (Matrix Events)[https://matrix.org/docs/spec/client_server/r0.4.0.html#id89] for more /// informations. - final String type; + final String eventType; // The json payload of the content of this event. final dynamic content; diff --git a/test/Timeline_test.dart b/test/Timeline_test.dart new file mode 100644 index 0000000..14f7c99 --- /dev/null +++ b/test/Timeline_test.dart @@ -0,0 +1,81 @@ +/* + * 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/Room.dart'; +import 'package:famedlysdk/src/Timeline.dart'; +import 'package:famedlysdk/src/sync/EventUpdate.dart'; +import 'package:famedlysdk/src/utils/ChatTime.dart'; + +void main() { + /// All Tests related to the MxContent + group("Timeline", () { + final String roomID = "!1234:example.com"; + final testTimeStamp = ChatTime.now().toTimeStamp(); + int updateCount = 0; + List insertList = []; + + test("Create", () async { + Client client = Client("testclient"); + client.homeserver = "https://testserver.abc"; + + Room room = Room(id: roomID, client: client); + Timeline timeline = Timeline( + room: room, + events: [], + onUpdate: () { + updateCount++; + }, + onInsert: (int insertID) { + insertList.add(insertID); + }); + + 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": "1", + "origin_server_ts": testTimeStamp + })); + + 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(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"); + }); + }); +}