Merge branch 'typingread-enhance-new-features' into 'master'

[Ephemerals] Add new features

See merge request famedly/famedlysdk!104
This commit is contained in:
Christian Pauly 2019-10-20 09:44:14 +00:00
commit 6c2251ed7a
9 changed files with 162 additions and 44 deletions

View file

@ -422,9 +422,9 @@ class Connection {
room["timeline"]["events"] is List<dynamic>)
_handleRoomEvents(id, room["timeline"]["events"], "timeline");
if (room["ephemetal"] is Map<String, dynamic> &&
room["ephemetal"]["events"] is List<dynamic>)
_handleEphemerals(id, room["ephemetal"]["events"]);
if (room["ephemeral"] is Map<String, dynamic> &&
room["ephemeral"]["events"] is List<dynamic>)
_handleEphemerals(id, room["ephemeral"]["events"]);
if (room["account_data"] is Map<String, dynamic> &&
room["account_data"]["events"] is List<dynamic>)
@ -434,32 +434,48 @@ class Connection {
void _handleEphemerals(String id, List<dynamic> events) {
for (num i = 0; i < events.length; i++) {
if (!(events[i]["type"] is String &&
events[i]["content"] is Map<String, dynamic>)) 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<String, dynamic> &&
events[i]["content"][e]["m.read"] is Map<String, dynamic>))
return;
events[i]["content"][e]["m.read"]
.forEach((String user, dynamic value) async {
if (!(events[i]["content"][e]["m.read"]["user"]
is Map<String, dynamic> &&
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<String>)) continue;
Map<String, dynamic> 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<String, dynamic> userTimestampMap =
events[i]["content"][eventID]["m.read"];
for (var userTimestampMapEntry in userTimestampMap.entries) {
final String mxid = userTimestampMapEntry.key;
List<String> 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<String, dynamic> &&
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");
}
}
}

View file

@ -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<Receipt> get receipts {
if (!(room.roomAccountData.containsKey("m.receipt") &&
room.roomAccountData["m.receipt"].content.containsKey(eventId)))
return [];
List<Receipt> 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<bool> remove() async {

View file

@ -69,6 +69,9 @@ class Room {
/// Key-Value store for room states.
Map<String, RoomState> states = {};
/// Key-Value store for ephemerals.
Map<String, RoomAccountData> ephemerals = {};
/// Key-Value store for private account data only visible for this user.
Map<String, RoomAccountData> roomAccountData = {};
@ -161,6 +164,17 @@ class Room {
return lastEvent;
}
/// Returns a list of all current typing users.
List<User> get typingUsers {
if (!ephemerals.containsKey("m.typing")) return [];
List<dynamic> typingMxid = ephemerals["m.typing"].content["user_ids"];
List<User> 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;

View file

@ -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;

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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<EventUpdate> 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 {

View file

@ -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"
}
]
},

View file

@ -21,6 +21,7 @@
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
*/
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 {