Merge branch 'store-enhance-usestatestore' into 'master'
[Store] Switch to a state focused store See merge request famedly/famedlysdk!67
This commit is contained in:
commit
902df33d50
|
@ -35,6 +35,7 @@ export 'package:famedlysdk/src/Connection.dart';
|
|||
export 'package:famedlysdk/src/Event.dart';
|
||||
export 'package:famedlysdk/src/Room.dart';
|
||||
export 'package:famedlysdk/src/RoomList.dart';
|
||||
export 'package:famedlysdk/src/RoomState.dart';
|
||||
export 'package:famedlysdk/src/Store.dart';
|
||||
export 'package:famedlysdk/src/Timeline.dart';
|
||||
export 'package:famedlysdk/src/User.dart';
|
||||
|
|
41
lib/src/AccountData.dart
Normal file
41
lib/src/AccountData.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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:famedlysdk/src/RoomState.dart';
|
||||
|
||||
class AccountData {
|
||||
/// The json payload of the content. The content highly depends on the type.
|
||||
final Map<String, dynamic> content;
|
||||
|
||||
/// The type String of this event. For example 'm.room.message'.
|
||||
final String typeKey;
|
||||
|
||||
AccountData({this.content, this.typeKey});
|
||||
|
||||
/// Get a State event from a table row or from the event stream.
|
||||
factory AccountData.fromJson(Map<String, dynamic> jsonPayload) {
|
||||
final Map<String, dynamic> content =
|
||||
RoomState.getMapFromPayload(jsonPayload['content']);
|
||||
return AccountData(content: content, typeKey: jsonPayload['type']);
|
||||
}
|
||||
}
|
|
@ -24,6 +24,10 @@
|
|||
import 'dart:async';
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:famedlysdk/src/AccountData.dart';
|
||||
import 'package:famedlysdk/src/Presence.dart';
|
||||
import 'package:famedlysdk/src/sync/UserUpdate.dart';
|
||||
|
||||
import 'Connection.dart';
|
||||
import 'Room.dart';
|
||||
import 'RoomList.dart';
|
||||
|
@ -33,6 +37,9 @@ import 'requests/SetPushersRequest.dart';
|
|||
import 'responses/ErrorResponse.dart';
|
||||
import 'responses/PushrulesResponse.dart';
|
||||
|
||||
typedef AccountDataEventCB = void Function(AccountData accountData);
|
||||
typedef PresenceCB = void Function(Presence presence);
|
||||
|
||||
/// Represents a Matrix client to communicate with a
|
||||
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
|
||||
/// SDK.
|
||||
|
@ -86,6 +93,56 @@ class Client {
|
|||
/// Returns the current login state.
|
||||
bool isLogged() => accessToken != null;
|
||||
|
||||
/// A list of all rooms the user is participating or invited.
|
||||
RoomList roomList;
|
||||
|
||||
/// Key/Value store of account data.
|
||||
Map<String, AccountData> accountData = {};
|
||||
|
||||
/// Presences of users by a given matrix ID
|
||||
Map<String, Presence> presences = {};
|
||||
|
||||
/// Callback will be called on account data updates.
|
||||
AccountDataEventCB onAccountData;
|
||||
|
||||
/// Callback will be called on presences.
|
||||
PresenceCB onPresence;
|
||||
|
||||
void handleUserUpdate(UserUpdate userUpdate) {
|
||||
if (userUpdate.type == "account_data") {
|
||||
AccountData newAccountData = AccountData.fromJson(userUpdate.content);
|
||||
accountData[newAccountData.typeKey] = newAccountData;
|
||||
if (onAccountData != null) onAccountData(newAccountData);
|
||||
}
|
||||
if (userUpdate.type == "presence") {
|
||||
Presence newPresence = Presence.fromJson(userUpdate.content);
|
||||
presences[newPresence.sender] = newPresence;
|
||||
if (onPresence != null) onPresence(newPresence);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> get directChats =>
|
||||
accountData["m.direct"] != null ? accountData["m.direct"].content : {};
|
||||
|
||||
/// Returns the (first) room ID from the store which is a private chat with the user [userId].
|
||||
/// Returns null if there is none.
|
||||
String getDirectChatFromUserId(String userId) {
|
||||
if (accountData["m.direct"] != null &&
|
||||
accountData["m.direct"].content[userId] is List<dynamic> &&
|
||||
accountData["m.direct"].content[userId].length > 0) {
|
||||
if (roomList.getRoomById(accountData["m.direct"].content[userId][0]) !=
|
||||
null) return accountData["m.direct"].content[userId][0];
|
||||
(accountData["m.direct"].content[userId] as List<dynamic>)
|
||||
.remove(accountData["m.direct"].content[userId][0]);
|
||||
connection.jsonRequest(
|
||||
type: HTTPType.PUT,
|
||||
action: "/client/r0/user/${userID}/account_data/m.direct",
|
||||
data: directChats);
|
||||
return getDirectChatFromUserId(userId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Checks the supported versions of the Matrix protocol and the supported
|
||||
/// login types. Returns false if the server is not compatible with the
|
||||
/// client. Automatically sets [matrixVersions] and [lazyLoadMembers].
|
||||
|
@ -225,12 +282,12 @@ class Client {
|
|||
/// defined by the autojoin room feature in Synapse.
|
||||
Future<List<User>> loadFamedlyContacts() async {
|
||||
List<User> contacts = [];
|
||||
Room contactDiscoveryRoom = await store
|
||||
Room contactDiscoveryRoom = roomList
|
||||
.getRoomByAlias("#famedlyContactDiscovery:${userID.split(":")[1]}");
|
||||
if (contactDiscoveryRoom != null)
|
||||
contacts = await contactDiscoveryRoom.requestParticipants();
|
||||
else
|
||||
contacts = await store.loadContacts();
|
||||
contacts = await store?.loadContacts();
|
||||
return contacts;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:famedlysdk/src/Room.dart';
|
||||
import 'package:famedlysdk/src/RoomList.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
|
@ -50,9 +52,9 @@ class Connection {
|
|||
}));
|
||||
}
|
||||
|
||||
String get _syncFilters => '{"room":{"state":{"lazy_load_members":true}}}';
|
||||
static String syncFilters = '{"room":{"state":{"lazy_load_members":true}}}';
|
||||
|
||||
String get _firstSyncFilters =>
|
||||
static String firstSyncFilters =
|
||||
'{"room":{"include_leave":true,"state":{"lazy_load_members":true}}}';
|
||||
|
||||
/// Handles the connection to the Matrix Homeserver. You can change this to a
|
||||
|
@ -147,13 +149,34 @@ class Connection {
|
|||
client.lazyLoadMembers = newLazyLoadMembers;
|
||||
client.prevBatch = newPrevBatch;
|
||||
|
||||
client.store?.storeClient();
|
||||
List<Room> rooms = [];
|
||||
if (client.store != null) {
|
||||
client.store.storeClient();
|
||||
rooms = await client.store
|
||||
.getRoomList(onlyLeft: false, onlyGroups: false, onlyDirect: false);
|
||||
client.accountData = await client.store.getAccountData();
|
||||
client.presences = await client.store.getPresences();
|
||||
}
|
||||
|
||||
client.roomList = RoomList(
|
||||
client: client,
|
||||
onlyLeft: false,
|
||||
onlyDirect: false,
|
||||
onlyGroups: false,
|
||||
onUpdate: null,
|
||||
onInsert: null,
|
||||
onRemove: null,
|
||||
rooms: rooms);
|
||||
|
||||
_userEventSub ??= onUserEvent.stream.listen(client.handleUserUpdate);
|
||||
|
||||
onLoginStateChanged.add(LoginState.logged);
|
||||
|
||||
_sync();
|
||||
}
|
||||
|
||||
StreamSubscription _userEventSub;
|
||||
|
||||
/// Resets all settings and stops the synchronisation.
|
||||
void clear() {
|
||||
client.store?.clear();
|
||||
|
@ -261,10 +284,10 @@ class Connection {
|
|||
Future<void> _sync() async {
|
||||
if (client.isLogged() == false) return;
|
||||
|
||||
String action = "/client/r0/sync?filter=$_firstSyncFilters";
|
||||
String action = "/client/r0/sync?filter=$firstSyncFilters";
|
||||
|
||||
if (client.prevBatch != null) {
|
||||
action = "/client/r0/sync?filter=$_syncFilters";
|
||||
action = "/client/r0/sync?filter=$syncFilters";
|
||||
action += "&timeout=30000";
|
||||
action += "&since=${client.prevBatch}";
|
||||
}
|
||||
|
@ -450,6 +473,8 @@ class Connection {
|
|||
}
|
||||
}
|
||||
|
||||
typedef _FutureVoidCallback = Future<void> Function();
|
||||
|
||||
class _LifecycleEventHandler extends WidgetsBindingObserver {
|
||||
_LifecycleEventHandler({this.resumeCallBack, this.suspendingCallBack});
|
||||
|
||||
|
@ -471,6 +496,4 @@ class _LifecycleEventHandler extends WidgetsBindingObserver {
|
|||
}
|
||||
}
|
||||
|
||||
typedef _FutureVoidCallback = Future<void> Function();
|
||||
|
||||
enum LoginState { logged, loggedOut }
|
||||
|
|
|
@ -21,37 +21,14 @@
|
|||
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/src/Client.dart';
|
||||
import 'package:famedlysdk/src/RoomState.dart';
|
||||
import 'package:famedlysdk/src/sync/EventUpdate.dart';
|
||||
import 'package:famedlysdk/src/utils/ChatTime.dart';
|
||||
|
||||
import './Room.dart';
|
||||
import './User.dart';
|
||||
|
||||
/// A single Matrix event, e.g. a message in a chat.
|
||||
class Event {
|
||||
/// The Matrix ID for this event in the format '$localpart:server.abc'.
|
||||
final String id;
|
||||
|
||||
/// The room this event belongs to.
|
||||
final Room room;
|
||||
|
||||
/// The time this event has received at the server.
|
||||
final ChatTime time;
|
||||
|
||||
/// The user who has sent this event.
|
||||
final User sender;
|
||||
|
||||
/// The user who is the target of this event e.g. for a m.room.member event.
|
||||
final User stateKey;
|
||||
|
||||
/// The type of this event. Mostly this is 'timeline'.
|
||||
final String environment;
|
||||
|
||||
Event replyEvent;
|
||||
|
||||
/// Defines a timeline event for a room.
|
||||
class Event extends RoomState {
|
||||
/// The status of this event.
|
||||
/// -1=ERROR
|
||||
/// 0=SENDING
|
||||
|
@ -59,20 +36,53 @@ class Event {
|
|||
/// 2=RECEIVED
|
||||
int status;
|
||||
|
||||
/// The json payload of the content. The content highly depends on the type.
|
||||
final Map<String, dynamic> content;
|
||||
static const int defaultStatus = 2;
|
||||
|
||||
Event(
|
||||
this.id,
|
||||
this.sender,
|
||||
this.time, {
|
||||
this.room,
|
||||
this.stateKey,
|
||||
this.status = 2,
|
||||
this.environment,
|
||||
this.content,
|
||||
this.replyEvent,
|
||||
});
|
||||
{this.status = defaultStatus,
|
||||
dynamic content,
|
||||
String typeKey,
|
||||
String eventId,
|
||||
String roomId,
|
||||
String senderId,
|
||||
ChatTime time,
|
||||
dynamic unsigned,
|
||||
dynamic prevContent,
|
||||
String stateKey,
|
||||
Room room})
|
||||
: super(
|
||||
content: content,
|
||||
typeKey: typeKey,
|
||||
eventId: eventId,
|
||||
roomId: roomId,
|
||||
senderId: senderId,
|
||||
time: time,
|
||||
unsigned: unsigned,
|
||||
prevContent: prevContent,
|
||||
stateKey: stateKey,
|
||||
room: room);
|
||||
|
||||
/// Get a State event from a table row or from the event stream.
|
||||
factory Event.fromJson(Map<String, dynamic> jsonPayload, Room room) {
|
||||
final Map<String, dynamic> content =
|
||||
RoomState.getMapFromPayload(jsonPayload['content']);
|
||||
final Map<String, dynamic> unsigned =
|
||||
RoomState.getMapFromPayload(jsonPayload['unsigned']);
|
||||
final Map<String, dynamic> prevContent =
|
||||
RoomState.getMapFromPayload(jsonPayload['prev_content']);
|
||||
return Event(
|
||||
status: jsonPayload['status'] ?? defaultStatus,
|
||||
content: content,
|
||||
typeKey: jsonPayload['type'],
|
||||
eventId: jsonPayload['event_id'],
|
||||
roomId: jsonPayload['room_id'],
|
||||
senderId: jsonPayload['sender'],
|
||||
time: ChatTime(jsonPayload['origin_server_ts']),
|
||||
unsigned: unsigned,
|
||||
prevContent: prevContent,
|
||||
stateKey: jsonPayload['state_key'],
|
||||
room: room);
|
||||
}
|
||||
|
||||
/// Returns the body of this event if it has a body.
|
||||
String get text => content["body"] ?? "";
|
||||
|
@ -84,89 +94,7 @@ class Event {
|
|||
String getBody() {
|
||||
if (text != "") return text;
|
||||
if (formattedText != "") return formattedText;
|
||||
return "*** Unable to parse Content ***";
|
||||
}
|
||||
|
||||
/// Get the real type.
|
||||
EventTypes get type {
|
||||
switch (environment) {
|
||||
case "m.room.avatar":
|
||||
return EventTypes.RoomAvatar;
|
||||
case "m.room.name":
|
||||
return EventTypes.RoomName;
|
||||
case "m.room.topic":
|
||||
return EventTypes.RoomTopic;
|
||||
case "m.room.Aliases":
|
||||
return EventTypes.RoomAliases;
|
||||
case "m.room.canonical_alias":
|
||||
return EventTypes.RoomCanonicalAlias;
|
||||
case "m.room.create":
|
||||
return EventTypes.RoomCreate;
|
||||
case "m.room.join_rules":
|
||||
return EventTypes.RoomJoinRules;
|
||||
case "m.room.member":
|
||||
return EventTypes.RoomMember;
|
||||
case "m.room.power_levels":
|
||||
return EventTypes.RoomPowerLevels;
|
||||
case "m.room.guest_access":
|
||||
return EventTypes.GuestAccess;
|
||||
case "m.room.history_visibility":
|
||||
return EventTypes.HistoryVisibility;
|
||||
case "m.room.message":
|
||||
switch (content["msgtype"] ?? "m.text") {
|
||||
case "m.text":
|
||||
if (content.containsKey("m.relates_to")) {
|
||||
return EventTypes.Reply;
|
||||
}
|
||||
return EventTypes.Text;
|
||||
case "m.notice":
|
||||
return EventTypes.Notice;
|
||||
case "m.emote":
|
||||
return EventTypes.Emote;
|
||||
case "m.image":
|
||||
return EventTypes.Image;
|
||||
case "m.video":
|
||||
return EventTypes.Video;
|
||||
case "m.audio":
|
||||
return EventTypes.Audio;
|
||||
case "m.file":
|
||||
return EventTypes.File;
|
||||
case "m.location":
|
||||
return EventTypes.Location;
|
||||
}
|
||||
}
|
||||
return EventTypes.Unknown;
|
||||
}
|
||||
|
||||
/// Generate a new Event object from a json string, mostly a table row.
|
||||
static Event fromJson(Map<String, dynamic> jsonObj, Room room,
|
||||
{User senderUser, User stateKeyUser}) {
|
||||
Map<String, dynamic> content = jsonObj["content"];
|
||||
|
||||
if (content == null && jsonObj["content_json"] != null)
|
||||
try {
|
||||
content = json.decode(jsonObj["content_json"]);
|
||||
} catch (e) {
|
||||
if (room.client.debug) {
|
||||
print("jsonObj decode of event content failed: ${e.toString()}");
|
||||
}
|
||||
content = {};
|
||||
}
|
||||
else if (content == null) content = {};
|
||||
|
||||
if (senderUser == null) senderUser = User.fromJson(jsonObj, room);
|
||||
if (stateKeyUser == null) stateKeyUser = User(jsonObj["state_key"]);
|
||||
|
||||
return Event(
|
||||
jsonObj["event_id"] ?? jsonObj["id"],
|
||||
senderUser,
|
||||
ChatTime(jsonObj["origin_server_ts"]),
|
||||
stateKey: stateKeyUser,
|
||||
environment: jsonObj["type"],
|
||||
status: jsonObj["status"] ?? 2,
|
||||
content: content,
|
||||
room: room,
|
||||
);
|
||||
return "$type";
|
||||
}
|
||||
|
||||
/// Removes this event if the status is < 1. This event will just be removed
|
||||
|
@ -175,14 +103,14 @@ class Event {
|
|||
if (status < 1) {
|
||||
if (room.client.store != null)
|
||||
await room.client.store.db
|
||||
.rawDelete("DELETE FROM Events WHERE id=?", [id]);
|
||||
.rawDelete("DELETE FROM Events WHERE id=?", [eventId]);
|
||||
|
||||
room.client.connection.onEvent.add(EventUpdate(
|
||||
roomID: room.id,
|
||||
type: "timeline",
|
||||
eventType: environment,
|
||||
eventType: typeKey,
|
||||
content: {
|
||||
"event_id": id,
|
||||
"event_id": eventId,
|
||||
"status": -2,
|
||||
"content": {"body": "Removed..."}
|
||||
}));
|
||||
|
@ -198,42 +126,4 @@ class Event {
|
|||
final String eventID = await room.sendTextEvent(text, txid: txid);
|
||||
return eventID;
|
||||
}
|
||||
|
||||
@Deprecated("Use [client.store.getEventList(Room room)] instead!")
|
||||
static Future<List<Event>> getEventList(Client matrix, Room room) async {
|
||||
List<Event> eventList = await matrix.store.getEventList(room);
|
||||
return eventList;
|
||||
}
|
||||
}
|
||||
|
||||
enum EventTypes {
|
||||
Text,
|
||||
Emote,
|
||||
Notice,
|
||||
Image,
|
||||
Video,
|
||||
Audio,
|
||||
File,
|
||||
Location,
|
||||
Reply,
|
||||
RoomAliases,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
RoomJoinRules,
|
||||
RoomMember,
|
||||
RoomPowerLevels,
|
||||
RoomName,
|
||||
RoomTopic,
|
||||
RoomAvatar,
|
||||
GuestAccess,
|
||||
HistoryVisibility,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
final Map<String, int> StatusTypes = {
|
||||
"REMOVE": -2,
|
||||
"ERROR": -1,
|
||||
"SENDING": 0,
|
||||
"SENT": 1,
|
||||
"RECEIVED": 2,
|
||||
};
|
||||
|
|
43
lib/src/Presence.dart
Normal file
43
lib/src/Presence.dart
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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:famedlysdk/src/AccountData.dart';
|
||||
import 'package:famedlysdk/src/RoomState.dart';
|
||||
|
||||
class Presence extends AccountData {
|
||||
/// The user who has sent this event if it is not a global account data event.
|
||||
final String sender;
|
||||
|
||||
Presence({this.sender, Map<String, dynamic> content, String typeKey})
|
||||
: super(content: content, typeKey: typeKey);
|
||||
|
||||
/// Get a State event from a table row or from the event stream.
|
||||
factory Presence.fromJson(Map<String, dynamic> jsonPayload) {
|
||||
final Map<String, dynamic> content =
|
||||
RoomState.getMapFromPayload(jsonPayload['content']);
|
||||
return Presence(
|
||||
content: content,
|
||||
typeKey: jsonPayload['type'],
|
||||
sender: jsonPayload['sender']);
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@
|
|||
|
||||
import 'package:famedlysdk/src/Client.dart';
|
||||
import 'package:famedlysdk/src/Event.dart';
|
||||
import 'package:famedlysdk/src/RoomAccountData.dart';
|
||||
import 'package:famedlysdk/src/RoomState.dart';
|
||||
import 'package:famedlysdk/src/responses/ErrorResponse.dart';
|
||||
import 'package:famedlysdk/src/sync/EventUpdate.dart';
|
||||
import 'package:famedlysdk/src/utils/ChatTime.dart';
|
||||
|
@ -40,15 +42,6 @@ class Room {
|
|||
/// Membership status of the user for this room.
|
||||
Membership membership;
|
||||
|
||||
/// The name of the room if set by a participant.
|
||||
String name;
|
||||
|
||||
/// The topic of the room if set by a participant.
|
||||
String topic;
|
||||
|
||||
/// The avatar of the room if set by a participant.
|
||||
MxContent avatar = MxContent("");
|
||||
|
||||
/// The count of unread notifications.
|
||||
int notificationCount;
|
||||
|
||||
|
@ -57,7 +50,13 @@ class Room {
|
|||
|
||||
String prev_batch;
|
||||
|
||||
String draft;
|
||||
List<String> mHeroes = [];
|
||||
int mJoinedMemberCount;
|
||||
int mInvitedMemberCount;
|
||||
|
||||
Map<String, RoomState> states = {};
|
||||
|
||||
Map<String, RoomAccountData> roomAccountData = {};
|
||||
|
||||
/// Time when the user has last read the chat.
|
||||
ChatTime unread;
|
||||
|
@ -65,69 +64,97 @@ class Room {
|
|||
/// ID of the fully read marker event.
|
||||
String fullyRead;
|
||||
|
||||
/// The address in the format: #roomname:homeserver.org.
|
||||
String canonicalAlias;
|
||||
/// The name of the room if set by a participant.
|
||||
String get name {
|
||||
if (states["m.room.name"] != null &&
|
||||
!states["m.room.name"].content["name"].isEmpty)
|
||||
return states["m.room.name"].content["name"];
|
||||
if (canonicalAlias != null && !canonicalAlias.isEmpty)
|
||||
return canonicalAlias.substring(1, canonicalAlias.length).split(":")[0];
|
||||
if (mHeroes != null && mHeroes.length > 0) {
|
||||
String displayname = "";
|
||||
for (int i = 0; i < mHeroes.length; i++) {
|
||||
User hero = states[mHeroes[i]] != null
|
||||
? states[mHeroes[i]].asUser
|
||||
: User(mHeroes[i]);
|
||||
displayname += hero.calcDisplayname() + ", ";
|
||||
}
|
||||
return displayname.substring(0, displayname.length - 2);
|
||||
}
|
||||
return "Empty chat";
|
||||
}
|
||||
|
||||
/// If this room is a direct chat, this is the matrix ID of the user
|
||||
String directChatMatrixID;
|
||||
/// The topic of the room if set by a participant.
|
||||
String get topic => states["m.room.topic"] != null
|
||||
? states["m.room.topic"].content["topic"]
|
||||
: "";
|
||||
|
||||
/// The avatar of the room if set by a participant.
|
||||
MxContent get avatar {
|
||||
if (states["m.room.avatar"] != null)
|
||||
return MxContent(states["m.room.avatar"].content["url"]);
|
||||
if (mHeroes != null && mHeroes.length == 1 && states[mHeroes[0]] != null)
|
||||
return states[mHeroes[0]].asUser.avatarUrl;
|
||||
return MxContent("");
|
||||
}
|
||||
|
||||
/// The address in the format: #roomname:homeserver.org.
|
||||
String get canonicalAlias => states["m.room.canonical_alias"] != null
|
||||
? states["m.room.canonical_alias"].content["alias"]
|
||||
: "";
|
||||
|
||||
/// If this room is a direct chat, this is the matrix ID of the user.
|
||||
/// Returns null otherwise.
|
||||
String get directChatMatrixID {
|
||||
String returnUserId = null;
|
||||
if (client.directChats is Map<String, dynamic>) {
|
||||
client.directChats.forEach((String userId, dynamic roomIds) {
|
||||
if (roomIds is List<dynamic>) {
|
||||
for (int i = 0; i < roomIds.length; i++)
|
||||
if (roomIds[i] == this.id) {
|
||||
returnUserId = userId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return returnUserId;
|
||||
}
|
||||
|
||||
/// Wheither this is a direct chat or not
|
||||
bool get isDirectChat => directChatMatrixID != null;
|
||||
|
||||
/// Must be one of [all, mention]
|
||||
String notificationSettings;
|
||||
|
||||
/// Are guest users allowed?
|
||||
String guestAccess;
|
||||
|
||||
/// Who can see the history of this room?
|
||||
String historyVisibility;
|
||||
|
||||
/// Who is allowed to join this room?
|
||||
String joinRules;
|
||||
|
||||
/// The needed power levels for all actions.
|
||||
Map<String, int> powerLevels = {};
|
||||
|
||||
List<String> mHeroes;
|
||||
int mJoinedMemberCount;
|
||||
int mInvitedMemberCount;
|
||||
|
||||
Event lastEvent;
|
||||
Event get lastEvent {
|
||||
ChatTime lastTime = ChatTime(0);
|
||||
Event lastEvent = null;
|
||||
states.forEach((String key, RoomState state) {
|
||||
if (state.time != null && state.time > lastTime) {
|
||||
lastTime = state.time;
|
||||
lastEvent = state.timelineEvent;
|
||||
}
|
||||
});
|
||||
return lastEvent;
|
||||
}
|
||||
|
||||
/// Your current client instance.
|
||||
final Client client;
|
||||
|
||||
@Deprecated("Rooms.roomID is deprecated! Use Rooms.id instead!")
|
||||
String get roomID => this.id;
|
||||
|
||||
@Deprecated("Rooms.matrix is deprecated! Use Rooms.client instead!")
|
||||
Client get matrix => this.client;
|
||||
|
||||
@Deprecated("Rooms.status is deprecated! Use Rooms.membership instead!")
|
||||
String get status => this.membership.toString().split('.').last;
|
||||
|
||||
Room({
|
||||
this.id,
|
||||
this.membership,
|
||||
this.name,
|
||||
this.topic,
|
||||
this.avatar,
|
||||
this.notificationCount,
|
||||
this.highlightCount,
|
||||
this.membership = Membership.join,
|
||||
this.notificationCount = 0,
|
||||
this.highlightCount = 0,
|
||||
this.prev_batch = "",
|
||||
this.draft,
|
||||
this.unread,
|
||||
this.fullyRead,
|
||||
this.canonicalAlias,
|
||||
this.directChatMatrixID,
|
||||
this.notificationSettings,
|
||||
this.guestAccess,
|
||||
this.historyVisibility,
|
||||
this.joinRules,
|
||||
this.powerLevels,
|
||||
this.lastEvent,
|
||||
this.client,
|
||||
this.mHeroes,
|
||||
this.mInvitedMemberCount,
|
||||
this.mJoinedMemberCount,
|
||||
this.notificationSettings,
|
||||
this.mHeroes = const [],
|
||||
this.mInvitedMemberCount = 0,
|
||||
this.mJoinedMemberCount = 0,
|
||||
this.states = const {},
|
||||
this.roomAccountData = const {},
|
||||
});
|
||||
|
||||
/// Calculates the displayname. First checks if there is a name, then checks for a canonical alias and
|
||||
|
@ -297,9 +324,10 @@ class Room {
|
|||
return res;
|
||||
}
|
||||
|
||||
/// Call the Matrix API to unban a banned user from this room.
|
||||
/// Set the power level of the user with the [userID] to the value [power].
|
||||
Future<dynamic> setPower(String userID, int power) async {
|
||||
Map<String, int> powerMap = await client.store.getPowerLevels(id);
|
||||
if (states["m.room.power_levels"] == null) return null;
|
||||
Map<String, int> powerMap = states["m.room.power_levels"].content["users"];
|
||||
powerMap[userID] = power;
|
||||
|
||||
dynamic res = await client.connection.jsonRequest(
|
||||
|
@ -325,7 +353,7 @@ class Room {
|
|||
final dynamic resp = await client.connection.jsonRequest(
|
||||
type: HTTPType.GET,
|
||||
action:
|
||||
"/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount");
|
||||
"/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount&filter=${Connection.syncFilters}");
|
||||
|
||||
if (resp is ErrorResponse) return;
|
||||
|
||||
|
@ -336,6 +364,33 @@ class Room {
|
|||
resp["chunk"].length > 0 &&
|
||||
resp["end"] is String)) return;
|
||||
|
||||
if (resp["state"] is List<dynamic>) {
|
||||
client.store?.transaction(() {
|
||||
for (int i = 0; i < resp["state"].length; i++) {
|
||||
EventUpdate eventUpdate = EventUpdate(
|
||||
type: "state",
|
||||
roomID: id,
|
||||
eventType: resp["state"][i]["type"],
|
||||
content: resp["state"][i],
|
||||
);
|
||||
client.connection.onEvent.add(eventUpdate);
|
||||
client.store.storeEventUpdate(eventUpdate);
|
||||
}
|
||||
return;
|
||||
});
|
||||
if (client.store == null) {
|
||||
for (int i = 0; i < resp["state"].length; i++) {
|
||||
EventUpdate eventUpdate = EventUpdate(
|
||||
type: "state",
|
||||
roomID: id,
|
||||
eventType: resp["state"][i]["type"],
|
||||
content: resp["state"][i],
|
||||
);
|
||||
client.connection.onEvent.add(eventUpdate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<dynamic> history = resp["chunk"];
|
||||
client.store?.transaction(() {
|
||||
for (int i = 0; i < history.length; i++) {
|
||||
|
@ -348,7 +403,7 @@ class Room {
|
|||
client.connection.onEvent.add(eventUpdate);
|
||||
client.store.storeEventUpdate(eventUpdate);
|
||||
client.store.txn.rawUpdate(
|
||||
"UPDATE Rooms SET prev_batch=? WHERE id=?", [resp["end"], id]);
|
||||
"UPDATE Rooms SET prev_batch=? WHERE room_id=?", [resp["end"], id]);
|
||||
}
|
||||
return;
|
||||
});
|
||||
|
@ -367,8 +422,7 @@ class Room {
|
|||
|
||||
/// Sets this room as a direct chat for this user.
|
||||
Future<dynamic> addToDirectChat(String userID) async {
|
||||
Map<String, List<String>> directChats =
|
||||
await client.store.getAccountDataDirectChats();
|
||||
Map<String, dynamic> directChats = client.directChats;
|
||||
if (directChats.containsKey(userID)) if (!directChats[userID].contains(id))
|
||||
directChats[userID].add(id);
|
||||
else
|
||||
|
@ -395,73 +449,58 @@ class Room {
|
|||
return resp;
|
||||
}
|
||||
|
||||
/// Returns a Room from a json String which comes normally from the store.
|
||||
/// Returns a Room from a json String which comes normally from the store. If the
|
||||
/// state are also given, the method will await them.
|
||||
static Future<Room> getRoomFromTableRow(
|
||||
Map<String, dynamic> row, Client matrix) async {
|
||||
String avatarUrl = row["avatar_url"];
|
||||
if (avatarUrl == "")
|
||||
avatarUrl = await matrix.store?.getAvatarFromSingleChat(row["id"]) ?? "";
|
||||
|
||||
return Room(
|
||||
id: row["id"],
|
||||
name: row["topic"],
|
||||
Map<String, dynamic> row, Client matrix,
|
||||
{Future<List<Map<String, dynamic>>> states,
|
||||
Future<List<Map<String, dynamic>>> roomAccountData}) async {
|
||||
Room newRoom = Room(
|
||||
id: row["room_id"],
|
||||
membership: Membership.values
|
||||
.firstWhere((e) => e.toString() == 'Membership.' + row["membership"]),
|
||||
topic: row["description"],
|
||||
avatar: MxContent(avatarUrl),
|
||||
notificationCount: row["notification_count"],
|
||||
highlightCount: row["highlight_count"],
|
||||
unread: ChatTime(row["unread"]),
|
||||
fullyRead: row["fully_read"],
|
||||
notificationSettings: row["notification_settings"],
|
||||
directChatMatrixID: row["direct_chat_matrix_id"],
|
||||
draft: row["draft"],
|
||||
prev_batch: row["prev_batch"],
|
||||
guestAccess: row["guest_access"],
|
||||
historyVisibility: row["history_visibility"],
|
||||
joinRules: row["join_rules"],
|
||||
canonicalAlias: row["canonical_alias"],
|
||||
mInvitedMemberCount: row["invited_member_count"],
|
||||
mJoinedMemberCount: row["joined_member_count"],
|
||||
mHeroes: row["heroes"]?.split(",") ?? [],
|
||||
powerLevels: {
|
||||
"power_events_default": row["power_events_default"],
|
||||
"power_state_default": row["power_state_default"],
|
||||
"power_redact": row["power_redact"],
|
||||
"power_invite": row["power_invite"],
|
||||
"power_ban": row["power_ban"],
|
||||
"power_kick": row["power_kick"],
|
||||
"power_user_default": row["power_user_default"],
|
||||
"power_event_avatar": row["power_event_avatar"],
|
||||
"power_event_history_visibility": row["power_event_history_visibility"],
|
||||
"power_event_canonical_alias": row["power_event_canonical_alias"],
|
||||
"power_event_aliases": row["power_event_aliases"],
|
||||
"power_event_name": row["power_event_name"],
|
||||
"power_event_power_levels": row["power_event_power_levels"],
|
||||
},
|
||||
lastEvent: Event.fromJson(row, null),
|
||||
client: matrix,
|
||||
states: {},
|
||||
roomAccountData: {},
|
||||
);
|
||||
}
|
||||
|
||||
@Deprecated("Use client.store.getRoomById(String id) instead!")
|
||||
static Future<Room> getRoomById(String id, Client matrix) async {
|
||||
Room room = await matrix.store.getRoomById(id);
|
||||
return room;
|
||||
}
|
||||
Map<String, RoomState> newStates = {};
|
||||
if (states != null) {
|
||||
List<Map<String, dynamic>> rawStates = await states;
|
||||
for (int i = 0; i < rawStates.length; i++) {
|
||||
RoomState newState = RoomState.fromJson(rawStates[i], newRoom);
|
||||
newStates[newState.key] = newState;
|
||||
}
|
||||
newRoom.states = newStates;
|
||||
}
|
||||
|
||||
/// Load a room from the store including all room events.
|
||||
static Future<Room> loadRoomEvents(String id, Client matrix) async {
|
||||
Room room = await matrix.store.getRoomById(id);
|
||||
await room.loadEvents();
|
||||
return room;
|
||||
Map<String, RoomAccountData> newRoomAccountData = {};
|
||||
if (roomAccountData != null) {
|
||||
List<Map<String, dynamic>> rawRoomAccountData = await roomAccountData;
|
||||
for (int i = 0; i < rawRoomAccountData.length; i++) {
|
||||
RoomAccountData newData =
|
||||
RoomAccountData.fromJson(rawRoomAccountData[i], newRoom);
|
||||
newRoomAccountData[newData.typeKey] = newData;
|
||||
}
|
||||
newRoom.roomAccountData = newRoomAccountData;
|
||||
}
|
||||
|
||||
return newRoom;
|
||||
}
|
||||
|
||||
/// Creates a timeline from the store. Returns a [Timeline] object.
|
||||
Future<Timeline> getTimeline(
|
||||
{onTimelineUpdateCallback onUpdate,
|
||||
onTimelineInsertCallback onInsert}) async {
|
||||
List<Event> events = await loadEvents();
|
||||
List<Event> events = [];
|
||||
if (client.store != null) events = await client.store.getEventList(this);
|
||||
return Timeline(
|
||||
room: this,
|
||||
events: events,
|
||||
|
@ -470,17 +509,23 @@ class Room {
|
|||
);
|
||||
}
|
||||
|
||||
/// 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<List<Event>> loadEvents() async {
|
||||
return await client.store.getEventList(this);
|
||||
}
|
||||
|
||||
/// Load all participants for a given room from the store.
|
||||
@deprecated
|
||||
Future<List<User>> loadParticipants() async {
|
||||
return await client.store.loadParticipants(this);
|
||||
}
|
||||
|
||||
/// Returns all participants for this room. With lazy loading this
|
||||
/// list may not be complete. User [requestParticipants] in this
|
||||
/// case.
|
||||
List<User> getParticipants() {
|
||||
List<User> userList = [];
|
||||
for (var entry in states.entries)
|
||||
if (entry.value.type == EventTypes.RoomMember)
|
||||
userList.add(entry.value.asUser);
|
||||
return userList;
|
||||
}
|
||||
|
||||
/// Request the full list of participants from the server. The local list
|
||||
/// from the store is not complete if the client uses lazy loading.
|
||||
Future<List<User>> requestParticipants() async {
|
||||
|
@ -492,14 +537,7 @@ class Room {
|
|||
return participants;
|
||||
|
||||
for (num i = 0; i < res["chunk"].length; i++) {
|
||||
User newUser = User(res["chunk"][i]["state_key"],
|
||||
displayName: res["chunk"][i]["content"]["displayname"] ?? "",
|
||||
membership: Membership.values.firstWhere((e) =>
|
||||
e.toString() ==
|
||||
'Membership.' + res["chunk"][i]["content"]["membership"] ??
|
||||
""),
|
||||
avatarUrl: MxContent(res["chunk"][i]["content"]["avatar_url"] ?? ""),
|
||||
room: this);
|
||||
User newUser = RoomState.fromJson(res["chunk"][i], this).asUser;
|
||||
if (newUser.membership != Membership.leave) participants.add(newUser);
|
||||
}
|
||||
|
||||
|
@ -507,18 +545,14 @@ class Room {
|
|||
}
|
||||
|
||||
Future<User> getUserByMXID(String mxID) async {
|
||||
if (client.store != null) {
|
||||
final User storeEvent =
|
||||
await client.store.getUser(matrixID: mxID, room: this);
|
||||
if (storeEvent != null) return storeEvent;
|
||||
}
|
||||
if (states[mxID] != null) return states[mxID].asUser;
|
||||
final dynamic resp = await client.connection.jsonRequest(
|
||||
type: HTTPType.GET,
|
||||
action: "/client/r0/rooms/$id/state/m.room.member/$mxID");
|
||||
if (resp is ErrorResponse) return null;
|
||||
// Somehow we miss the mxid in the response and only get the content of the event.
|
||||
resp["matrix_id"] = mxID;
|
||||
return User.fromJson(resp, this);
|
||||
return RoomState.fromJson(resp, this).asUser;
|
||||
}
|
||||
|
||||
/// Searches for the event in the store. If it isn't found, try to request it
|
||||
|
@ -531,7 +565,30 @@ class Room {
|
|||
final dynamic resp = await client.connection.jsonRequest(
|
||||
type: HTTPType.GET, action: "/client/r0/rooms/$id/event/$eventID");
|
||||
if (resp is ErrorResponse) return null;
|
||||
return Event.fromJson(resp, this,
|
||||
senderUser: (await getUserByMXID(resp["sender"])));
|
||||
return Event.fromJson(resp, this);
|
||||
}
|
||||
|
||||
/// Returns the user's own power level.
|
||||
int getPowerLevelByUserId(String userId) {
|
||||
int powerLevel = 0;
|
||||
RoomState powerLevelState = states["m.room.power_levels"];
|
||||
if (powerLevelState == null) return powerLevel;
|
||||
if (powerLevelState.content["users_default"] is int)
|
||||
powerLevel = powerLevelState.content["users_default"];
|
||||
if (powerLevelState.content["users"] is Map<String, dynamic> &&
|
||||
powerLevelState.content["users"][userId] != null)
|
||||
powerLevel = powerLevelState.content["users"][userId];
|
||||
return powerLevel;
|
||||
}
|
||||
|
||||
/// Returns the user's own power level.
|
||||
int get ownPowerLevel => getPowerLevelByUserId(client.userID);
|
||||
|
||||
/// Returns the power levels from all users for this room or null if not given.
|
||||
Map<String, int> get powerLevels {
|
||||
RoomState powerLevelState = states["m.room.power_levels"];
|
||||
if (powerLevelState.content["users"] is Map<String, int>)
|
||||
return powerLevelState.content["users"];
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
49
lib/src/RoomAccountData.dart
Normal file
49
lib/src/RoomAccountData.dart
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/AccountData.dart';
|
||||
import 'package:famedlysdk/src/RoomState.dart';
|
||||
|
||||
class RoomAccountData extends AccountData {
|
||||
/// The user who has sent this event if it is not a global account data event.
|
||||
final String roomId;
|
||||
|
||||
final Room room;
|
||||
|
||||
RoomAccountData(
|
||||
{this.roomId, this.room, Map<String, dynamic> content, String typeKey})
|
||||
: super(content: content, typeKey: typeKey);
|
||||
|
||||
/// Get a State event from a table row or from the event stream.
|
||||
factory RoomAccountData.fromJson(
|
||||
Map<String, dynamic> jsonPayload, Room room) {
|
||||
final Map<String, dynamic> content =
|
||||
RoomState.getMapFromPayload(jsonPayload['content']);
|
||||
return RoomAccountData(
|
||||
content: content,
|
||||
typeKey: jsonPayload['type'],
|
||||
roomId: jsonPayload['room_id'],
|
||||
room: room);
|
||||
}
|
||||
}
|
|
@ -24,14 +24,17 @@
|
|||
import 'dart:async';
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:famedlysdk/src/RoomState.dart';
|
||||
|
||||
import 'Client.dart';
|
||||
import 'Event.dart';
|
||||
import 'Room.dart';
|
||||
import 'User.dart';
|
||||
import 'sync/EventUpdate.dart';
|
||||
import 'sync/RoomUpdate.dart';
|
||||
import 'utils/ChatTime.dart';
|
||||
import 'utils/MxContent.dart';
|
||||
|
||||
typedef onRoomListUpdateCallback = void Function();
|
||||
typedef onRoomListInsertCallback = void Function(int insertID);
|
||||
typedef onRoomListRemoveCallback = void Function(int insertID);
|
||||
|
||||
/// Represents a list of rooms for this client, which will automatically update
|
||||
/// itself and call the [onUpdate], [onInsert] and [onDelete] callbacks. To get
|
||||
|
@ -69,6 +72,21 @@ class RoomList {
|
|||
this.onlyGroups = false}) {
|
||||
eventSub ??= client.connection.onEvent.stream.listen(_handleEventUpdate);
|
||||
roomSub ??= client.connection.onRoomUpdate.stream.listen(_handleRoomUpdate);
|
||||
sort();
|
||||
}
|
||||
|
||||
Room getRoomByAlias(String alias) {
|
||||
for (int i = 0; i < rooms.length; i++) {
|
||||
if (rooms[i].canonicalAlias == alias) return rooms[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Room getRoomById(String id) {
|
||||
for (int j = 0; j < rooms.length; j++) {
|
||||
if (rooms[j].id == id) return rooms[j];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _handleRoomUpdate(RoomUpdate chatUpdate) {
|
||||
|
@ -87,7 +105,6 @@ class RoomList {
|
|||
// 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,
|
||||
|
@ -95,6 +112,9 @@ class RoomList {
|
|||
mHeroes: chatUpdate.summary?.mHeroes,
|
||||
mJoinedMemberCount: chatUpdate.summary?.mJoinedMemberCount,
|
||||
mInvitedMemberCount: chatUpdate.summary?.mInvitedMemberCount,
|
||||
states: {},
|
||||
roomAccountData: {},
|
||||
client: client,
|
||||
);
|
||||
rooms.insert(position, newRoom);
|
||||
if (onInsert != null) onInsert(position);
|
||||
|
@ -125,11 +145,7 @@ class RoomList {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
if (eventUpdate.type != "timeline" && eventUpdate.type != "state") return;
|
||||
// Search the room in the rooms
|
||||
num j = 0;
|
||||
for (j = 0; j < rooms.length; j++) {
|
||||
|
@ -138,44 +154,20 @@ class RoomList {
|
|||
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") {
|
||||
User stateKey = null;
|
||||
if (eventUpdate.content["state_key"] is String)
|
||||
stateKey = User(eventUpdate.content["state_key"]);
|
||||
// Update the last message preview
|
||||
rooms[j].lastEvent = Event(
|
||||
eventUpdate.content["id"],
|
||||
User(eventUpdate.content["sender"]),
|
||||
ChatTime(eventUpdate.content["origin_server_ts"]),
|
||||
room: rooms[j],
|
||||
stateKey: stateKey,
|
||||
content: eventUpdate.content["content"],
|
||||
environment: eventUpdate.eventType,
|
||||
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"]);
|
||||
}
|
||||
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;
|
||||
sortAndUpdate();
|
||||
}
|
||||
|
||||
sortAndUpdate() {
|
||||
sort() {
|
||||
rooms?.sort((a, b) =>
|
||||
b.timeCreated.toTimeStamp().compareTo(a.timeCreated.toTimeStamp()));
|
||||
}
|
||||
|
||||
sortAndUpdate() {
|
||||
sort();
|
||||
if (onUpdate != null) onUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
typedef onRoomListUpdateCallback = void Function();
|
||||
typedef onRoomListInsertCallback = void Function(int insertID);
|
||||
typedef onRoomListRemoveCallback = void Function(int insertID);
|
||||
|
|
215
lib/src/RoomState.dart
Normal file
215
lib/src/RoomState.dart
Normal file
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* 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:convert';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/ChatTime.dart';
|
||||
import './Room.dart';
|
||||
|
||||
class RoomState {
|
||||
/// The Matrix ID for this event in the format '$localpart:server.abc'. Please not
|
||||
/// that account data, presence and other events may not have an eventId.
|
||||
final String eventId;
|
||||
|
||||
/// The json payload of the content. The content highly depends on the type.
|
||||
final Map<String, dynamic> content;
|
||||
|
||||
/// The type String of this event. For example 'm.room.message'.
|
||||
final String typeKey;
|
||||
|
||||
/// The ID of the room this event belongs to.
|
||||
final String roomId;
|
||||
|
||||
/// The user who has sent this event if it is not a global account data event.
|
||||
final String senderId;
|
||||
|
||||
User get sender => room.states[senderId]?.asUser ?? User(senderId);
|
||||
|
||||
/// The time this event has received at the server. May be null for events like
|
||||
/// account data.
|
||||
final ChatTime time;
|
||||
|
||||
/// Optional additional content for this event.
|
||||
final Map<String, dynamic> unsigned;
|
||||
|
||||
/// The room this event belongs to. May be null.
|
||||
final Room room;
|
||||
|
||||
/// Optional. The previous content for this state.
|
||||
/// This will be present only for state events appearing in the timeline.
|
||||
/// If this is not a state event, or there is no previous content, this key will be null.
|
||||
final Map<String, dynamic> prevContent;
|
||||
|
||||
/// Optional. This key will only be present for state events. A unique key which defines
|
||||
/// the overwriting semantics for this piece of room state.
|
||||
final String stateKey;
|
||||
|
||||
User get stateKeyUser => room.states[stateKey]?.asUser ?? User(stateKey);
|
||||
|
||||
RoomState(
|
||||
{this.content,
|
||||
this.typeKey,
|
||||
this.eventId,
|
||||
this.roomId,
|
||||
this.senderId,
|
||||
this.time,
|
||||
this.unsigned,
|
||||
this.prevContent,
|
||||
this.stateKey,
|
||||
this.room});
|
||||
|
||||
static Map<String, dynamic> getMapFromPayload(dynamic payload) {
|
||||
if (payload is String)
|
||||
try {
|
||||
return json.decode(payload);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
if (payload is Map<String, dynamic>) return payload;
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Get a State event from a table row or from the event stream.
|
||||
factory RoomState.fromJson(Map<String, dynamic> jsonPayload, Room room) {
|
||||
final Map<String, dynamic> content =
|
||||
RoomState.getMapFromPayload(jsonPayload['content']);
|
||||
final Map<String, dynamic> unsigned =
|
||||
RoomState.getMapFromPayload(jsonPayload['unsigned']);
|
||||
final Map<String, dynamic> prevContent =
|
||||
RoomState.getMapFromPayload(jsonPayload['prev_content']);
|
||||
return RoomState(
|
||||
stateKey: jsonPayload['state_key'],
|
||||
prevContent: prevContent,
|
||||
content: content,
|
||||
typeKey: jsonPayload['type'],
|
||||
eventId: jsonPayload['event_id'],
|
||||
roomId: jsonPayload['room_id'],
|
||||
senderId: jsonPayload['sender'],
|
||||
time: ChatTime(jsonPayload['origin_server_ts']),
|
||||
unsigned: unsigned,
|
||||
room: room);
|
||||
}
|
||||
|
||||
Event get timelineEvent => Event(
|
||||
content: content,
|
||||
typeKey: typeKey,
|
||||
eventId: eventId,
|
||||
room: room,
|
||||
roomId: roomId,
|
||||
senderId: senderId,
|
||||
time: time,
|
||||
unsigned: unsigned,
|
||||
status: 1,
|
||||
);
|
||||
|
||||
/// The unique key of this event. For events with a [stateKey], it will be the
|
||||
/// stateKey. Otherwise it will be the [type] as a string.
|
||||
String get key => stateKey == null || stateKey.isEmpty ? typeKey : stateKey;
|
||||
|
||||
User get asUser => User.fromState(
|
||||
stateKey: stateKey,
|
||||
prevContent: prevContent,
|
||||
content: content,
|
||||
typeKey: typeKey,
|
||||
eventId: eventId,
|
||||
roomId: roomId,
|
||||
senderId: senderId,
|
||||
time: time,
|
||||
unsigned: unsigned,
|
||||
room: room);
|
||||
|
||||
/// Get the real type.
|
||||
EventTypes get type {
|
||||
switch (typeKey) {
|
||||
case "m.room.avatar":
|
||||
return EventTypes.RoomAvatar;
|
||||
case "m.room.name":
|
||||
return EventTypes.RoomName;
|
||||
case "m.room.topic":
|
||||
return EventTypes.RoomTopic;
|
||||
case "m.room.Aliases":
|
||||
return EventTypes.RoomAliases;
|
||||
case "m.room.canonical_alias":
|
||||
return EventTypes.RoomCanonicalAlias;
|
||||
case "m.room.create":
|
||||
return EventTypes.RoomCreate;
|
||||
case "m.room.join_rules":
|
||||
return EventTypes.RoomJoinRules;
|
||||
case "m.room.member":
|
||||
return EventTypes.RoomMember;
|
||||
case "m.room.power_levels":
|
||||
return EventTypes.RoomPowerLevels;
|
||||
case "m.room.guest_access":
|
||||
return EventTypes.GuestAccess;
|
||||
case "m.room.history_visibility":
|
||||
return EventTypes.HistoryVisibility;
|
||||
case "m.room.message":
|
||||
switch (content["msgtype"] ?? "m.text") {
|
||||
case "m.text":
|
||||
if (content.containsKey("m.relates_to")) {
|
||||
return EventTypes.Reply;
|
||||
}
|
||||
return EventTypes.Text;
|
||||
case "m.notice":
|
||||
return EventTypes.Notice;
|
||||
case "m.emote":
|
||||
return EventTypes.Emote;
|
||||
case "m.image":
|
||||
return EventTypes.Image;
|
||||
case "m.video":
|
||||
return EventTypes.Video;
|
||||
case "m.audio":
|
||||
return EventTypes.Audio;
|
||||
case "m.file":
|
||||
return EventTypes.File;
|
||||
case "m.location":
|
||||
return EventTypes.Location;
|
||||
}
|
||||
}
|
||||
return EventTypes.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
enum EventTypes {
|
||||
Text,
|
||||
Emote,
|
||||
Notice,
|
||||
Image,
|
||||
Video,
|
||||
Audio,
|
||||
File,
|
||||
Location,
|
||||
Reply,
|
||||
RoomAliases,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
RoomJoinRules,
|
||||
RoomMember,
|
||||
RoomPowerLevels,
|
||||
RoomName,
|
||||
RoomTopic,
|
||||
RoomAvatar,
|
||||
GuestAccess,
|
||||
HistoryVisibility,
|
||||
Unknown,
|
||||
}
|
|
@ -25,6 +25,9 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:famedlysdk/src/AccountData.dart';
|
||||
import 'package:famedlysdk/src/Presence.dart';
|
||||
import 'package:famedlysdk/src/RoomState.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
|
@ -55,7 +58,7 @@ class Store {
|
|||
_init() async {
|
||||
var databasePath = await getDatabasesPath();
|
||||
String path = p.join(databasePath, "FluffyMatrix.db");
|
||||
_db = await openDatabase(path, version: 12,
|
||||
_db = await openDatabase(path, version: 14,
|
||||
onCreate: (Database db, int version) async {
|
||||
await createTables(db);
|
||||
}, onUpgrade: (Database db, int oldVersion, int newVersion) async {
|
||||
|
@ -153,8 +156,8 @@ class Store {
|
|||
}
|
||||
|
||||
Future<void> storeRoomPrevBatch(Room room) async {
|
||||
await _db.rawUpdate(
|
||||
"UPDATE Rooms SET prev_batch=? WHERE id=?", [room.prev_batch, room.id]);
|
||||
await _db.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE room_id=?",
|
||||
[room.prev_batch, room.id]);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -163,8 +166,7 @@ class Store {
|
|||
Future<void> storeRoomUpdate(RoomUpdate roomUpdate) {
|
||||
// Insert the chat into the database if not exists
|
||||
txn.rawInsert(
|
||||
"INSERT OR IGNORE INTO Rooms " +
|
||||
"VALUES(?, ?, '', 0, 0, 0, 0, '', '', '', '', 0, '', '', '', '', '', '', '', '', 0, 50, 50, 0, 50, 50, 0, 50, 100, 50, 50, 50, 100) ",
|
||||
"INSERT OR IGNORE INTO Rooms " + "VALUES(?, ?, 0, 0, '', 0, 0, '') ",
|
||||
[roomUpdate.id, roomUpdate.membership.toString().split('.').last]);
|
||||
|
||||
// Update the notification counts and the limited timeline boolean and the summary
|
||||
|
@ -187,15 +189,15 @@ class Store {
|
|||
updateQuery += ", heroes=?";
|
||||
updateArgs.add(roomUpdate.summary.mHeroes.join(","));
|
||||
}
|
||||
updateQuery += " WHERE id=?";
|
||||
updateQuery += " WHERE room_id=?";
|
||||
updateArgs.add(roomUpdate.id);
|
||||
txn.rawUpdate(updateQuery, updateArgs);
|
||||
|
||||
// Is the timeline limited? Then all previous messages should be
|
||||
// removed from the database!
|
||||
if (roomUpdate.limitedTimeline) {
|
||||
txn.rawDelete("DELETE FROM Events WHERE chat_id=?", [roomUpdate.id]);
|
||||
txn.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE id=?",
|
||||
txn.rawDelete("DELETE FROM Events WHERE room_id=?", [roomUpdate.id]);
|
||||
txn.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE room_id=?",
|
||||
[roomUpdate.prev_batch, roomUpdate.id]);
|
||||
}
|
||||
return null;
|
||||
|
@ -204,21 +206,17 @@ class Store {
|
|||
/// Stores an UserUpdate object in the database. Must be called inside of
|
||||
/// [transaction].
|
||||
Future<void> storeUserEventUpdate(UserUpdate userUpdate) {
|
||||
switch (userUpdate.eventType) {
|
||||
case "m.direct":
|
||||
if (userUpdate.content["content"] is Map<String, dynamic>) {
|
||||
final Map<String, dynamic> directMap = userUpdate.content["content"];
|
||||
directMap.forEach((String key, dynamic value) {
|
||||
if (value is List<dynamic> && value.length > 0)
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
txn.rawUpdate(
|
||||
"UPDATE Rooms SET direct_chat_matrix_id=? WHERE id=?",
|
||||
[key, value[i]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (userUpdate.type == "account_data")
|
||||
txn.rawInsert("INSERT OR REPLACE INTO AccountData VALUES(?, ?)", [
|
||||
userUpdate.eventType,
|
||||
json.encode(userUpdate.content["content"]),
|
||||
]);
|
||||
else if (userUpdate.type == "presence")
|
||||
txn.rawInsert("INSERT OR REPLACE INTO Presences VALUES(?, ?, ?)", [
|
||||
userUpdate.eventType,
|
||||
userUpdate.content["sender"],
|
||||
json.encode(userUpdate.content["content"]),
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -229,252 +227,96 @@ class Store {
|
|||
String type = eventUpdate.type;
|
||||
String chat_id = eventUpdate.roomID;
|
||||
|
||||
// Get the state_key for m.room.member events
|
||||
String state_key = "";
|
||||
if (eventContent["state_key"] is String) {
|
||||
state_key = eventContent["state_key"];
|
||||
}
|
||||
|
||||
if (type == "timeline" || type == "history") {
|
||||
// calculate the status
|
||||
num status = 2;
|
||||
if (eventContent["status"] is num) status = eventContent["status"];
|
||||
|
||||
// Make unsigned part of the content
|
||||
if (eventContent.containsKey("unsigned")) {
|
||||
Map<String, dynamic> newContent = {
|
||||
"unsigned": eventContent["unsigned"]
|
||||
};
|
||||
eventContent["content"].forEach((key, val) => newContent[key] = val);
|
||||
eventContent["content"] = newContent;
|
||||
}
|
||||
|
||||
// Get the state_key for m.room.member events
|
||||
String state_key = "";
|
||||
if (eventContent["state_key"] is String) {
|
||||
state_key = eventContent["state_key"];
|
||||
}
|
||||
|
||||
// Save the event in the database
|
||||
if ((status == 1 || status == -1) &&
|
||||
eventContent["unsigned"] is Map<String, dynamic> &&
|
||||
eventContent["unsigned"]["transaction_id"] is String)
|
||||
txn.rawUpdate("UPDATE Events SET status=?, id=? WHERE id=?", [
|
||||
txn.rawUpdate(
|
||||
"UPDATE Events SET status=?, event_id=? WHERE event_id=?", [
|
||||
status,
|
||||
eventContent["event_id"],
|
||||
eventContent["unsigned"]["transaction_id"]
|
||||
]);
|
||||
else
|
||||
txn.rawInsert(
|
||||
"INSERT OR REPLACE INTO Events VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
||||
eventContent["event_id"],
|
||||
chat_id,
|
||||
eventContent["origin_server_ts"],
|
||||
eventContent["sender"],
|
||||
state_key,
|
||||
eventContent["content"]["body"],
|
||||
eventContent["type"],
|
||||
json.encode(eventContent["content"]),
|
||||
status
|
||||
]);
|
||||
"INSERT OR REPLACE INTO Events VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
eventContent["event_id"],
|
||||
chat_id,
|
||||
eventContent["origin_server_ts"],
|
||||
eventContent["sender"],
|
||||
eventContent["type"],
|
||||
json.encode(eventContent["unsigned"] ?? ""),
|
||||
json.encode(eventContent["content"]),
|
||||
json.encode(eventContent["prevContent"]),
|
||||
eventContent["state_key"],
|
||||
status
|
||||
]);
|
||||
|
||||
// Is there a transaction id? Then delete the event with this id.
|
||||
if (status != -1 &&
|
||||
eventUpdate.content.containsKey("unsigned") &&
|
||||
eventUpdate.content["unsigned"]["transaction_id"] is String)
|
||||
txn.rawDelete("DELETE FROM Events WHERE id=?",
|
||||
txn.rawDelete("DELETE FROM Events WHERE event_id=?",
|
||||
[eventUpdate.content["unsigned"]["transaction_id"]]);
|
||||
}
|
||||
|
||||
if (type == "history") return null;
|
||||
|
||||
switch (eventUpdate.eventType) {
|
||||
case "m.receipt":
|
||||
if (eventContent["user"] == client.userID) {
|
||||
txn.rawUpdate("UPDATE Rooms SET unread=? WHERE id=?",
|
||||
[eventContent["ts"], chat_id]);
|
||||
} else {
|
||||
// Mark all previous received messages as seen
|
||||
txn.rawUpdate(
|
||||
"UPDATE Events SET status=3 WHERE origin_server_ts<=? AND chat_id=? AND status=2",
|
||||
[eventContent["ts"], chat_id]);
|
||||
}
|
||||
break;
|
||||
// This event means, that the name of a room has been changed, so
|
||||
// it has to be changed in the database.
|
||||
case "m.room.name":
|
||||
txn.rawUpdate("UPDATE Rooms SET topic=? WHERE id=?",
|
||||
[eventContent["content"]["name"], chat_id]);
|
||||
break;
|
||||
// This event means, that the topic of a room has been changed, so
|
||||
// it has to be changed in the database
|
||||
case "m.room.topic":
|
||||
txn.rawUpdate("UPDATE Rooms SET description=? WHERE id=?",
|
||||
[eventContent["content"]["topic"], chat_id]);
|
||||
break;
|
||||
// This event means, that the topic of a room has been changed, so
|
||||
// it has to be changed in the database
|
||||
case "m.room.history_visibility":
|
||||
txn.rawUpdate("UPDATE Rooms SET history_visibility=? WHERE id=?",
|
||||
[eventContent["content"]["history_visibility"], chat_id]);
|
||||
break;
|
||||
// This event means, that the topic of a room has been changed, so
|
||||
// it has to be changed in the database
|
||||
case "m.room.redaction":
|
||||
txn.rawDelete(
|
||||
"DELETE FROM Events WHERE id=?", [eventContent["redacts"]]);
|
||||
break;
|
||||
// This event means, that the topic of a room has been changed, so
|
||||
// it has to be changed in the database
|
||||
case "m.room.guest_access":
|
||||
txn.rawUpdate("UPDATE Rooms SET guest_access=? WHERE id=?",
|
||||
[eventContent["content"]["guest_access"], chat_id]);
|
||||
break;
|
||||
// This event means, that the canonical alias of a room has been changed, so
|
||||
// it has to be changed in the database
|
||||
case "m.room.canonical_alias":
|
||||
txn.rawUpdate("UPDATE Rooms SET canonical_alias=? WHERE id=?",
|
||||
[eventContent["content"]["alias"], chat_id]);
|
||||
break;
|
||||
// This event means, that the topic of a room has been changed, so
|
||||
// it has to be changed in the database
|
||||
case "m.room.join_rules":
|
||||
txn.rawUpdate("UPDATE Rooms SET join_rules=? WHERE id=?",
|
||||
[eventContent["content"]["join_rule"], chat_id]);
|
||||
break;
|
||||
// This event means, that the avatar of a room has been changed, so
|
||||
// it has to be changed in the database
|
||||
case "m.room.avatar":
|
||||
txn.rawUpdate("UPDATE Rooms SET avatar_url=? WHERE id=?",
|
||||
[eventContent["content"]["url"], chat_id]);
|
||||
break;
|
||||
// This event means, that the aliases of a room has been changed, so
|
||||
// it has to be changed in the database
|
||||
case "m.fully_read":
|
||||
txn.rawUpdate("UPDATE Rooms SET fully_read=? WHERE id=?",
|
||||
[eventContent["content"]["event_id"], chat_id]);
|
||||
break;
|
||||
// This event means, that someone joined the room, has left the room
|
||||
// or has changed his nickname
|
||||
case "m.room.member":
|
||||
String membership = eventContent["content"]["membership"];
|
||||
String state_key = eventContent["state_key"];
|
||||
String insertDisplayname = "";
|
||||
String insertAvatarUrl = "";
|
||||
if (eventContent["content"]["displayname"] is String) {
|
||||
insertDisplayname = eventContent["content"]["displayname"];
|
||||
}
|
||||
if (eventContent["content"]["avatar_url"] is String) {
|
||||
insertAvatarUrl = eventContent["content"]["avatar_url"];
|
||||
}
|
||||
if (eventUpdate.content["event_id"] != null) {
|
||||
txn.rawInsert(
|
||||
"INSERT OR REPLACE INTO RoomStates VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
eventContent["event_id"],
|
||||
chat_id,
|
||||
eventContent["origin_server_ts"],
|
||||
eventContent["sender"],
|
||||
state_key,
|
||||
json.encode(eventContent["unsigned"] ?? ""),
|
||||
json.encode(eventContent["prev_content"] ?? ""),
|
||||
eventContent["type"],
|
||||
json.encode(eventContent["content"]),
|
||||
]);
|
||||
} else
|
||||
txn.rawInsert("INSERT OR REPLACE INTO RoomAccountData VALUES(?, ?, ?)", [
|
||||
eventContent["type"],
|
||||
chat_id,
|
||||
json.encode(eventContent["content"]),
|
||||
]);
|
||||
|
||||
// Update membership table
|
||||
txn.rawInsert("INSERT OR IGNORE INTO Users VALUES(?,?,?,?,?,0)", [
|
||||
chat_id,
|
||||
state_key,
|
||||
insertDisplayname,
|
||||
insertAvatarUrl,
|
||||
membership
|
||||
]);
|
||||
String queryStr = "UPDATE Users SET membership=?";
|
||||
List<String> queryArgs = [membership];
|
||||
|
||||
if (eventContent["content"]["displayname"] is String) {
|
||||
queryStr += " , displayname=?";
|
||||
queryArgs.add(eventContent["content"]["displayname"]);
|
||||
}
|
||||
if (eventContent["content"]["avatar_url"] is String) {
|
||||
queryStr += " , avatar_url=?";
|
||||
queryArgs.add(eventContent["content"]["avatar_url"]);
|
||||
}
|
||||
|
||||
queryStr += " WHERE matrix_id=? AND chat_id=?";
|
||||
queryArgs.add(state_key);
|
||||
queryArgs.add(chat_id);
|
||||
txn.rawUpdate(queryStr, queryArgs);
|
||||
break;
|
||||
// This event changes the permissions of the users and the power levels
|
||||
case "m.room.power_levels":
|
||||
String query = "UPDATE Rooms SET ";
|
||||
if (eventContent["content"]["ban"] is num)
|
||||
query += ", power_ban=" + eventContent["content"]["ban"].toString();
|
||||
if (eventContent["content"]["events_default"] is num)
|
||||
query += ", power_events_default=" +
|
||||
eventContent["content"]["events_default"].toString();
|
||||
if (eventContent["content"]["state_default"] is num)
|
||||
query += ", power_state_default=" +
|
||||
eventContent["content"]["state_default"].toString();
|
||||
if (eventContent["content"]["redact"] is num)
|
||||
query +=
|
||||
", power_redact=" + eventContent["content"]["redact"].toString();
|
||||
if (eventContent["content"]["invite"] is num)
|
||||
query +=
|
||||
", power_invite=" + eventContent["content"]["invite"].toString();
|
||||
if (eventContent["content"]["kick"] is num)
|
||||
query += ", power_kick=" + eventContent["content"]["kick"].toString();
|
||||
if (eventContent["content"]["user_default"] is num)
|
||||
query += ", power_user_default=" +
|
||||
eventContent["content"]["user_default"].toString();
|
||||
if (eventContent["content"]["events"] is Map<String, dynamic>) {
|
||||
if (eventContent["content"]["events"]["m.room.avatar"] is num)
|
||||
query += ", power_event_avatar=" +
|
||||
eventContent["content"]["events"]["m.room.avatar"].toString();
|
||||
if (eventContent["content"]["events"]["m.room.history_visibility"]
|
||||
is num)
|
||||
query += ", power_event_history_visibility=" +
|
||||
eventContent["content"]["events"]["m.room.history_visibility"]
|
||||
.toString();
|
||||
if (eventContent["content"]["events"]["m.room.canonical_alias"]
|
||||
is num)
|
||||
query += ", power_event_canonical_alias=" +
|
||||
eventContent["content"]["events"]["m.room.canonical_alias"]
|
||||
.toString();
|
||||
if (eventContent["content"]["events"]["m.room.aliases"] is num)
|
||||
query += ", power_event_aliases=" +
|
||||
eventContent["content"]["events"]["m.room.aliases"].toString();
|
||||
if (eventContent["content"]["events"]["m.room.name"] is num)
|
||||
query += ", power_event_name=" +
|
||||
eventContent["content"]["events"]["m.room.name"].toString();
|
||||
if (eventContent["content"]["events"]["m.room.power_levels"] is num)
|
||||
query += ", power_event_power_levels=" +
|
||||
eventContent["content"]["events"]["m.room.power_levels"]
|
||||
.toString();
|
||||
}
|
||||
if (query != "UPDATE Rooms SET ") {
|
||||
query = query.replaceFirst(",", "");
|
||||
txn.rawUpdate(query + " WHERE id=?", [chat_id]);
|
||||
}
|
||||
|
||||
// Set the users power levels:
|
||||
if (eventContent["content"]["users"] is Map<String, dynamic>) {
|
||||
eventContent["content"]["users"]
|
||||
.forEach((String user, dynamic value) async {
|
||||
num power_level = eventContent["content"]["users"][user];
|
||||
txn.rawUpdate(
|
||||
"UPDATE Users SET power_level=? WHERE matrix_id=? AND chat_id=?",
|
||||
[power_level, user, chat_id]);
|
||||
txn.rawInsert(
|
||||
"INSERT OR IGNORE INTO Users VALUES(?, ?, '', '', ?, ?)",
|
||||
[chat_id, user, "unknown", power_level]);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns a User object by a given Matrix ID and a Room.
|
||||
Future<User> getUser({String matrixID, Room room}) async {
|
||||
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||
"SELECT * FROM Users WHERE matrix_id=? AND chat_id=?",
|
||||
"SELECT * FROM RoomStates WHERE state_key=? AND room_id=?",
|
||||
[matrixID, room.id]);
|
||||
if (res.length != 1) return null;
|
||||
return User.fromJson(res[0], room);
|
||||
return RoomState.fromJson(res[0], room).asUser;
|
||||
}
|
||||
|
||||
/// Loads all Users in the database to provide a contact list
|
||||
/// except users who are in the Room with the ID [exceptRoomID].
|
||||
Future<List<User>> loadContacts({String exceptRoomID = ""}) async {
|
||||
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||
"SELECT * FROM Users WHERE matrix_id!=? AND chat_id!=? GROUP BY matrix_id ORDER BY displayname",
|
||||
"SELECT * FROM RoomStates WHERE state_key LIKE '@%:%' AND state_key!=? AND room_id!=? GROUP BY state_key ORDER BY state_key",
|
||||
[client.userID, exceptRoomID]);
|
||||
List<User> userList = [];
|
||||
for (int i = 0; i < res.length; i++)
|
||||
userList.add(User.fromJson(res[i], Room(id: "", client: client)));
|
||||
userList
|
||||
.add(RoomState.fromJson(res[i], Room(id: "", client: client)).asUser);
|
||||
return userList;
|
||||
}
|
||||
|
||||
|
@ -482,15 +324,15 @@ class Store {
|
|||
Future<List<User>> loadParticipants(Room room) async {
|
||||
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||
"SELECT * " +
|
||||
" FROM Users " +
|
||||
" WHERE chat_id=? " +
|
||||
" AND membership='join'",
|
||||
" FROM RoomStates " +
|
||||
" WHERE room_id=? " +
|
||||
" AND type='m.room.member'",
|
||||
[room.id]);
|
||||
|
||||
List<User> participants = [];
|
||||
|
||||
for (num i = 0; i < res.length; i++) {
|
||||
participants.add(User.fromJson(res[i], room));
|
||||
participants.add(RoomState.fromJson(res[i], room).asUser);
|
||||
}
|
||||
|
||||
return participants;
|
||||
|
@ -498,26 +340,18 @@ class Store {
|
|||
|
||||
/// Returns a list of events for the given room and sets all participants.
|
||||
Future<List<Event>> getEventList(Room room) async {
|
||||
List<Map<String, dynamic>> memberRes = await db.rawQuery(
|
||||
"SELECT * " + " FROM Users " + " WHERE Users.chat_id=?", [room.id]);
|
||||
Map<String, User> userMap = {};
|
||||
for (num i = 0; i < memberRes.length; i++)
|
||||
userMap[memberRes[i]["matrix_id"]] = User.fromJson(memberRes[i], room);
|
||||
|
||||
List<Map<String, dynamic>> eventRes = await db.rawQuery(
|
||||
"SELECT * " +
|
||||
" FROM Events events " +
|
||||
" WHERE events.chat_id=?" +
|
||||
" GROUP BY events.id " +
|
||||
" FROM Events " +
|
||||
" WHERE room_id=?" +
|
||||
" GROUP BY event_id " +
|
||||
" ORDER BY origin_server_ts DESC",
|
||||
[room.id]);
|
||||
|
||||
List<Event> eventList = [];
|
||||
|
||||
for (num i = 0; i < eventRes.length; i++)
|
||||
eventList.add(Event.fromJson(eventRes[i], room,
|
||||
senderUser: userMap[eventRes[i]["sender"]],
|
||||
stateKeyUser: userMap[eventRes[i]["state_key"]]));
|
||||
eventList.add(Event.fromJson(eventRes[i], room));
|
||||
|
||||
return eventList;
|
||||
}
|
||||
|
@ -528,25 +362,17 @@ class Store {
|
|||
bool onlyDirect = false,
|
||||
bool onlyGroups = false}) async {
|
||||
if (onlyDirect && onlyGroups) return [];
|
||||
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||
"SELECT rooms.*, events.origin_server_ts, events.content_json, events.type, events.sender, events.status, events.state_key " +
|
||||
" FROM Rooms rooms LEFT JOIN Events events " +
|
||||
" ON rooms.id=events.chat_id " +
|
||||
" WHERE rooms.membership" +
|
||||
(onlyLeft ? "=" : "!=") +
|
||||
"'leave' " +
|
||||
(onlyDirect ? " AND rooms.direct_chat_matrix_id!= '' " : "") +
|
||||
(onlyGroups ? " AND rooms.direct_chat_matrix_id= '' " : "") +
|
||||
" GROUP BY rooms.id " +
|
||||
" ORDER BY origin_server_ts DESC ");
|
||||
List<Map<String, dynamic>> res = await db.rawQuery("SELECT * " +
|
||||
" FROM Rooms" +
|
||||
" WHERE membership" +
|
||||
(onlyLeft ? "=" : "!=") +
|
||||
"'leave' " +
|
||||
" GROUP BY room_id ");
|
||||
List<Room> roomList = [];
|
||||
for (num i = 0; i < res.length; i++) {
|
||||
try {
|
||||
Room room = await Room.getRoomFromTableRow(res[i], client);
|
||||
roomList.add(room);
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
Room room = await Room.getRoomFromTableRow(res[i], client,
|
||||
states: getStatesFromRoomId(res[i]["room_id"]));
|
||||
roomList.add(room);
|
||||
}
|
||||
return roomList;
|
||||
}
|
||||
|
@ -554,114 +380,47 @@ class Store {
|
|||
/// Returns a room without events and participants.
|
||||
Future<Room> getRoomById(String id) async {
|
||||
List<Map<String, dynamic>> res =
|
||||
await db.rawQuery("SELECT * FROM Rooms WHERE id=?", [id]);
|
||||
await db.rawQuery("SELECT * FROM Rooms WHERE room_id=?", [id]);
|
||||
if (res.length != 1) return null;
|
||||
return Room.getRoomFromTableRow(res[0], client);
|
||||
return Room.getRoomFromTableRow(res[0], client,
|
||||
states: getStatesFromRoomId(id));
|
||||
}
|
||||
|
||||
/// Returns a room without events and participants.
|
||||
Future<Room> getRoomByAlias(String alias) async {
|
||||
List<Map<String, dynamic>> res = await db
|
||||
.rawQuery("SELECT * FROM Rooms WHERE canonical_alias=?", [alias]);
|
||||
if (res.length != 1) return null;
|
||||
return Room.getRoomFromTableRow(res[0], client);
|
||||
}
|
||||
|
||||
/// Calculates and returns an avatar for a direct chat by a given [roomID].
|
||||
Future<String> getAvatarFromSingleChat(String roomID) async {
|
||||
String avatarStr = "";
|
||||
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||
"SELECT avatar_url FROM Users " +
|
||||
" WHERE Users.chat_id=? " +
|
||||
" AND (Users.membership='join' OR Users.membership='invite') " +
|
||||
" AND Users.matrix_id!=? ",
|
||||
[roomID, client.userID]);
|
||||
if (res.length == 1) avatarStr = res[0]["avatar_url"];
|
||||
return avatarStr;
|
||||
}
|
||||
|
||||
/// Calculates a chat name for a groupchat without a name. The chat name will
|
||||
/// be the name of all users (excluding the user of this client) divided by
|
||||
/// ','.
|
||||
Future<String> getChatNameFromMemberNames(String roomID) async {
|
||||
String displayname = 'Empty chat';
|
||||
List<Map<String, dynamic>> rs = await db.rawQuery(
|
||||
"SELECT Users.displayname, Users.matrix_id, Users.membership FROM Users " +
|
||||
" WHERE Users.chat_id=? " +
|
||||
" AND (Users.membership='join' OR Users.membership='invite') " +
|
||||
" AND Users.matrix_id!=? ",
|
||||
[roomID, client.userID]);
|
||||
if (rs.length > 0) {
|
||||
displayname = "";
|
||||
for (var i = 0; i < rs.length; i++) {
|
||||
String username = rs[i]["displayname"];
|
||||
if (username == "" || username == null) username = rs[i]["matrix_id"];
|
||||
if (rs[i]["state_key"] != client.userID) displayname += username + ", ";
|
||||
}
|
||||
if (displayname == "" || displayname == null)
|
||||
displayname = 'Empty chat';
|
||||
else
|
||||
displayname = displayname.substring(0, displayname.length - 2);
|
||||
}
|
||||
return displayname;
|
||||
}
|
||||
|
||||
/// Returns the (first) room ID from the store which is a private chat with
|
||||
/// the user [userID]. Returns null if there is none.
|
||||
Future<String> getDirectChatRoomID(String userID) async {
|
||||
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||
"SELECT id FROM Rooms WHERE direct_chat_matrix_id=? AND membership!='leave' LIMIT 1",
|
||||
[userID]);
|
||||
if (res.length != 1) return null;
|
||||
return res[0]["id"];
|
||||
}
|
||||
|
||||
/// Returns the power level of the user for the given [roomID]. Returns null if
|
||||
/// the room or the own user wasn't found.
|
||||
Future<int> getPowerLevel(String roomID) async {
|
||||
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||
"SELECT power_level FROM Users WHERE matrix_id=? AND chat_id=?",
|
||||
[roomID, client.userID]);
|
||||
if (res.length != 1) return null;
|
||||
return res[0]["power_level"];
|
||||
}
|
||||
|
||||
/// Returns the power levels from all users for the given [roomID].
|
||||
Future<Map<String, int>> getPowerLevels(String roomID) async {
|
||||
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||
"SELECT matrix_id, power_level FROM Users WHERE chat_id=?",
|
||||
[roomID, client.userID]);
|
||||
Map<String, int> powerMap = {};
|
||||
for (int i = 0; i < res.length; i++)
|
||||
powerMap[res[i]["matrix_id"]] = res[i]["power_level"];
|
||||
return powerMap;
|
||||
}
|
||||
|
||||
Future<Map<String, List<String>>> getAccountDataDirectChats() async {
|
||||
Map<String, List<String>> directChats = {};
|
||||
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||
"SELECT id, direct_chat_matrix_id FROM Rooms WHERE direct_chat_matrix_id!=''");
|
||||
for (int i = 0; i < res.length; i++) {
|
||||
if (directChats.containsKey(res[i]["direct_chat_matrix_id"]))
|
||||
directChats[res[i]["direct_chat_matrix_id"]].add(res[i]["id"]);
|
||||
else
|
||||
directChats[res[i]["direct_chat_matrix_id"]] = [res[i]["id"]];
|
||||
}
|
||||
return directChats;
|
||||
Future<List<Map<String, dynamic>>> getStatesFromRoomId(String id) async {
|
||||
return db.rawQuery("SELECT * FROM RoomStates WHERE room_id=?", [id]);
|
||||
}
|
||||
|
||||
Future<void> forgetRoom(String roomID) async {
|
||||
await db.rawDelete("DELETE FROM Rooms WHERE id=?", [roomID]);
|
||||
await db.rawDelete("DELETE FROM Rooms WHERE room_id=?", [roomID]);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Searches for the event in the store.
|
||||
Future<Event> getEventById(String eventID, Room room) async {
|
||||
List<Map<String, dynamic>> res = await db.rawQuery(
|
||||
"SELECT * FROM Events WHERE id=? AND chat_id=?", [eventID, room.id]);
|
||||
"SELECT * FROM Events WHERE id=? AND room_id=?", [eventID, room.id]);
|
||||
if (res.length == 0) return null;
|
||||
return Event.fromJson(res[0], room,
|
||||
senderUser: (await room.getUserByMXID(res[0]["sender"])));
|
||||
return Event.fromJson(res[0], room);
|
||||
}
|
||||
|
||||
Future<Map<String, AccountData>> getAccountData() async {
|
||||
Map<String, AccountData> newAccountData = {};
|
||||
List<Map<String, dynamic>> rawAccountData =
|
||||
await db.rawQuery("SELECT * FROM AccountData");
|
||||
for (int i = 0; i < rawAccountData.length; i++)
|
||||
newAccountData[rawAccountData[i]["type"]] =
|
||||
AccountData.fromJson(rawAccountData[i]);
|
||||
return newAccountData;
|
||||
}
|
||||
|
||||
Future<Map<String, Presence>> getPresences() async {
|
||||
Map<String, Presence> newPresences = {};
|
||||
List<Map<String, dynamic>> rawPresences =
|
||||
await db.rawQuery("SELECT * FROM Presences");
|
||||
for (int i = 0; i < rawPresences.length; i++)
|
||||
newPresences[rawPresences[i]["type"]] =
|
||||
Presence.fromJson(rawPresences[i]);
|
||||
return newPresences;
|
||||
}
|
||||
|
||||
Future forgetNotification(String roomID) async {
|
||||
|
@ -679,7 +438,8 @@ class Store {
|
|||
"INSERT INTO NotificationsCache(id, chat_id, event_id) VALUES (?, ?, ?)",
|
||||
[uniqueID, roomID, event_id]);
|
||||
// Make sure we got the same unique ID everywhere
|
||||
await db.rawUpdate("UPDATE NotificationsCache SET id=? WHERE chat_id=?", [uniqueID, roomID]);
|
||||
await db.rawUpdate("UPDATE NotificationsCache SET id=? WHERE chat_id=?",
|
||||
[uniqueID, roomID]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -707,70 +467,63 @@ class Store {
|
|||
'UNIQUE(client))',
|
||||
|
||||
/// The database scheme for the Room class.
|
||||
"Rooms": 'CREATE TABLE IF NOT EXISTS Rooms(' +
|
||||
'id TEXT PRIMARY KEY, ' +
|
||||
'Rooms': 'CREATE TABLE IF NOT EXISTS Rooms(' +
|
||||
'room_id TEXT PRIMARY KEY, ' +
|
||||
'membership TEXT, ' +
|
||||
'topic TEXT, ' +
|
||||
'highlight_count INTEGER, ' +
|
||||
'notification_count INTEGER, ' +
|
||||
'prev_batch TEXT, ' +
|
||||
'joined_member_count INTEGER, ' +
|
||||
'invited_member_count INTEGER, ' +
|
||||
'heroes TEXT, ' +
|
||||
'prev_batch TEXT, ' +
|
||||
'avatar_url TEXT, ' +
|
||||
'draft TEXT, ' +
|
||||
'unread INTEGER, ' + // Timestamp of when the user has last read the chat
|
||||
'fully_read TEXT, ' + // ID of the fully read marker event
|
||||
'description TEXT, ' +
|
||||
'canonical_alias TEXT, ' + // The address in the form: #roomname:homeserver.org
|
||||
'direct_chat_matrix_id TEXT, ' + //If this room is a direct chat, this is the matrix ID of the user
|
||||
'notification_settings TEXT, ' + // Must be one of [all, mention]
|
||||
'UNIQUE(room_id))',
|
||||
|
||||
// Security rules
|
||||
'guest_access TEXT, ' +
|
||||
'history_visibility TEXT, ' +
|
||||
'join_rules TEXT, ' +
|
||||
/// The database scheme for the TimelineEvent class.
|
||||
'Events': 'CREATE TABLE IF NOT EXISTS Events(' +
|
||||
'event_id TEXT PRIMARY KEY, ' +
|
||||
'room_id TEXT, ' +
|
||||
'origin_server_ts INTEGER, ' +
|
||||
'sender TEXT, ' +
|
||||
'type TEXT, ' +
|
||||
'unsigned TEXT, ' +
|
||||
'content TEXT, ' +
|
||||
'prev_content TEXT, ' +
|
||||
'state_key TEXT, ' +
|
||||
"status INTEGER, " +
|
||||
'UNIQUE(event_id))',
|
||||
|
||||
// Power levels
|
||||
'power_events_default INTEGER, ' +
|
||||
'power_state_default INTEGER, ' +
|
||||
'power_redact INTEGER, ' +
|
||||
'power_invite INTEGER, ' +
|
||||
'power_ban INTEGER, ' +
|
||||
'power_kick INTEGER, ' +
|
||||
'power_user_default INTEGER, ' +
|
||||
|
||||
// Power levels for events
|
||||
'power_event_avatar INTEGER, ' +
|
||||
'power_event_history_visibility INTEGER, ' +
|
||||
'power_event_canonical_alias INTEGER, ' +
|
||||
'power_event_aliases INTEGER, ' +
|
||||
'power_event_name INTEGER, ' +
|
||||
'power_event_power_levels INTEGER, ' +
|
||||
'UNIQUE(id))',
|
||||
|
||||
/// The database scheme for the Event class.
|
||||
"Events": 'CREATE TABLE IF NOT EXISTS Events(' +
|
||||
'id TEXT PRIMARY KEY, ' +
|
||||
'chat_id TEXT, ' +
|
||||
/// The database scheme for room states.
|
||||
'RoomStates': 'CREATE TABLE IF NOT EXISTS RoomStates(' +
|
||||
'event_id TEXT PRIMARY KEY, ' +
|
||||
'room_id TEXT, ' +
|
||||
'origin_server_ts INTEGER, ' +
|
||||
'sender TEXT, ' +
|
||||
'state_key TEXT, ' +
|
||||
'content_body TEXT, ' +
|
||||
'unsigned TEXT, ' +
|
||||
'prev_content TEXT, ' +
|
||||
'type TEXT, ' +
|
||||
'content_json TEXT, ' +
|
||||
"status INTEGER, " +
|
||||
'UNIQUE(id))',
|
||||
'content TEXT, ' +
|
||||
'UNIQUE(room_id,state_key,type))',
|
||||
|
||||
/// The database scheme for the User class.
|
||||
"Users": 'CREATE TABLE IF NOT EXISTS Users(' +
|
||||
'chat_id TEXT, ' + // The chat id of this membership
|
||||
'matrix_id TEXT, ' + // The matrix id of this user
|
||||
'displayname TEXT, ' +
|
||||
'avatar_url TEXT, ' +
|
||||
'membership TEXT, ' + // The status of the membership. Must be one of [join, invite, ban, leave]
|
||||
'power_level INTEGER, ' + // The power level of this user. Must be in [0,..,100]
|
||||
'UNIQUE(chat_id, matrix_id))',
|
||||
/// The database scheme for room states.
|
||||
'AccountData': 'CREATE TABLE IF NOT EXISTS AccountData(' +
|
||||
'type TEXT PRIMARY KEY, ' +
|
||||
'content TEXT, ' +
|
||||
'UNIQUE(type))',
|
||||
|
||||
/// The database scheme for room states.
|
||||
'RoomAccountData': 'CREATE TABLE IF NOT EXISTS RoomAccountData(' +
|
||||
'type TEXT PRIMARY KEY, ' +
|
||||
'room_id TEXT, ' +
|
||||
'content TEXT, ' +
|
||||
'UNIQUE(type,room_id))',
|
||||
|
||||
/// The database scheme for room states.
|
||||
'Presences': 'CREATE TABLE IF NOT EXISTS Presences(' +
|
||||
'type TEXT PRIMARY KEY, ' +
|
||||
'sender TEXT, ' +
|
||||
'content TEXT, ' +
|
||||
'UNIQUE(sender))',
|
||||
|
||||
/// The database scheme for the NotificationsCache class.
|
||||
"NotificationsCache": 'CREATE TABLE IF NOT EXISTS NotificationsCache(' +
|
||||
|
|
|
@ -28,6 +28,9 @@ import 'Room.dart';
|
|||
import 'User.dart';
|
||||
import 'sync/EventUpdate.dart';
|
||||
|
||||
typedef onTimelineUpdateCallback = void Function();
|
||||
typedef onTimelineInsertCallback = void Function(int insertID);
|
||||
|
||||
/// 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.
|
||||
|
@ -47,8 +50,8 @@ class Timeline {
|
|||
int _findEvent({String event_id, String unsigned_txid}) {
|
||||
int i;
|
||||
for (i = 0; i < events.length; i++) {
|
||||
if (events[i].id == event_id ||
|
||||
(unsigned_txid != null && events[i].id == unsigned_txid)) break;
|
||||
if (events[i].eventId == event_id ||
|
||||
(unsigned_txid != null && events[i].eventId == unsigned_txid)) break;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
@ -82,33 +85,7 @@ class Timeline {
|
|||
eventUpdate.content["avatar_url"] = senderUser.avatarUrl.mxc;
|
||||
}
|
||||
|
||||
User stateKeyUser;
|
||||
if (eventUpdate.content.containsKey("state_key")) {
|
||||
stateKeyUser = await room.client.store?.getUser(
|
||||
matrixID: eventUpdate.content["state_key"], room: room);
|
||||
}
|
||||
|
||||
if (senderUser != null && stateKeyUser != null) {
|
||||
newEvent = Event.fromJson(eventUpdate.content, room,
|
||||
senderUser: senderUser, stateKeyUser: stateKeyUser);
|
||||
} else if (senderUser != null) {
|
||||
newEvent = Event.fromJson(eventUpdate.content, room,
|
||||
senderUser: senderUser);
|
||||
} else if (stateKeyUser != null) {
|
||||
newEvent = Event.fromJson(eventUpdate.content, room,
|
||||
stateKeyUser: stateKeyUser);
|
||||
} else {
|
||||
newEvent = Event.fromJson(eventUpdate.content, room);
|
||||
}
|
||||
|
||||
// TODO update to type check when https://gitlab.com/famedly/famedlysdk/merge_requests/28/ is merged
|
||||
if (newEvent.content.containsKey("m.relates_to")) {
|
||||
Map<String, dynamic> relates_to = newEvent.content["m.relates_to"];
|
||||
if (relates_to.containsKey("m.in_reply_to")) {
|
||||
newEvent.replyEvent = await room.getEventById(newEvent
|
||||
.content["m.relates_to"]["m.in_reply_to"]["event_id"]);
|
||||
}
|
||||
}
|
||||
newEvent = Event.fromJson(eventUpdate.content, room);
|
||||
|
||||
events.insert(0, newEvent);
|
||||
if (onInsert != null) onInsert(0);
|
||||
|
@ -128,6 +105,3 @@ class Timeline {
|
|||
if (onUpdate != null) onUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
typedef onTimelineUpdateCallback = void Function();
|
||||
typedef onTimelineInsertCallback = void Function(int insertID);
|
||||
|
|
|
@ -22,7 +22,9 @@
|
|||
*/
|
||||
|
||||
import 'package:famedlysdk/src/Room.dart';
|
||||
import 'package:famedlysdk/src/RoomState.dart';
|
||||
import 'package:famedlysdk/src/responses/ErrorResponse.dart';
|
||||
import 'package:famedlysdk/src/utils/ChatTime.dart';
|
||||
import 'package:famedlysdk/src/utils/MxContent.dart';
|
||||
|
||||
import 'Connection.dart';
|
||||
|
@ -30,85 +32,80 @@ import 'Connection.dart';
|
|||
enum Membership { join, invite, leave, ban }
|
||||
|
||||
/// Represents a Matrix User which may be a participant in a Matrix Room.
|
||||
class User {
|
||||
class User extends RoomState {
|
||||
factory User(
|
||||
String id, {
|
||||
String membership,
|
||||
String displayName,
|
||||
String avatarUrl,
|
||||
Room room,
|
||||
}) {
|
||||
Map<String, String> content = {};
|
||||
if (membership != null) content["membership"] = membership;
|
||||
if (displayName != null) content["displayname"] = displayName;
|
||||
if (avatarUrl != null) content["avatar_url"] = avatarUrl;
|
||||
return User.fromState(
|
||||
stateKey: id,
|
||||
content: content,
|
||||
typeKey: "m.room.member",
|
||||
roomId: room?.id,
|
||||
room: room,
|
||||
time: ChatTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
User.fromState(
|
||||
{dynamic prevContent,
|
||||
String stateKey,
|
||||
dynamic content,
|
||||
String typeKey,
|
||||
String eventId,
|
||||
String roomId,
|
||||
String senderId,
|
||||
ChatTime time,
|
||||
dynamic unsigned,
|
||||
Room room})
|
||||
: super(
|
||||
stateKey: stateKey,
|
||||
prevContent: prevContent,
|
||||
content: content,
|
||||
typeKey: typeKey,
|
||||
eventId: eventId,
|
||||
roomId: roomId,
|
||||
senderId: senderId,
|
||||
time: time,
|
||||
unsigned: unsigned,
|
||||
room: room);
|
||||
|
||||
/// The full qualified Matrix ID in the format @username:server.abc.
|
||||
final String id;
|
||||
String get id => stateKey;
|
||||
|
||||
/// The displayname of the user if the user has set one.
|
||||
final String displayName;
|
||||
String get displayName => content != null ? content["displayname"] : null;
|
||||
|
||||
/// The membership status of the user. One of:
|
||||
/// join
|
||||
/// invite
|
||||
/// leave
|
||||
/// ban
|
||||
Membership membership;
|
||||
Membership get membership => Membership.values.firstWhere((e) {
|
||||
if (content["membership"] != null) {
|
||||
return e.toString() == 'Membership.' + content['membership'];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
/// The avatar if the user has one.
|
||||
MxContent avatarUrl;
|
||||
|
||||
/// The powerLevel of the user. Normally:
|
||||
/// 0=Normal user
|
||||
/// 50=Moderator
|
||||
/// 100=Admin
|
||||
int powerLevel = 0;
|
||||
|
||||
/// All users normally belong to a room.
|
||||
final Room room;
|
||||
|
||||
@Deprecated("Use membership instead!")
|
||||
String get status => membership.toString().split('.').last;
|
||||
|
||||
@Deprecated("Use ID instead!")
|
||||
String get mxid => id;
|
||||
|
||||
@Deprecated("Use avatarUrl instead!")
|
||||
MxContent get avatar_url => avatarUrl;
|
||||
|
||||
User(
|
||||
String id, {
|
||||
this.membership,
|
||||
this.displayName,
|
||||
this.avatarUrl,
|
||||
this.powerLevel,
|
||||
this.room,
|
||||
}) : this.id = id ?? "";
|
||||
MxContent get avatarUrl => content != null && content["avatar_url"] is String
|
||||
? MxContent(content["avatar_url"])
|
||||
: MxContent("");
|
||||
|
||||
/// Returns the displayname or the local part of the Matrix ID if the user
|
||||
/// has no displayname.
|
||||
String calcDisplayname() => (displayName == null || displayName.isEmpty)
|
||||
? id.replaceFirst("@", "").split(":")[0]
|
||||
? stateKey.replaceFirst("@", "").split(":")[0]
|
||||
: displayName;
|
||||
|
||||
/// Creates a new User object from a json string like a row from the database.
|
||||
static User fromJson(Map<String, dynamic> json, Room room) {
|
||||
return User(json['matrix_id'] ?? json['sender'],
|
||||
displayName: json['displayname'],
|
||||
avatarUrl: MxContent(json['avatar_url']),
|
||||
membership: Membership.values.firstWhere((e) {
|
||||
if (json["membership"] != null) {
|
||||
return e.toString() == 'Membership.' + json['membership'];
|
||||
}
|
||||
return false;
|
||||
}, orElse: () => null),
|
||||
powerLevel: json['power_level'],
|
||||
room: room);
|
||||
}
|
||||
|
||||
/// Checks if the client's user has the permission to kick this user.
|
||||
Future<bool> get canKick async {
|
||||
final int ownPowerLevel = await room.client.store.getPowerLevel(room.id);
|
||||
return ownPowerLevel > powerLevel &&
|
||||
ownPowerLevel >= room.powerLevels["power_kick"];
|
||||
}
|
||||
|
||||
/// Checks if the client's user has the permission to ban or unban this user.
|
||||
Future<bool> get canBan async {
|
||||
final int ownPowerLevel = await room.client.store.getPowerLevel(room.id);
|
||||
return ownPowerLevel > powerLevel &&
|
||||
ownPowerLevel >= room.powerLevels["power_ban"];
|
||||
}
|
||||
|
||||
/// Call the Matrix API to kick this user from this room.
|
||||
Future<dynamic> kick() async {
|
||||
dynamic res = await room.kick(id);
|
||||
|
@ -137,7 +134,7 @@ class User {
|
|||
/// Returns null on error.
|
||||
Future<String> startDirectChat() async {
|
||||
// Try to find an existing direct chat
|
||||
String roomID = await room.client?.store?.getDirectChatRoomID(id);
|
||||
String roomID = await room.client?.getDirectChatFromUserId(id);
|
||||
if (roomID != null) return roomID;
|
||||
|
||||
// Start a new direct chat
|
||||
|
|
|
@ -59,7 +59,6 @@ class ChatTime {
|
|||
return toTimeString();
|
||||
} else if (sameWeek) {
|
||||
switch (dateTime.weekday) {
|
||||
// TODO: Needs localization
|
||||
case 1:
|
||||
return "Montag";
|
||||
case 2:
|
||||
|
|
|
@ -23,8 +23,10 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:famedlysdk/src/AccountData.dart';
|
||||
import 'package:famedlysdk/src/Client.dart';
|
||||
import 'package:famedlysdk/src/Connection.dart';
|
||||
import 'package:famedlysdk/src/Presence.dart';
|
||||
import 'package:famedlysdk/src/User.dart';
|
||||
import 'package:famedlysdk/src/requests/SetPushersRequest.dart';
|
||||
import 'package:famedlysdk/src/responses/ErrorResponse.dart';
|
||||
|
@ -61,10 +63,19 @@ void main() {
|
|||
Future<ErrorResponse> errorFuture =
|
||||
matrix.connection.onError.stream.first;
|
||||
|
||||
int presenceCounter = 0;
|
||||
int accountDataCounter = 0;
|
||||
matrix.onPresence = (Presence data) {
|
||||
presenceCounter++;
|
||||
};
|
||||
matrix.onAccountData = (AccountData data) {
|
||||
accountDataCounter++;
|
||||
};
|
||||
|
||||
final bool checkResp1 =
|
||||
await matrix.checkServer("https://fakeServer.wrongaddress");
|
||||
await matrix.checkServer("https://fakeserver.wrongaddress");
|
||||
final bool checkResp2 =
|
||||
await matrix.checkServer("https://fakeServer.notExisting");
|
||||
await matrix.checkServer("https://fakeserver.notexisting");
|
||||
|
||||
ErrorResponse checkError = await errorFuture;
|
||||
|
||||
|
@ -107,6 +118,23 @@ void main() {
|
|||
expect(loginState, LoginState.logged);
|
||||
expect(firstSync, true);
|
||||
expect(sync["next_batch"] == matrix.prevBatch, true);
|
||||
|
||||
expect(matrix.accountData.length, 2);
|
||||
expect(matrix.getDirectChatFromUserId("@bob:example.com"),
|
||||
"!726s6s6q:example.com");
|
||||
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.length, 2);
|
||||
expect(matrix.roomList.rooms[1].canonicalAlias,
|
||||
"#famedlyContactDiscovery:${matrix.userID.split(":")[1]}");
|
||||
final List<User> contacts = await matrix.loadFamedlyContacts();
|
||||
expect(contacts.length, 1);
|
||||
expect(contacts[0].senderId, "@alice:example.org");
|
||||
expect(
|
||||
matrix.presences["@alice:example.com"].content["presence"], "online");
|
||||
expect(presenceCounter, 1);
|
||||
expect(accountDataCounter, 2);
|
||||
});
|
||||
|
||||
test('Try to get ErrorResponse', () async {
|
||||
|
@ -172,36 +200,39 @@ void main() {
|
|||
|
||||
List<EventUpdate> eventUpdateList = await eventUpdateListFuture;
|
||||
|
||||
expect(eventUpdateList.length, 7);
|
||||
expect(eventUpdateList.length, 8);
|
||||
|
||||
expect(eventUpdateList[0].eventType == "m.room.member", true);
|
||||
expect(eventUpdateList[0].roomID == "!726s6s6q:example.com", true);
|
||||
expect(eventUpdateList[0].type == "state", true);
|
||||
expect(eventUpdateList[0].eventType, "m.room.member");
|
||||
expect(eventUpdateList[0].roomID, "!726s6s6q:example.com");
|
||||
expect(eventUpdateList[0].type, "state");
|
||||
|
||||
expect(eventUpdateList[1].eventType == "m.room.member", true);
|
||||
expect(eventUpdateList[1].roomID == "!726s6s6q:example.com", true);
|
||||
expect(eventUpdateList[1].type == "timeline", true);
|
||||
expect(eventUpdateList[1].eventType, "m.room.canonical_alias");
|
||||
expect(eventUpdateList[1].roomID, "!726s6s6q:example.com");
|
||||
expect(eventUpdateList[1].type, "state");
|
||||
|
||||
expect(eventUpdateList[2].eventType == "m.room.message", true);
|
||||
expect(eventUpdateList[2].roomID == "!726s6s6q:example.com", true);
|
||||
expect(eventUpdateList[2].type == "timeline", true);
|
||||
expect(eventUpdateList[2].eventType, "m.room.member");
|
||||
expect(eventUpdateList[2].roomID, "!726s6s6q:example.com");
|
||||
expect(eventUpdateList[2].type, "timeline");
|
||||
|
||||
expect(eventUpdateList[3].eventType == "m.tag", true);
|
||||
expect(eventUpdateList[3].roomID == "!726s6s6q:example.com", true);
|
||||
expect(eventUpdateList[3].type == "account_data", true);
|
||||
expect(eventUpdateList[3].eventType, "m.room.message");
|
||||
expect(eventUpdateList[3].roomID, "!726s6s6q:example.com");
|
||||
expect(eventUpdateList[3].type, "timeline");
|
||||
|
||||
expect(eventUpdateList[4].eventType == "org.example.custom.room.config",
|
||||
true);
|
||||
expect(eventUpdateList[4].roomID == "!726s6s6q:example.com", true);
|
||||
expect(eventUpdateList[4].type == "account_data", true);
|
||||
expect(eventUpdateList[4].eventType, "m.tag");
|
||||
expect(eventUpdateList[4].roomID, "!726s6s6q:example.com");
|
||||
expect(eventUpdateList[4].type, "account_data");
|
||||
|
||||
expect(eventUpdateList[5].eventType == "m.room.name", true);
|
||||
expect(eventUpdateList[5].roomID == "!696r7674:example.com", true);
|
||||
expect(eventUpdateList[5].type == "invite_state", true);
|
||||
expect(eventUpdateList[5].eventType, "org.example.custom.room.config");
|
||||
expect(eventUpdateList[5].roomID, "!726s6s6q:example.com");
|
||||
expect(eventUpdateList[5].type, "account_data");
|
||||
|
||||
expect(eventUpdateList[6].eventType == "m.room.member", true);
|
||||
expect(eventUpdateList[6].roomID == "!696r7674:example.com", true);
|
||||
expect(eventUpdateList[6].type == "invite_state", true);
|
||||
expect(eventUpdateList[6].eventType, "m.room.name");
|
||||
expect(eventUpdateList[6].roomID, "!696r7674:example.com");
|
||||
expect(eventUpdateList[6].type, "invite_state");
|
||||
|
||||
expect(eventUpdateList[7].eventType, "m.room.member");
|
||||
expect(eventUpdateList[7].roomID, "!696r7674:example.com");
|
||||
expect(eventUpdateList[7].type, "invite_state");
|
||||
});
|
||||
|
||||
test('User Update Test', () async {
|
||||
|
@ -209,16 +240,13 @@ void main() {
|
|||
|
||||
List<UserUpdate> eventUpdateList = await userUpdateListFuture;
|
||||
|
||||
expect(eventUpdateList.length, 3);
|
||||
expect(eventUpdateList.length, 4);
|
||||
|
||||
expect(eventUpdateList[0].eventType == "m.presence", true);
|
||||
expect(eventUpdateList[0].type == "presence", true);
|
||||
|
||||
expect(eventUpdateList[1].eventType == "org.example.custom.config", true);
|
||||
expect(eventUpdateList[1].type == "account_data", true);
|
||||
|
||||
expect(eventUpdateList[2].eventType == "m.new_device", true);
|
||||
expect(eventUpdateList[2].type == "to_device", true);
|
||||
});
|
||||
|
||||
testWidgets('should get created', create);
|
||||
|
|
|
@ -24,8 +24,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/Event.dart';
|
||||
import 'package:famedlysdk/src/User.dart';
|
||||
import 'package:famedlysdk/src/RoomState.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'FakeMatrixApi.dart';
|
||||
|
@ -36,9 +35,6 @@ void main() {
|
|||
final int timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final String id = "!4fsdfjisjf:server.abc";
|
||||
final String senderID = "@alice:server.abc";
|
||||
final String senderDisplayname = "Alice";
|
||||
final String empty = "";
|
||||
final Membership membership = Membership.join;
|
||||
final String type = "m.room.message";
|
||||
final String msgtype = "m.text";
|
||||
final String body = "Hello World";
|
||||
|
@ -49,29 +45,29 @@ void main() {
|
|||
|
||||
Map<String, dynamic> jsonObj = {
|
||||
"event_id": id,
|
||||
"matrix_id": senderID,
|
||||
"displayname": senderDisplayname,
|
||||
"avatar_url": empty,
|
||||
"membership": membership.toString().split('.').last,
|
||||
"sender": senderID,
|
||||
"origin_server_ts": timestamp,
|
||||
"state_key": empty,
|
||||
"type": type,
|
||||
"content_json": contentJson,
|
||||
"status": 2,
|
||||
"content": contentJson,
|
||||
};
|
||||
|
||||
test("Create from json", () async {
|
||||
Event event = Event.fromJson(jsonObj, null);
|
||||
|
||||
expect(event.id, id);
|
||||
expect(event.sender.id, senderID);
|
||||
expect(event.sender.displayName, senderDisplayname);
|
||||
expect(event.sender.avatarUrl.mxc, empty);
|
||||
expect(event.sender.membership, membership);
|
||||
expect(event.eventId, id);
|
||||
expect(event.senderId, senderID);
|
||||
expect(event.status, 2);
|
||||
expect(event.text, body);
|
||||
expect(event.formattedText, formatted_body);
|
||||
expect(event.getBody(), body);
|
||||
expect(event.type, EventTypes.Text);
|
||||
jsonObj["state_key"] = "";
|
||||
RoomState state = RoomState.fromJson(jsonObj, null);
|
||||
expect(state.eventId, id);
|
||||
expect(state.stateKey, "");
|
||||
expect(state.key, "m.room.message");
|
||||
expect(state.timelineEvent.status, 1);
|
||||
});
|
||||
test("Test all EventTypes", () async {
|
||||
Event event;
|
||||
|
@ -121,7 +117,7 @@ void main() {
|
|||
expect(event.type, EventTypes.HistoryVisibility);
|
||||
|
||||
jsonObj["type"] = "m.room.message";
|
||||
jsonObj["content"] = json.decode(jsonObj["content_json"]);
|
||||
jsonObj["content"] = json.decode(jsonObj["content"]);
|
||||
|
||||
jsonObj["content"]["msgtype"] = "m.notice";
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
|
|
|
@ -39,7 +39,7 @@ class FakeMatrixApi extends MockClient {
|
|||
method == "GET" ? request.url.queryParameters : request.body;
|
||||
var res = {};
|
||||
|
||||
print("$method request to $action with Data: $data");
|
||||
//print("$method request to $action with Data: $data");
|
||||
|
||||
// Sync requests with timeout
|
||||
if (data is Map<String, dynamic> && data["timeout"] is String) {
|
||||
|
@ -64,6 +64,20 @@ class FakeMatrixApi extends MockClient {
|
|||
|
||||
static final Map<String, Map<String, dynamic>> api = {
|
||||
"GET": {
|
||||
"/client/r0/rooms/!localpart:server.abc/state/m.room.member/@getme:example.com":
|
||||
(var req) => {
|
||||
"content": {
|
||||
"membership": "join",
|
||||
"displayname": "You got me",
|
||||
},
|
||||
"type": "m.room.member",
|
||||
"event_id": "143273582443PhrSn:example.org",
|
||||
"room_id": "!localpart:server.abc",
|
||||
"sender": "@getme:example.com",
|
||||
"state_key": "@getme:example.com",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"unsigned": {"age": 1234}
|
||||
},
|
||||
"/client/r0/rooms/!localpart:server.abc/event/1234": (var req) => {
|
||||
"content": {
|
||||
"body": "This is an example text message",
|
||||
|
@ -78,7 +92,7 @@ class FakeMatrixApi extends MockClient {
|
|||
"origin_server_ts": 1432735824653,
|
||||
"unsigned": {"age": 1234}
|
||||
},
|
||||
"/client/r0/rooms/!1234:example.com/messages?from=1234&dir=b&limit=100":
|
||||
"/client/r0/rooms/!1234:example.com/messages?from=1234&dir=b&limit=100&filter=%7B%22room%22:%7B%22state%22:%7B%22lazy_load_members%22:true%7D%7D%7D":
|
||||
(var req) => {
|
||||
"start": "t47429-4392820_219380_26003_2265",
|
||||
"end": "t47409-4357353_219380_26003_2265",
|
||||
|
@ -153,6 +167,24 @@ class FakeMatrixApi extends MockClient {
|
|||
{"type": "m.login.password"}
|
||||
]
|
||||
},
|
||||
"/client/r0/rooms/!726s6s6q:example.com/members": (var req) => {
|
||||
"chunk": [
|
||||
{
|
||||
"content": {
|
||||
"membership": "join",
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid"
|
||||
},
|
||||
"type": "m.room.member",
|
||||
"event_id": "§143273582443PhrSn:example.org",
|
||||
"room_id": "!636q39766251:example.com",
|
||||
"sender": "@alice:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"unsigned": {"age": 1234},
|
||||
"state_key": "@alice:example.org"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/client/r0/rooms/!localpart:server.abc/members": (var req) => {
|
||||
"chunk": [
|
||||
{
|
||||
|
@ -333,7 +365,16 @@ class FakeMatrixApi extends MockClient {
|
|||
{
|
||||
"type": "org.example.custom.config",
|
||||
"content": {"custom_config_key": "custom_config_value"}
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"@bob:example.com": [
|
||||
"!726s6s6q:example.com",
|
||||
"!hgfedcba:example.com"
|
||||
]
|
||||
},
|
||||
"type": "m.direct"
|
||||
},
|
||||
]
|
||||
},
|
||||
"to_device": {
|
||||
|
@ -364,6 +405,17 @@ class FakeMatrixApi extends MockClient {
|
|||
"content": {"membership": "join"},
|
||||
"origin_server_ts": 1417731086795,
|
||||
"event_id": "66697273743031:example.com"
|
||||
},
|
||||
{
|
||||
"sender": "@alice:example.com",
|
||||
"type": "m.room.canonical_alias",
|
||||
"content": {
|
||||
"alias":
|
||||
"#famedlyContactDiscovery:fakeServer.notExisting"
|
||||
},
|
||||
"state_key": "",
|
||||
"origin_server_ts": 1417731086796,
|
||||
"event_id": "66697273743032:example.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -465,12 +517,30 @@ class FakeMatrixApi extends MockClient {
|
|||
"room_id": "!1234:fakeServer.notExisting",
|
||||
},
|
||||
"/client/r0/rooms/!localpart:server.abc/read_markers": (var reqI) => {},
|
||||
"/client/r0/rooms/!localpart:server.abc/kick": (var reqI) => {},
|
||||
"/client/r0/rooms/!localpart:server.abc/ban": (var reqI) => {},
|
||||
"/client/r0/rooms/!localpart:server.abc/unban": (var reqI) => {},
|
||||
"/client/r0/rooms/!localpart:server.abc/invite": (var reqI) => {},
|
||||
},
|
||||
"PUT": {
|
||||
"/client/r0/rooms/!1234:example.com/send/m.room.message/1234":
|
||||
(var reqI) => {
|
||||
"event_id": "42",
|
||||
},
|
||||
"/client/r0/rooms/!localpart:server.abc/state/m.room.name": (var reqI) =>
|
||||
{
|
||||
"event_id": "42",
|
||||
},
|
||||
"/client/r0/rooms/!localpart:server.abc/state/m.room.topic": (var reqI) =>
|
||||
{
|
||||
"event_id": "42",
|
||||
},
|
||||
"/client/r0/rooms/!localpart:server.abc/state/m.room.power_levels":
|
||||
(var reqI) => {
|
||||
"event_id": "42",
|
||||
},
|
||||
"/client/r0/user/@test:fakeServer.notExisting/account_data/m.direct":
|
||||
(var reqI) => {},
|
||||
},
|
||||
"DELETE": {
|
||||
"/unknown/token": (var req) => {"errcode": "M_UNKNOWN_TOKEN"},
|
||||
|
|
|
@ -24,7 +24,10 @@
|
|||
import 'package:famedlysdk/src/Client.dart';
|
||||
import 'package:famedlysdk/src/Event.dart';
|
||||
import 'package:famedlysdk/src/Room.dart';
|
||||
import 'package:famedlysdk/src/RoomState.dart';
|
||||
import 'package:famedlysdk/src/Timeline.dart';
|
||||
import 'package:famedlysdk/src/User.dart';
|
||||
import 'package:famedlysdk/src/utils/ChatTime.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'FakeMatrixApi.dart';
|
||||
|
@ -50,24 +53,9 @@ void main() {
|
|||
|
||||
test("Create from json", () async {
|
||||
final String id = "!localpart:server.abc";
|
||||
final String name = "My Room";
|
||||
final Membership membership = Membership.join;
|
||||
final String topic = "This is my own room";
|
||||
final int unread = DateTime.now().millisecondsSinceEpoch;
|
||||
final int notificationCount = 2;
|
||||
final int highlightCount = 1;
|
||||
final String fullyRead = "fjh82jdjifd:server.abc";
|
||||
final String notificationSettings = "all";
|
||||
final String guestAccess = "forbidden";
|
||||
final String canonicalAlias = "#testroom:example.com";
|
||||
final String historyVisibility = "invite";
|
||||
final String joinRules = "invite";
|
||||
final int now = DateTime.now().millisecondsSinceEpoch;
|
||||
final String msgtype = "m.text";
|
||||
final String body = "Hello World";
|
||||
final String formatted_body = "<b>Hello</b> World";
|
||||
final String contentJson =
|
||||
'{"msgtype":"$msgtype","body":"$body","formatted_body":"$formatted_body"}';
|
||||
final List<String> heroes = [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com",
|
||||
|
@ -75,37 +63,12 @@ void main() {
|
|||
];
|
||||
|
||||
Map<String, dynamic> jsonObj = {
|
||||
"id": id,
|
||||
"room_id": id,
|
||||
"membership": membership.toString().split('.').last,
|
||||
"topic": name,
|
||||
"description": topic,
|
||||
"avatar_url": "",
|
||||
"notification_count": notificationCount,
|
||||
"highlight_count": highlightCount,
|
||||
"unread": unread,
|
||||
"fully_read": fullyRead,
|
||||
"notification_settings": notificationSettings,
|
||||
"direct_chat_matrix_id": "",
|
||||
"draft": "",
|
||||
"prev_batch": "",
|
||||
"guest_access": guestAccess,
|
||||
"history_visibility": historyVisibility,
|
||||
"join_rules": joinRules,
|
||||
"canonical_alias": canonicalAlias,
|
||||
"power_events_default": 0,
|
||||
"power_state_default": 0,
|
||||
"power_redact": 0,
|
||||
"power_invite": 0,
|
||||
"power_ban": 0,
|
||||
"power_kick": 0,
|
||||
"power_user_default": 0,
|
||||
"power_event_avatar": 0,
|
||||
"power_event_history_visibility": 0,
|
||||
"power_event_canonical_alias": 0,
|
||||
"power_event_aliases": 0,
|
||||
"power_event_name": 0,
|
||||
"power_event_power_levels": 0,
|
||||
"content_json": contentJson,
|
||||
"joined_member_count": notificationCount,
|
||||
"invited_member_count": notificationCount,
|
||||
"heroes": heroes.join(","),
|
||||
|
@ -115,37 +78,69 @@ void main() {
|
|||
|
||||
expect(room.id, id);
|
||||
expect(room.membership, membership);
|
||||
expect(room.name, name);
|
||||
expect(room.displayname, name);
|
||||
expect(room.topic, topic);
|
||||
expect(room.avatar.mxc, "");
|
||||
expect(room.notificationCount, notificationCount);
|
||||
expect(room.highlightCount, highlightCount);
|
||||
expect(room.unread.toTimeStamp(), unread);
|
||||
expect(room.fullyRead, fullyRead);
|
||||
expect(room.notificationSettings, notificationSettings);
|
||||
expect(room.directChatMatrixID, "");
|
||||
expect(room.draft, "");
|
||||
expect(room.canonicalAlias, canonicalAlias);
|
||||
expect(room.prev_batch, "");
|
||||
expect(room.guestAccess, guestAccess);
|
||||
expect(room.historyVisibility, historyVisibility);
|
||||
expect(room.joinRules, joinRules);
|
||||
expect(room.lastMessage, body);
|
||||
expect(room.timeCreated.toTimeStamp() >= now, true);
|
||||
room.powerLevels.forEach((String key, int value) {
|
||||
expect(value, 0);
|
||||
});
|
||||
expect(room.mJoinedMemberCount, notificationCount);
|
||||
expect(room.mInvitedMemberCount, notificationCount);
|
||||
expect(room.mHeroes, heroes);
|
||||
|
||||
jsonObj["topic"] = "";
|
||||
room = await Room.getRoomFromTableRow(jsonObj, matrix);
|
||||
expect(room.displayname, "testroom");
|
||||
jsonObj["canonical_alias"] = "";
|
||||
room = await Room.getRoomFromTableRow(jsonObj, matrix);
|
||||
expect(room.displayname, "alice, bob, charley");
|
||||
|
||||
room.states["m.room.canonical_alias"] = RoomState(
|
||||
senderId: "@test:example.com",
|
||||
typeKey: "m.room.canonical_alias",
|
||||
roomId: room.id,
|
||||
room: room,
|
||||
eventId: "123",
|
||||
content: {"alias": "#testalias:example.com"},
|
||||
stateKey: "");
|
||||
expect(room.displayname, "testalias");
|
||||
expect(room.canonicalAlias, "#testalias:example.com");
|
||||
|
||||
room.states["m.room.name"] = RoomState(
|
||||
senderId: "@test:example.com",
|
||||
typeKey: "m.room.name",
|
||||
roomId: room.id,
|
||||
room: room,
|
||||
eventId: "123",
|
||||
content: {"name": "testname"},
|
||||
stateKey: "");
|
||||
expect(room.displayname, "testname");
|
||||
|
||||
expect(room.topic, "");
|
||||
room.states["m.room.topic"] = RoomState(
|
||||
senderId: "@test:example.com",
|
||||
typeKey: "m.room.topic",
|
||||
roomId: room.id,
|
||||
room: room,
|
||||
eventId: "123",
|
||||
content: {"topic": "testtopic"},
|
||||
stateKey: "");
|
||||
expect(room.topic, "testtopic");
|
||||
|
||||
expect(room.avatar.mxc, "");
|
||||
room.states["m.room.avatar"] = RoomState(
|
||||
senderId: "@test:example.com",
|
||||
typeKey: "m.room.avatar",
|
||||
roomId: room.id,
|
||||
room: room,
|
||||
eventId: "123",
|
||||
content: {"url": "mxc://testurl"},
|
||||
stateKey: "");
|
||||
expect(room.avatar.mxc, "mxc://testurl");
|
||||
|
||||
expect(room.lastEvent, null);
|
||||
room.states["m.room.message"] = RoomState(
|
||||
senderId: "@test:example.com",
|
||||
typeKey: "m.room.message",
|
||||
roomId: room.id,
|
||||
room: room,
|
||||
eventId: "12345",
|
||||
time: ChatTime.now(),
|
||||
content: {"msgtype": "m.text", "body": "test"},
|
||||
stateKey: "");
|
||||
expect(room.lastEvent.eventId, "12345");
|
||||
expect(room.lastMessage, "test");
|
||||
expect(room.timeCreated, room.lastEvent.time);
|
||||
});
|
||||
|
||||
test("sendReadReceipt", () async {
|
||||
|
@ -167,7 +162,98 @@ void main() {
|
|||
|
||||
test("getEventByID", () async {
|
||||
final Event event = await room.getEventById("1234");
|
||||
expect(event.id, "143273582443PhrSn:example.org");
|
||||
expect(event.eventId, "143273582443PhrSn:example.org");
|
||||
});
|
||||
|
||||
test("setName", () async {
|
||||
final dynamic resp = await room.setName("Testname");
|
||||
expect(resp["event_id"], "42");
|
||||
});
|
||||
|
||||
test("setDescription", () async {
|
||||
final dynamic resp = await room.setDescription("Testname");
|
||||
expect(resp["event_id"], "42");
|
||||
});
|
||||
|
||||
test("kick", () async {
|
||||
final dynamic resp = await room.kick("Testname");
|
||||
expect(resp, {});
|
||||
});
|
||||
|
||||
test("ban", () async {
|
||||
final dynamic resp = await room.ban("Testname");
|
||||
expect(resp, {});
|
||||
});
|
||||
|
||||
test("unban", () async {
|
||||
final dynamic resp = await room.unban("Testname");
|
||||
expect(resp, {});
|
||||
});
|
||||
|
||||
test("PowerLevels", () async {
|
||||
room.states["m.room.power_levels"] = RoomState(
|
||||
senderId: "@test:example.com",
|
||||
typeKey: "m.room.power_levels",
|
||||
roomId: room.id,
|
||||
room: room,
|
||||
eventId: "123",
|
||||
content: {
|
||||
"ban": 50,
|
||||
"events": {"m.room.name": 100, "m.room.power_levels": 100},
|
||||
"events_default": 0,
|
||||
"invite": 50,
|
||||
"kick": 50,
|
||||
"notifications": {"room": 20},
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {"@test:fakeServer.notExisting": 100},
|
||||
"users_default": 10
|
||||
},
|
||||
stateKey: "");
|
||||
expect(room.ownPowerLevel, 100);
|
||||
expect(room.getPowerLevelByUserId(matrix.userID), room.ownPowerLevel);
|
||||
expect(room.getPowerLevelByUserId("@nouser:example.com"), 10);
|
||||
expect(room.powerLevels,
|
||||
room.states["m.room.power_levels"].content["users"]);
|
||||
final dynamic resp =
|
||||
await room.setPower("@test:fakeServer.notExisting", 90);
|
||||
expect(resp["event_id"], "42");
|
||||
});
|
||||
|
||||
test("invite", () async {
|
||||
final dynamic resp = await room.invite("Testname");
|
||||
expect(resp, {});
|
||||
});
|
||||
|
||||
test("getParticipants", () async {
|
||||
room.states["@alice:test.abc"] = RoomState(
|
||||
senderId: "@alice:test.abc",
|
||||
typeKey: "m.room.member",
|
||||
roomId: room.id,
|
||||
room: room,
|
||||
eventId: "12345",
|
||||
time: ChatTime.now(),
|
||||
content: {"displayname": "alice"},
|
||||
stateKey: "@alice:test.abc");
|
||||
final List<User> userList = room.getParticipants();
|
||||
expect(userList.length, 1);
|
||||
expect(userList[0].displayName, "alice");
|
||||
});
|
||||
|
||||
test("addToDirectChat", () async {
|
||||
final dynamic resp = await room.addToDirectChat("Testname");
|
||||
expect(resp, {});
|
||||
});
|
||||
|
||||
test("getTimeline", () async {
|
||||
final Timeline timeline = await room.getTimeline();
|
||||
expect(timeline.events, []);
|
||||
});
|
||||
|
||||
test("getUserByMXID", () async {
|
||||
final User user = await room.getUserByMXID("@getme:example.com");
|
||||
expect(user.stateKey, "@getme:example.com");
|
||||
expect(user.calcDisplayname(), "You got me");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -41,7 +41,12 @@ void main() {
|
|||
client.connection.httpClient = FakeMatrixApi();
|
||||
client.homeserver = "https://fakeServer.notExisting";
|
||||
|
||||
Room room = Room(id: roomID, client: client, prev_batch: "1234");
|
||||
Room room = Room(
|
||||
id: roomID,
|
||||
client: client,
|
||||
prev_batch: "1234",
|
||||
states: {},
|
||||
roomAccountData: {});
|
||||
Timeline timeline = Timeline(
|
||||
room: room,
|
||||
events: [],
|
||||
|
@ -87,10 +92,9 @@ void main() {
|
|||
expect(insertList, [0, 0]);
|
||||
expect(insertList.length, timeline.events.length);
|
||||
expect(timeline.events.length, 2);
|
||||
expect(timeline.events[0].id, "1");
|
||||
expect(timeline.events[0].eventId, "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);
|
||||
});
|
||||
|
@ -103,7 +107,7 @@ void main() {
|
|||
expect(updateCount, 4);
|
||||
expect(insertList, [0, 0, 0]);
|
||||
expect(insertList.length, timeline.events.length);
|
||||
expect(timeline.events[0].id, "42");
|
||||
expect(timeline.events[0].eventId, "42");
|
||||
expect(timeline.events[0].status, 1);
|
||||
|
||||
client.connection.onEvent.add(EventUpdate(
|
||||
|
@ -125,7 +129,7 @@ void main() {
|
|||
expect(updateCount, 5);
|
||||
expect(insertList, [0, 0, 0]);
|
||||
expect(insertList.length, timeline.events.length);
|
||||
expect(timeline.events[0].id, "42");
|
||||
expect(timeline.events[0].eventId, "42");
|
||||
expect(timeline.events[0].status, 2);
|
||||
});
|
||||
|
||||
|
@ -189,9 +193,9 @@ void main() {
|
|||
|
||||
expect(updateCount, 19);
|
||||
expect(timeline.events.length, 9);
|
||||
expect(timeline.events[6].id, "1143273582443PhrSn:example.org");
|
||||
expect(timeline.events[7].id, "2143273582443PhrSn:example.org");
|
||||
expect(timeline.events[8].id, "3143273582443PhrSn:example.org");
|
||||
expect(timeline.events[6].eventId, "1143273582443PhrSn:example.org");
|
||||
expect(timeline.events[7].eventId, "2143273582443PhrSn:example.org");
|
||||
expect(timeline.events[8].eventId, "3143273582443PhrSn:example.org");
|
||||
expect(room.prev_batch, "t47409-4357353_219380_26003_2265");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/src/RoomState.dart';
|
||||
import 'package:famedlysdk/src/User.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
|
@ -32,30 +33,35 @@ void main() {
|
|||
final Membership membership = Membership.join;
|
||||
final String displayName = "Alice";
|
||||
final String avatarUrl = "";
|
||||
final int powerLevel = 50;
|
||||
|
||||
final Map<String, dynamic> jsonObj = {
|
||||
"matrix_id": id,
|
||||
"displayname": displayName,
|
||||
"avatar_url": avatarUrl,
|
||||
"membership": membership.toString().split('.').last,
|
||||
"power_level": powerLevel,
|
||||
"content": {
|
||||
"membership": "join",
|
||||
"avatar_url": avatarUrl,
|
||||
"displayname": displayName
|
||||
},
|
||||
"type": "m.room.member",
|
||||
"event_id": "143273582443PhrSn:example.org",
|
||||
"room_id": "!636q39766251:example.com",
|
||||
"sender": id,
|
||||
"origin_server_ts": 1432735824653,
|
||||
"unsigned": {"age": 1234},
|
||||
"state_key": id
|
||||
};
|
||||
|
||||
User user = User.fromJson(jsonObj, null);
|
||||
User user = RoomState.fromJson(jsonObj, null).asUser;
|
||||
|
||||
expect(user.id, id);
|
||||
expect(user.membership, membership);
|
||||
expect(user.displayName, displayName);
|
||||
expect(user.avatarUrl.mxc, avatarUrl);
|
||||
expect(user.powerLevel, powerLevel);
|
||||
expect(user.calcDisplayname(), displayName);
|
||||
});
|
||||
|
||||
test("calcDisplayname", () async {
|
||||
final User user1 = User("@alice:example.com");
|
||||
final User user2 = User("@alice:example.com", displayName: "SuperAlice");
|
||||
final User user3 = User("@alice:example.com", displayName: "");
|
||||
final User user2 = User("@SuperAlice:example.com");
|
||||
final User user3 = User("@alice:example.com");
|
||||
expect(user1.calcDisplayname(), "alice");
|
||||
expect(user2.calcDisplayname(), "SuperAlice");
|
||||
expect(user3.calcDisplayname(), "alice");
|
||||
|
|
Loading…
Reference in a new issue