2019-06-09 11:57:33 +00:00
|
|
|
|
/*
|
|
|
|
|
* 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
|
2019-06-21 07:46:53 +00:00
|
|
|
|
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
|
2019-06-09 11:57:33 +00:00
|
|
|
|
*/
|
|
|
|
|
|
2019-06-09 10:16:48 +00:00
|
|
|
|
import 'package:famedlysdk/src/Client.dart';
|
2019-07-12 09:26:07 +00:00
|
|
|
|
import 'package:famedlysdk/src/Event.dart';
|
2019-08-07 08:32:18 +00:00
|
|
|
|
import 'package:famedlysdk/src/RoomAccountData.dart';
|
2019-08-08 10:51:07 +00:00
|
|
|
|
import 'package:famedlysdk/src/RoomState.dart';
|
2019-06-11 11:44:25 +00:00
|
|
|
|
import 'package:famedlysdk/src/sync/EventUpdate.dart';
|
2019-10-24 09:39:39 +00:00
|
|
|
|
import 'package:famedlysdk/src/sync/RoomUpdate.dart';
|
2019-07-12 09:26:07 +00:00
|
|
|
|
import 'package:famedlysdk/src/utils/ChatTime.dart';
|
2019-12-29 10:28:33 +00:00
|
|
|
|
import 'package:famedlysdk/src/utils/MatrixException.dart';
|
2019-10-18 11:05:07 +00:00
|
|
|
|
import 'package:famedlysdk/src/utils/MatrixFile.dart';
|
2019-07-12 09:26:07 +00:00
|
|
|
|
import 'package:famedlysdk/src/utils/MxContent.dart';
|
2019-10-02 11:33:01 +00:00
|
|
|
|
//import 'package:image/image.dart';
|
2019-09-09 13:22:02 +00:00
|
|
|
|
import 'package:mime_type/mime_type.dart';
|
2019-07-12 09:26:07 +00:00
|
|
|
|
|
2019-06-09 11:57:33 +00:00
|
|
|
|
import './User.dart';
|
2019-07-12 09:26:07 +00:00
|
|
|
|
import 'Connection.dart';
|
2019-06-21 10:18:54 +00:00
|
|
|
|
import 'Timeline.dart';
|
2019-11-20 13:02:23 +00:00
|
|
|
|
import 'utils/StatesMap.dart';
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
2019-09-03 11:24:44 +00:00
|
|
|
|
typedef onRoomUpdate = void Function();
|
|
|
|
|
|
2019-06-09 12:33:25 +00:00
|
|
|
|
/// Represents a Matrix room.
|
2019-06-09 10:16:48 +00:00
|
|
|
|
class Room {
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// The full qualified Matrix ID for the room in the format '!localid:server.abc'.
|
|
|
|
|
final String id;
|
|
|
|
|
|
|
|
|
|
/// Membership status of the user for this room.
|
2019-07-12 09:26:07 +00:00
|
|
|
|
Membership membership;
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
|
|
|
|
/// The count of unread notifications.
|
2019-06-09 10:16:48 +00:00
|
|
|
|
int notificationCount;
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
|
|
|
|
/// The count of highlighted notifications.
|
2019-06-09 10:16:48 +00:00
|
|
|
|
int highlightCount;
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-09-03 14:34:38 +00:00
|
|
|
|
/// A token that can be supplied to the from parameter of the rooms/{roomId}/messages endpoint.
|
2019-06-11 08:51:45 +00:00
|
|
|
|
String prev_batch;
|
|
|
|
|
|
2019-09-03 14:34:38 +00:00
|
|
|
|
/// The users which can be used to generate a room name if the room does not have one.
|
|
|
|
|
/// Required if the room's m.room.name or m.room.canonical_alias state events are unset or empty.
|
2019-08-08 12:31:47 +00:00
|
|
|
|
List<String> mHeroes = [];
|
2019-09-03 14:34:38 +00:00
|
|
|
|
|
|
|
|
|
/// The number of users with membership of join, including the client's own user ID.
|
2019-08-07 08:17:03 +00:00
|
|
|
|
int mJoinedMemberCount;
|
2019-09-03 14:34:38 +00:00
|
|
|
|
|
|
|
|
|
/// The number of users with membership of invite.
|
2019-08-07 08:17:03 +00:00
|
|
|
|
int mInvitedMemberCount;
|
|
|
|
|
|
2019-11-20 13:02:23 +00:00
|
|
|
|
StatesMap states = StatesMap();
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-10-20 09:44:14 +00:00
|
|
|
|
/// Key-Value store for ephemerals.
|
|
|
|
|
Map<String, RoomAccountData> ephemerals = {};
|
|
|
|
|
|
2019-09-03 14:34:38 +00:00
|
|
|
|
/// Key-Value store for private account data only visible for this user.
|
2019-08-07 10:27:02 +00:00
|
|
|
|
Map<String, RoomAccountData> roomAccountData = {};
|
2019-08-07 08:32:18 +00:00
|
|
|
|
|
2019-11-20 13:42:08 +00:00
|
|
|
|
/// Returns the [RoomState] for the given [typeKey] and optional [stateKey].
|
|
|
|
|
/// If no [stateKey] is provided, it defaults to an empty string.
|
|
|
|
|
RoomState getState(String typeKey, [String stateKey = ""]) =>
|
|
|
|
|
states.states[typeKey] != null ? states.states[typeKey][stateKey] : null;
|
|
|
|
|
|
2019-11-21 14:10:24 +00:00
|
|
|
|
/// Adds the [state] to this room and overwrites a state with the same
|
|
|
|
|
/// typeKey/stateKey key pair if there is one.
|
|
|
|
|
void setState(RoomState state) {
|
|
|
|
|
if (!states.states.containsKey(state.typeKey))
|
|
|
|
|
states.states[state.typeKey] = {};
|
|
|
|
|
states.states[state.typeKey][state.stateKey ?? ""] = state;
|
2019-11-20 13:42:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// ID of the fully read marker event.
|
2019-09-03 15:57:27 +00:00
|
|
|
|
String get fullyRead => roomAccountData["m.fully_read"] != null
|
|
|
|
|
? roomAccountData["m.fully_read"].content["event_id"]
|
|
|
|
|
: "";
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-09-03 11:24:44 +00:00
|
|
|
|
/// If something changes, this callback will be triggered.
|
|
|
|
|
onRoomUpdate onUpdate;
|
|
|
|
|
|
2019-08-07 08:17:03 +00:00
|
|
|
|
/// The name of the room if set by a participant.
|
2019-11-26 12:46:46 +00:00
|
|
|
|
String get name => states["m.room.name"] != null
|
|
|
|
|
? states["m.room.name"].content["name"]
|
|
|
|
|
: "";
|
2019-08-07 08:17:03 +00:00
|
|
|
|
|
|
|
|
|
/// 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)
|
2019-08-08 09:41:42 +00:00
|
|
|
|
return MxContent(states["m.room.avatar"].content["url"]);
|
2019-08-28 10:32:50 +00:00
|
|
|
|
if (mHeroes != null && mHeroes.length == 1 && states[mHeroes[0]] != null)
|
2019-08-08 09:41:42 +00:00
|
|
|
|
return states[mHeroes[0]].asUser.avatarUrl;
|
2019-11-26 12:46:46 +00:00
|
|
|
|
if (membership == Membership.invite &&
|
|
|
|
|
getState("m.room.member", client.userID) != null) {
|
|
|
|
|
return getState("m.room.member", client.userID).sender.avatarUrl;
|
2019-09-30 08:19:28 +00:00
|
|
|
|
}
|
2019-08-07 08:17:03 +00:00
|
|
|
|
return MxContent("");
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// The address in the format: #roomname:homeserver.org.
|
2019-08-07 08:17:03 +00:00
|
|
|
|
String get canonicalAlias => states["m.room.canonical_alias"] != null
|
2019-08-08 07:58:37 +00:00
|
|
|
|
? states["m.room.canonical_alias"].content["alias"]
|
2019-08-07 08:17:03 +00:00
|
|
|
|
: "";
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-08-08 08:31:39 +00:00
|
|
|
|
/// 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;
|
|
|
|
|
}
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-08-29 08:49:07 +00:00
|
|
|
|
/// Wheither this is a direct chat or not
|
|
|
|
|
bool get isDirectChat => directChatMatrixID != null;
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Must be one of [all, mention]
|
|
|
|
|
String notificationSettings;
|
|
|
|
|
|
2019-08-29 07:50:04 +00:00
|
|
|
|
Event get lastEvent {
|
|
|
|
|
ChatTime lastTime = ChatTime(0);
|
2019-11-21 14:10:24 +00:00
|
|
|
|
Event lastEvent = getState("m.room.message")?.timelineEvent;
|
2019-11-20 13:02:23 +00:00
|
|
|
|
if (lastEvent == null)
|
|
|
|
|
states.forEach((final String key, final entry) {
|
|
|
|
|
if (!entry.containsKey("")) return;
|
|
|
|
|
final RoomState state = entry[""];
|
|
|
|
|
if (state.time != null && state.time > lastTime) {
|
|
|
|
|
lastTime = state.time;
|
|
|
|
|
lastEvent = state.timelineEvent;
|
2019-11-13 14:08:27 +00:00
|
|
|
|
}
|
2019-11-20 13:02:23 +00:00
|
|
|
|
});
|
2019-08-29 07:50:04 +00:00
|
|
|
|
return lastEvent;
|
|
|
|
|
}
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-10-20 09:44:14 +00:00
|
|
|
|
/// Returns a list of all current typing users.
|
|
|
|
|
List<User> get typingUsers {
|
|
|
|
|
if (!ephemerals.containsKey("m.typing")) return [];
|
|
|
|
|
List<dynamic> typingMxid = ephemerals["m.typing"].content["user_ids"];
|
|
|
|
|
List<User> typingUsers = [];
|
|
|
|
|
for (int i = 0; i < typingMxid.length; i++)
|
2019-11-15 11:08:43 +00:00
|
|
|
|
typingUsers.add(getUserByMXIDSync(typingMxid[i]));
|
2019-10-20 09:44:14 +00:00
|
|
|
|
return typingUsers;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Your current client instance.
|
|
|
|
|
final Client client;
|
|
|
|
|
|
2019-06-09 10:16:48 +00:00
|
|
|
|
Room({
|
2019-06-11 08:51:45 +00:00
|
|
|
|
this.id,
|
2019-08-08 11:00:56 +00:00
|
|
|
|
this.membership = Membership.join,
|
|
|
|
|
this.notificationCount = 0,
|
|
|
|
|
this.highlightCount = 0,
|
2019-06-28 09:42:57 +00:00
|
|
|
|
this.prev_batch = "",
|
2019-06-11 08:51:45 +00:00
|
|
|
|
this.client,
|
2019-08-07 08:17:03 +00:00
|
|
|
|
this.notificationSettings,
|
2019-08-08 11:00:56 +00:00
|
|
|
|
this.mHeroes = const [],
|
|
|
|
|
this.mInvitedMemberCount = 0,
|
|
|
|
|
this.mJoinedMemberCount = 0,
|
|
|
|
|
this.roomAccountData = const {},
|
2019-06-09 10:16:48 +00:00
|
|
|
|
});
|
|
|
|
|
|
2019-09-26 09:30:07 +00:00
|
|
|
|
/// The default count of how much events should be requested when requesting the
|
|
|
|
|
/// history of this room.
|
|
|
|
|
static const int DefaultHistoryCount = 100;
|
|
|
|
|
|
2019-08-06 09:47:09 +00:00
|
|
|
|
/// Calculates the displayname. First checks if there is a name, then checks for a canonical alias and
|
|
|
|
|
/// then generates a name from the heroes.
|
|
|
|
|
String get displayname {
|
2019-11-26 12:46:46 +00:00
|
|
|
|
if (name != null && name.isNotEmpty) return name;
|
2019-08-06 09:47:09 +00:00
|
|
|
|
if (canonicalAlias != null &&
|
|
|
|
|
!canonicalAlias.isEmpty &&
|
|
|
|
|
canonicalAlias.length > 3)
|
|
|
|
|
return canonicalAlias.substring(1, canonicalAlias.length).split(":")[0];
|
2019-11-29 16:19:32 +00:00
|
|
|
|
List<String> heroes = [];
|
|
|
|
|
if (mHeroes != null &&
|
|
|
|
|
mHeroes.length > 0 &&
|
|
|
|
|
mHeroes.any((h) => h.isNotEmpty)) {
|
|
|
|
|
heroes = mHeroes;
|
|
|
|
|
} else {
|
|
|
|
|
if (states["m.room.member"] is Map<String, dynamic>) {
|
|
|
|
|
for (var entry in states["m.room.member"].entries) {
|
|
|
|
|
RoomState state = entry.value;
|
|
|
|
|
if (state.type == EventTypes.RoomMember &&
|
|
|
|
|
state.stateKey != client?.userID) heroes.add(state.stateKey);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (heroes.length > 0) {
|
2019-08-06 09:47:09 +00:00
|
|
|
|
String displayname = "";
|
2019-11-29 16:19:32 +00:00
|
|
|
|
for (int i = 0; i < heroes.length; i++) {
|
|
|
|
|
if (heroes[i].isEmpty) continue;
|
2019-12-05 16:42:12 +00:00
|
|
|
|
displayname += getUserByMXIDSync(heroes[i]).calcDisplayname() + ", ";
|
2019-11-26 12:46:46 +00:00
|
|
|
|
}
|
2019-08-06 09:47:09 +00:00
|
|
|
|
return displayname.substring(0, displayname.length - 2);
|
|
|
|
|
}
|
2019-11-26 12:46:46 +00:00
|
|
|
|
if (membership == Membership.invite &&
|
|
|
|
|
getState("m.room.member", client.userID) != null) {
|
|
|
|
|
return getState("m.room.member", client.userID).sender.calcDisplayname();
|
|
|
|
|
}
|
2019-08-06 09:47:09 +00:00
|
|
|
|
return "Empty chat";
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// The last message sent to this room.
|
|
|
|
|
String get lastMessage {
|
2019-06-21 10:18:54 +00:00
|
|
|
|
if (lastEvent != null)
|
|
|
|
|
return lastEvent.getBody();
|
2019-06-11 11:44:25 +00:00
|
|
|
|
else
|
|
|
|
|
return "";
|
2019-06-11 08:51:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// When the last message received.
|
|
|
|
|
ChatTime get timeCreated {
|
2019-06-21 10:18:54 +00:00
|
|
|
|
if (lastEvent != null)
|
|
|
|
|
return lastEvent.time;
|
2019-06-11 11:44:25 +00:00
|
|
|
|
else
|
|
|
|
|
return ChatTime.now();
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
/// Call the Matrix API to change the name of this room. Returns the event ID of the
|
|
|
|
|
/// new m.room.name event.
|
|
|
|
|
Future<String> setName(String newName) async {
|
|
|
|
|
final Map<String, dynamic> resp = await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2019-07-26 11:32:18 +00:00
|
|
|
|
action: "/client/r0/rooms/${id}/state/m.room.name",
|
2019-06-09 10:16:48 +00:00
|
|
|
|
data: {"name": newName});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return resp["event_id"];
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to change the topic of this room.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<String> setDescription(String newName) async {
|
|
|
|
|
final Map<String, dynamic> resp = await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2019-07-26 11:32:18 +00:00
|
|
|
|
action: "/client/r0/rooms/${id}/state/m.room.topic",
|
2019-06-09 10:16:48 +00:00
|
|
|
|
data: {"topic": newName});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return resp["event_id"];
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<String> _sendRawEventNow(Map<String, dynamic> content,
|
2019-09-09 13:22:02 +00:00
|
|
|
|
{String txid = null}) async {
|
2019-06-11 15:16:01 +00:00
|
|
|
|
if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}";
|
2019-12-29 10:28:33 +00:00
|
|
|
|
final Map<String, dynamic> res = await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2019-06-12 09:46:57 +00:00
|
|
|
|
action: "/client/r0/rooms/${id}/send/m.room.message/$txid",
|
2019-09-09 13:22:02 +00:00
|
|
|
|
data: content);
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return res["event_id"];
|
2019-06-11 15:16:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-09 13:22:02 +00:00
|
|
|
|
Future<String> sendTextEvent(String message, {String txid = null}) =>
|
|
|
|
|
sendEvent({"msgtype": "m.text", "body": message}, txid: txid);
|
|
|
|
|
|
2019-12-18 11:46:25 +00:00
|
|
|
|
/// Sends a [file] to this room after uploading it. The [msgType] is optional
|
|
|
|
|
/// and will be detected by the mimetype of the file.
|
|
|
|
|
Future<String> sendFileEvent(MatrixFile file,
|
|
|
|
|
{String msgType = "m.file", String txid = null}) async {
|
|
|
|
|
if (msgType == "m.image") return sendImageEvent(file);
|
|
|
|
|
if (msgType == "m.audio") return sendVideoEvent(file);
|
|
|
|
|
if (msgType == "m.video") return sendAudioEvent(file);
|
2019-10-02 11:33:01 +00:00
|
|
|
|
String fileName = file.path.split("/").last;
|
2019-09-09 13:22:02 +00:00
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
final String uploadResp = await client.connection.upload(file);
|
2019-09-09 13:22:02 +00:00
|
|
|
|
|
|
|
|
|
// Send event
|
|
|
|
|
Map<String, dynamic> content = {
|
|
|
|
|
"msgtype": msgType,
|
|
|
|
|
"body": fileName,
|
|
|
|
|
"filename": fileName,
|
|
|
|
|
"url": uploadResp,
|
|
|
|
|
"info": {
|
2019-12-18 11:46:25 +00:00
|
|
|
|
"mimetype": mime(file.path),
|
|
|
|
|
"size": file.size,
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
return await sendEvent(content, txid: txid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<String> sendAudioEvent(MatrixFile file,
|
|
|
|
|
{String txid = null, int width, int height}) async {
|
|
|
|
|
String fileName = file.path.split("/").last;
|
2019-12-29 10:28:33 +00:00
|
|
|
|
final String uploadResp = await client.connection.upload(file);
|
2019-12-18 11:46:25 +00:00
|
|
|
|
Map<String, dynamic> content = {
|
|
|
|
|
"msgtype": "m.audio",
|
|
|
|
|
"body": fileName,
|
|
|
|
|
"filename": fileName,
|
|
|
|
|
"url": uploadResp,
|
|
|
|
|
"info": {
|
|
|
|
|
"mimetype": mime(fileName),
|
|
|
|
|
"size": file.size,
|
2019-09-09 13:22:02 +00:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
return await sendEvent(content, txid: txid);
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-18 11:05:07 +00:00
|
|
|
|
Future<String> sendImageEvent(MatrixFile file,
|
2019-10-02 11:33:01 +00:00
|
|
|
|
{String txid = null, int width, int height}) async {
|
|
|
|
|
String fileName = file.path.split("/").last;
|
2019-12-29 10:28:33 +00:00
|
|
|
|
final String uploadResp = await client.connection.upload(file);
|
2019-09-09 13:22:02 +00:00
|
|
|
|
Map<String, dynamic> content = {
|
|
|
|
|
"msgtype": "m.image",
|
|
|
|
|
"body": fileName,
|
|
|
|
|
"url": uploadResp,
|
2019-09-30 12:03:34 +00:00
|
|
|
|
"info": {
|
2019-10-18 11:05:07 +00:00
|
|
|
|
"size": file.size,
|
2019-10-02 11:33:01 +00:00
|
|
|
|
"mimetype": mime(fileName),
|
|
|
|
|
"w": width,
|
|
|
|
|
"h": height,
|
2019-09-30 12:03:34 +00:00
|
|
|
|
},
|
2019-09-09 13:22:02 +00:00
|
|
|
|
};
|
|
|
|
|
return await sendEvent(content, txid: txid);
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-18 11:46:25 +00:00
|
|
|
|
Future<String> sendVideoEvent(MatrixFile file,
|
|
|
|
|
{String txid = null,
|
|
|
|
|
int videoWidth,
|
|
|
|
|
int videoHeight,
|
|
|
|
|
int duration,
|
|
|
|
|
MatrixFile thumbnail,
|
|
|
|
|
int thumbnailWidth,
|
|
|
|
|
int thumbnailHeight}) async {
|
|
|
|
|
String fileName = file.path.split("/").last;
|
2019-12-29 10:28:33 +00:00
|
|
|
|
final String uploadResp = await client.connection.upload(file);
|
2019-12-18 11:46:25 +00:00
|
|
|
|
Map<String, dynamic> content = {
|
|
|
|
|
"msgtype": "m.video",
|
|
|
|
|
"body": fileName,
|
|
|
|
|
"url": uploadResp,
|
|
|
|
|
"info": {
|
|
|
|
|
"size": file.size,
|
|
|
|
|
"mimetype": mime(fileName),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
if (videoWidth != null) {
|
|
|
|
|
content["info"]["w"] = videoWidth;
|
|
|
|
|
}
|
|
|
|
|
if (thumbnailHeight != null) {
|
|
|
|
|
content["info"]["h"] = thumbnailHeight;
|
|
|
|
|
}
|
|
|
|
|
if (duration != null) {
|
|
|
|
|
content["info"]["duration"] = duration;
|
|
|
|
|
}
|
|
|
|
|
if (thumbnail != null) {
|
|
|
|
|
String thumbnailName = file.path.split("/").last;
|
2019-12-29 10:28:33 +00:00
|
|
|
|
final String thumbnailUploadResp = await client.connection.upload(file);
|
2019-12-18 11:46:25 +00:00
|
|
|
|
content["info"]["thumbnail_url"] = thumbnailUploadResp;
|
|
|
|
|
content["info"]["thumbnail_info"] = {
|
|
|
|
|
"size": thumbnail.size,
|
|
|
|
|
"mimetype": mime(thumbnailName),
|
|
|
|
|
};
|
|
|
|
|
if (thumbnailWidth != null) {
|
|
|
|
|
content["info"]["thumbnail_info"]["w"] = thumbnailWidth;
|
|
|
|
|
}
|
|
|
|
|
if (thumbnailHeight != null) {
|
|
|
|
|
content["info"]["thumbnail_info"]["h"] = thumbnailHeight;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return await sendEvent(content, txid: txid);
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-09 13:22:02 +00:00
|
|
|
|
Future<String> sendEvent(Map<String, dynamic> content,
|
|
|
|
|
{String txid = null}) async {
|
2019-06-11 15:16:01 +00:00
|
|
|
|
final String type = "m.room.message";
|
2019-06-27 07:25:25 +00:00
|
|
|
|
|
|
|
|
|
// Create new transaction id
|
2019-06-26 14:36:34 +00:00
|
|
|
|
String messageID;
|
2019-06-11 15:16:01 +00:00
|
|
|
|
final int now = DateTime.now().millisecondsSinceEpoch;
|
2019-06-26 14:36:34 +00:00
|
|
|
|
if (txid == null) {
|
|
|
|
|
messageID = "msg$now";
|
|
|
|
|
} else
|
|
|
|
|
messageID = txid;
|
2019-06-11 15:16:01 +00:00
|
|
|
|
|
2019-06-26 14:36:34 +00:00
|
|
|
|
// Display a *sending* event and store it.
|
2019-06-12 09:46:57 +00:00
|
|
|
|
EventUpdate eventUpdate =
|
2019-06-18 10:33:40 +00:00
|
|
|
|
EventUpdate(type: "timeline", roomID: id, eventType: type, content: {
|
2019-06-12 09:46:57 +00:00
|
|
|
|
"type": type,
|
2019-06-27 07:44:37 +00:00
|
|
|
|
"event_id": messageID,
|
2019-06-12 09:46:57 +00:00
|
|
|
|
"sender": client.userID,
|
|
|
|
|
"status": 0,
|
|
|
|
|
"origin_server_ts": now,
|
2019-09-09 13:22:02 +00:00
|
|
|
|
"content": content
|
2019-06-12 09:46:57 +00:00
|
|
|
|
});
|
2019-06-11 15:16:01 +00:00
|
|
|
|
client.connection.onEvent.add(eventUpdate);
|
2019-06-26 14:36:34 +00:00
|
|
|
|
await client.store?.transaction(() {
|
2019-06-11 15:16:01 +00:00
|
|
|
|
client.store.storeEventUpdate(eventUpdate);
|
2019-07-12 09:26:07 +00:00
|
|
|
|
return;
|
2019-06-11 15:16:01 +00:00
|
|
|
|
});
|
2019-06-26 14:36:34 +00:00
|
|
|
|
|
|
|
|
|
// Send the text and on success, store and display a *sent* event.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
try {
|
|
|
|
|
final String res = await _sendRawEventNow(content, txid: messageID);
|
|
|
|
|
eventUpdate.content["status"] = 1;
|
2019-07-23 09:09:13 +00:00
|
|
|
|
eventUpdate.content["unsigned"] = {"transaction_id": messageID};
|
2019-12-29 10:28:33 +00:00
|
|
|
|
eventUpdate.content["event_id"] = res;
|
2019-06-26 14:36:34 +00:00
|
|
|
|
client.connection.onEvent.add(eventUpdate);
|
2019-06-26 18:03:20 +00:00
|
|
|
|
await client.store?.transaction(() {
|
|
|
|
|
client.store.storeEventUpdate(eventUpdate);
|
2019-07-12 09:26:07 +00:00
|
|
|
|
return;
|
2019-06-26 18:03:20 +00:00
|
|
|
|
});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return res;
|
|
|
|
|
} catch (exception) {
|
|
|
|
|
// On error, set status to -1
|
|
|
|
|
eventUpdate.content["status"] = -1;
|
2019-07-23 09:09:13 +00:00
|
|
|
|
eventUpdate.content["unsigned"] = {"transaction_id": messageID};
|
2019-06-26 14:36:34 +00:00
|
|
|
|
client.connection.onEvent.add(eventUpdate);
|
2019-06-26 18:03:20 +00:00
|
|
|
|
await client.store?.transaction(() {
|
|
|
|
|
client.store.storeEventUpdate(eventUpdate);
|
2019-07-12 09:26:07 +00:00
|
|
|
|
return;
|
2019-06-26 18:03:20 +00:00
|
|
|
|
});
|
2019-06-11 15:16:01 +00:00
|
|
|
|
}
|
|
|
|
|
return null;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-30 08:19:28 +00:00
|
|
|
|
/// Call the Matrix API to join this room if the user is not already a member.
|
|
|
|
|
/// If this room is intended to be a direct chat, the direct chat flag will
|
|
|
|
|
/// automatically be set.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> join() async {
|
|
|
|
|
try {
|
|
|
|
|
await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.POST, action: "/client/r0/rooms/${id}/join");
|
|
|
|
|
if (states.containsKey(client.userID) &&
|
|
|
|
|
states[client.userID].content["is_direct"] is bool &&
|
|
|
|
|
states[client.userID].content["is_direct"])
|
|
|
|
|
addToDirectChat(states[client.userID].sender.id);
|
|
|
|
|
} on MatrixException catch (exception) {
|
|
|
|
|
if (exception.errorMessage == "No known servers") {
|
2019-11-13 13:56:20 +00:00
|
|
|
|
client.store?.forgetRoom(id);
|
|
|
|
|
client.connection.onRoomUpdate.add(
|
|
|
|
|
RoomUpdate(
|
|
|
|
|
id: id,
|
|
|
|
|
membership: Membership.leave,
|
|
|
|
|
notification_count: 0,
|
|
|
|
|
highlight_count: 0),
|
|
|
|
|
);
|
|
|
|
|
}
|
2019-12-29 10:28:33 +00:00
|
|
|
|
rethrow;
|
2019-11-13 13:56:20 +00:00
|
|
|
|
}
|
2019-09-30 08:19:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Call the Matrix API to leave this room. If this room is set as a direct
|
|
|
|
|
/// chat, this will be removed too.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> leave() async {
|
2019-09-30 08:19:28 +00:00
|
|
|
|
if (directChatMatrixID != "") await removeFromDirectChat();
|
2019-12-29 10:28:33 +00:00
|
|
|
|
await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST, action: "/client/r0/rooms/${id}/leave");
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to forget this room if you already left it.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> forget() async {
|
2019-06-12 11:22:16 +00:00
|
|
|
|
client.store.forgetRoom(id);
|
2019-12-29 10:28:33 +00:00
|
|
|
|
await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST, action: "/client/r0/rooms/${id}/forget");
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to kick a user from this room.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> kick(String userID) async {
|
|
|
|
|
await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST,
|
2019-06-11 11:44:25 +00:00
|
|
|
|
action: "/client/r0/rooms/${id}/kick",
|
2019-06-09 10:16:48 +00:00
|
|
|
|
data: {"user_id": userID});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to ban a user from this room.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> ban(String userID) async {
|
|
|
|
|
await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST,
|
2019-06-11 11:44:25 +00:00
|
|
|
|
action: "/client/r0/rooms/${id}/ban",
|
2019-06-09 10:16:48 +00:00
|
|
|
|
data: {"user_id": userID});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to unban a banned user from this room.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> unban(String userID) async {
|
|
|
|
|
await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST,
|
2019-06-11 11:44:25 +00:00
|
|
|
|
action: "/client/r0/rooms/${id}/unban",
|
2019-06-09 10:16:48 +00:00
|
|
|
|
data: {"user_id": userID});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-08 09:41:42 +00:00
|
|
|
|
/// Set the power level of the user with the [userID] to the value [power].
|
2019-12-29 10:28:33 +00:00
|
|
|
|
/// Returns the event ID of the new state event. If there is no known
|
|
|
|
|
/// power level event, there might something broken and this returns null.
|
|
|
|
|
Future<String> setPower(String userID, int power) async {
|
2019-08-08 09:41:42 +00:00
|
|
|
|
if (states["m.room.power_levels"] == null) return null;
|
2019-12-05 09:06:23 +00:00
|
|
|
|
Map<String, dynamic> powerMap = {}
|
|
|
|
|
..addAll(states["m.room.power_levels"].content);
|
|
|
|
|
if (powerMap["users"] == null) powerMap["users"] = {};
|
|
|
|
|
powerMap["users"][userID] = power;
|
2019-06-11 11:32:14 +00:00
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
final Map<String, dynamic> resp = await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2019-07-26 08:05:08 +00:00
|
|
|
|
action: "/client/r0/rooms/$id/state/m.room.power_levels",
|
2019-12-05 09:06:23 +00:00
|
|
|
|
data: powerMap);
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return resp["event_id"];
|
2019-06-11 11:32:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to invite a user to this room.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> invite(String userID) async {
|
|
|
|
|
await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST,
|
2019-06-11 11:44:25 +00:00
|
|
|
|
action: "/client/r0/rooms/${id}/invite",
|
2019-06-09 10:16:48 +00:00
|
|
|
|
data: {"user_id": userID});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-26 09:30:07 +00:00
|
|
|
|
/// Request more previous events from the server. [historyCount] defines how much events should
|
|
|
|
|
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
|
|
|
|
|
/// the historical events will be published in the onEvent stream.
|
|
|
|
|
Future<void> requestHistory(
|
|
|
|
|
{int historyCount = DefaultHistoryCount, onHistoryReceived}) async {
|
2019-06-28 06:39:43 +00:00
|
|
|
|
final dynamic resp = await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.GET,
|
2019-06-28 09:42:57 +00:00
|
|
|
|
action:
|
2019-08-29 10:28:50 +00:00
|
|
|
|
"/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount&filter=${Connection.syncFilters}");
|
2019-06-11 11:44:25 +00:00
|
|
|
|
|
2019-09-26 09:30:07 +00:00
|
|
|
|
if (onHistoryReceived != null) onHistoryReceived();
|
2019-06-28 10:32:33 +00:00
|
|
|
|
prev_batch = resp["end"];
|
|
|
|
|
client.store?.storeRoomPrevBatch(this);
|
|
|
|
|
|
2019-06-12 09:46:57 +00:00
|
|
|
|
if (!(resp["chunk"] is List<dynamic> &&
|
|
|
|
|
resp["chunk"].length > 0 &&
|
2019-06-11 11:44:25 +00:00
|
|
|
|
resp["end"] is String)) return;
|
|
|
|
|
|
2019-08-29 10:28:50 +00:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 11:44:25 +00:00
|
|
|
|
List<dynamic> history = resp["chunk"];
|
2019-06-28 06:39:43 +00:00
|
|
|
|
client.store?.transaction(() {
|
2019-06-11 11:44:25 +00:00
|
|
|
|
for (int i = 0; i < history.length; i++) {
|
|
|
|
|
EventUpdate eventUpdate = EventUpdate(
|
2019-06-28 06:39:43 +00:00
|
|
|
|
type: "history",
|
2019-06-11 11:44:25 +00:00
|
|
|
|
roomID: id,
|
2019-06-28 06:39:43 +00:00
|
|
|
|
eventType: history[i]["type"],
|
2019-06-11 11:44:25 +00:00
|
|
|
|
content: history[i],
|
|
|
|
|
);
|
|
|
|
|
client.connection.onEvent.add(eventUpdate);
|
|
|
|
|
client.store.storeEventUpdate(eventUpdate);
|
|
|
|
|
client.store.txn.rawUpdate(
|
2019-08-29 07:03:05 +00:00
|
|
|
|
"UPDATE Rooms SET prev_batch=? WHERE room_id=?", [resp["end"], id]);
|
2019-06-11 11:44:25 +00:00
|
|
|
|
}
|
2019-07-12 09:26:07 +00:00
|
|
|
|
return;
|
2019-06-11 11:44:25 +00:00
|
|
|
|
});
|
2019-06-28 06:39:43 +00:00
|
|
|
|
if (client.store == null) {
|
|
|
|
|
for (int i = 0; i < history.length; i++) {
|
|
|
|
|
EventUpdate eventUpdate = EventUpdate(
|
|
|
|
|
type: "history",
|
|
|
|
|
roomID: id,
|
|
|
|
|
eventType: history[i]["type"],
|
|
|
|
|
content: history[i],
|
|
|
|
|
);
|
|
|
|
|
client.connection.onEvent.add(eventUpdate);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-10-24 09:39:39 +00:00
|
|
|
|
client.connection.onRoomUpdate.add(
|
|
|
|
|
RoomUpdate(
|
|
|
|
|
id: id,
|
|
|
|
|
membership: membership,
|
|
|
|
|
prev_batch: resp["end"],
|
|
|
|
|
notification_count: notificationCount,
|
|
|
|
|
highlight_count: highlightCount,
|
|
|
|
|
),
|
|
|
|
|
);
|
2019-06-11 11:44:25 +00:00
|
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
/// Sets this room as a direct chat for this user if not already.
|
|
|
|
|
Future<void> addToDirectChat(String userID) async {
|
2019-08-08 09:41:42 +00:00
|
|
|
|
Map<String, dynamic> directChats = client.directChats;
|
2019-06-12 09:46:57 +00:00
|
|
|
|
if (directChats.containsKey(userID)) if (!directChats[userID].contains(id))
|
|
|
|
|
directChats[userID].add(id);
|
|
|
|
|
else
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return; // Is already in direct chats
|
2019-06-12 09:46:57 +00:00
|
|
|
|
else
|
|
|
|
|
directChats[userID] = [id];
|
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2019-06-12 09:46:57 +00:00
|
|
|
|
action: "/client/r0/user/${client.userID}/account_data/m.direct",
|
|
|
|
|
data: directChats);
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-12 09:46:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
/// Removes this room from all direct chat tags.
|
|
|
|
|
Future<void> removeFromDirectChat() async {
|
2019-09-30 08:19:28 +00:00
|
|
|
|
Map<String, dynamic> directChats = client.directChats;
|
|
|
|
|
if (directChats.containsKey(directChatMatrixID) &&
|
|
|
|
|
directChats[directChatMatrixID].contains(id))
|
|
|
|
|
directChats[directChatMatrixID].remove(id);
|
|
|
|
|
else
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return; // Nothing to do here
|
2019-09-30 08:19:28 +00:00
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
await client.connection.jsonRequest(
|
2019-09-30 08:19:28 +00:00
|
|
|
|
type: HTTPType.PUT,
|
|
|
|
|
action: "/client/r0/user/${client.userID}/account_data/m.direct",
|
|
|
|
|
data: directChats);
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-09-30 08:19:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-26 14:39:52 +00:00
|
|
|
|
/// Sends *m.fully_read* and *m.read* for the given event ID.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> sendReadReceipt(String eventID) async {
|
2019-12-17 11:07:25 +00:00
|
|
|
|
this.notificationCount = 0;
|
|
|
|
|
client?.store?.resetNotificationCount(this.id);
|
2019-12-29 10:28:33 +00:00
|
|
|
|
client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST,
|
2019-06-11 12:13:30 +00:00
|
|
|
|
action: "/client/r0/rooms/$id/read_markers",
|
2019-06-12 09:46:57 +00:00
|
|
|
|
data: {
|
|
|
|
|
"m.fully_read": eventID,
|
|
|
|
|
"m.read": eventID,
|
|
|
|
|
});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-11 12:13:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-07 07:50:40 +00:00
|
|
|
|
/// Returns a Room from a json String which comes normally from the store. If the
|
|
|
|
|
/// state are also given, the method will await them.
|
2019-06-12 09:46:57 +00:00
|
|
|
|
static Future<Room> getRoomFromTableRow(
|
2019-08-07 07:50:40 +00:00
|
|
|
|
Map<String, dynamic> row, Client matrix,
|
2019-08-07 08:46:59 +00:00
|
|
|
|
{Future<List<Map<String, dynamic>>> states,
|
|
|
|
|
Future<List<Map<String, dynamic>>> roomAccountData}) async {
|
2019-08-07 08:17:03 +00:00
|
|
|
|
Room newRoom = Room(
|
2019-08-28 11:06:41 +00:00
|
|
|
|
id: row["room_id"],
|
2019-08-08 09:41:42 +00:00
|
|
|
|
membership: Membership.values
|
|
|
|
|
.firstWhere((e) => e.toString() == 'Membership.' + row["membership"]),
|
2019-06-09 10:16:48 +00:00
|
|
|
|
notificationCount: row["notification_count"],
|
|
|
|
|
highlightCount: row["highlight_count"],
|
2019-06-11 10:21:45 +00:00
|
|
|
|
notificationSettings: row["notification_settings"],
|
|
|
|
|
prev_batch: row["prev_batch"],
|
2019-08-06 09:47:09 +00:00
|
|
|
|
mInvitedMemberCount: row["invited_member_count"],
|
|
|
|
|
mJoinedMemberCount: row["joined_member_count"],
|
|
|
|
|
mHeroes: row["heroes"]?.split(",") ?? [],
|
2019-06-11 08:51:45 +00:00
|
|
|
|
client: matrix,
|
2019-08-08 09:41:42 +00:00
|
|
|
|
roomAccountData: {},
|
2019-06-09 10:16:48 +00:00
|
|
|
|
);
|
2019-08-07 08:17:03 +00:00
|
|
|
|
|
|
|
|
|
if (states != null) {
|
|
|
|
|
List<Map<String, dynamic>> rawStates = await states;
|
|
|
|
|
for (int i = 0; i < rawStates.length; i++) {
|
2019-08-08 10:51:07 +00:00
|
|
|
|
RoomState newState = RoomState.fromJson(rawStates[i], newRoom);
|
2019-11-22 08:53:48 +00:00
|
|
|
|
newRoom.setState(newState);
|
2019-08-07 08:17:03 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-07 08:46:59 +00:00
|
|
|
|
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;
|
|
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
2019-08-07 08:46:59 +00:00
|
|
|
|
return newRoom;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-26 14:39:52 +00:00
|
|
|
|
/// Creates a timeline from the store. Returns a [Timeline] object.
|
2019-06-25 10:06:26 +00:00
|
|
|
|
Future<Timeline> getTimeline(
|
|
|
|
|
{onTimelineUpdateCallback onUpdate,
|
|
|
|
|
onTimelineInsertCallback onInsert}) async {
|
2019-09-02 08:55:46 +00:00
|
|
|
|
List<Event> events = [];
|
2019-09-10 05:27:00 +00:00
|
|
|
|
if (client.store != null)
|
|
|
|
|
events = await client.store.getEventList(this);
|
|
|
|
|
else {
|
|
|
|
|
prev_batch = "";
|
|
|
|
|
requestHistory();
|
|
|
|
|
}
|
2019-06-21 10:18:54 +00:00
|
|
|
|
return Timeline(
|
|
|
|
|
room: this,
|
|
|
|
|
events: events,
|
|
|
|
|
onUpdate: onUpdate,
|
|
|
|
|
onInsert: onInsert,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Load all participants for a given room from the store.
|
2019-08-08 09:41:42 +00:00
|
|
|
|
@deprecated
|
2019-06-11 08:51:45 +00:00
|
|
|
|
Future<List<User>> loadParticipants() async {
|
2019-06-21 10:18:54 +00:00
|
|
|
|
return await client.store.loadParticipants(this);
|
2019-06-11 08:51:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-02 10:09:30 +00:00
|
|
|
|
/// 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 = [];
|
2019-11-20 13:02:23 +00:00
|
|
|
|
if (states["m.room.member"] is Map<String, dynamic>) {
|
|
|
|
|
for (var entry in states["m.room.member"].entries) {
|
|
|
|
|
RoomState state = entry.value;
|
|
|
|
|
if (state.type == EventTypes.RoomMember) userList.add(state.asUser);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-02 10:09:30 +00:00
|
|
|
|
return userList;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Request the full list of participants from the server. The local list
|
|
|
|
|
/// from the store is not complete if the client uses lazy loading.
|
2019-06-18 10:06:55 +00:00
|
|
|
|
Future<List<User>> requestParticipants() async {
|
2019-06-09 10:16:48 +00:00
|
|
|
|
List<User> participants = [];
|
|
|
|
|
|
2019-07-12 09:26:07 +00:00
|
|
|
|
dynamic res = await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.GET, action: "/client/r0/rooms/${id}/members");
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
|
|
for (num i = 0; i < res["chunk"].length; i++) {
|
2019-08-08 10:51:07 +00:00
|
|
|
|
User newUser = RoomState.fromJson(res["chunk"][i], this).asUser;
|
2019-07-12 09:26:07 +00:00
|
|
|
|
if (newUser.membership != Membership.leave) participants.add(newUser);
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-21 10:18:54 +00:00
|
|
|
|
return participants;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
2019-06-28 08:59:00 +00:00
|
|
|
|
|
2019-11-15 11:08:43 +00:00
|
|
|
|
/// Returns the [User] object for the given [mxID] or requests it from
|
|
|
|
|
/// the homeserver and waits for a response.
|
2019-07-29 14:16:20 +00:00
|
|
|
|
Future<User> getUserByMXID(String mxID) async {
|
2019-08-08 09:41:42 +00:00
|
|
|
|
if (states[mxID] != null) return states[mxID].asUser;
|
2019-11-15 11:08:43 +00:00
|
|
|
|
return requestUser(mxID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the [User] object for the given [mxID] or requests it from
|
|
|
|
|
/// the homeserver and returns a default [User] object while waiting.
|
|
|
|
|
User getUserByMXIDSync(String mxID) {
|
|
|
|
|
if (states[mxID] != null)
|
|
|
|
|
return states[mxID].asUser;
|
|
|
|
|
else {
|
2019-12-29 10:28:33 +00:00
|
|
|
|
try {
|
|
|
|
|
requestUser(mxID);
|
|
|
|
|
} catch (_) {}
|
2019-11-15 11:08:43 +00:00
|
|
|
|
return User(mxID, room: this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Set<String> _requestingMatrixIds = Set();
|
|
|
|
|
|
|
|
|
|
/// Requests a missing [User] for this room. Important for clients using
|
|
|
|
|
/// lazy loading.
|
|
|
|
|
Future<User> requestUser(String mxID) async {
|
|
|
|
|
if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Map<String, dynamic> resp;
|
|
|
|
|
try {
|
|
|
|
|
resp = await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.GET,
|
|
|
|
|
action: "/client/r0/rooms/$id/state/m.room.member/$mxID");
|
|
|
|
|
} catch (exception) {
|
2019-11-15 11:08:43 +00:00
|
|
|
|
_requestingMatrixIds.remove(mxID);
|
2019-12-29 10:28:33 +00:00
|
|
|
|
rethrow;
|
2019-11-15 11:08:43 +00:00
|
|
|
|
}
|
|
|
|
|
final User user = User(mxID,
|
2019-09-17 12:21:16 +00:00
|
|
|
|
displayName: resp["displayname"],
|
|
|
|
|
avatarUrl: resp["avatar_url"],
|
|
|
|
|
room: this);
|
2019-11-15 11:08:43 +00:00
|
|
|
|
states[mxID] = user;
|
|
|
|
|
if (client.store != null)
|
|
|
|
|
client.store.transaction(() {
|
|
|
|
|
client.store.storeEventUpdate(
|
|
|
|
|
EventUpdate(
|
|
|
|
|
content: resp,
|
|
|
|
|
roomID: id,
|
|
|
|
|
type: "state",
|
|
|
|
|
eventType: "m.room.member"),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
});
|
|
|
|
|
if (onUpdate != null) onUpdate();
|
|
|
|
|
_requestingMatrixIds.remove(mxID);
|
|
|
|
|
return user;
|
2019-07-29 14:16:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-29 11:12:04 +00:00
|
|
|
|
/// Searches for the event on the server. Returns null if not found.
|
2019-06-28 08:59:00 +00:00
|
|
|
|
Future<Event> getEventById(String eventID) async {
|
|
|
|
|
final dynamic resp = await client.connection.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.GET, action: "/client/r0/rooms/$id/event/$eventID");
|
2019-08-07 08:17:03 +00:00
|
|
|
|
return Event.fromJson(resp, this);
|
2019-06-28 08:59:00 +00:00
|
|
|
|
}
|
2019-08-07 09:23:57 +00:00
|
|
|
|
|
2019-09-03 15:57:27 +00:00
|
|
|
|
/// Returns the power level of the given user ID.
|
2019-08-08 09:41:42 +00:00
|
|
|
|
int getPowerLevelByUserId(String userId) {
|
2019-08-07 09:23:57 +00:00
|
|
|
|
int powerLevel = 0;
|
2019-08-08 10:51:07 +00:00
|
|
|
|
RoomState powerLevelState = states["m.room.power_levels"];
|
2019-08-07 09:23:57 +00:00
|
|
|
|
if (powerLevelState == null) return powerLevel;
|
|
|
|
|
if (powerLevelState.content["users_default"] is int)
|
|
|
|
|
powerLevel = powerLevelState.content["users_default"];
|
2019-08-08 09:41:42 +00:00
|
|
|
|
if (powerLevelState.content["users"] is Map<String, dynamic> &&
|
|
|
|
|
powerLevelState.content["users"][userId] != null)
|
|
|
|
|
powerLevel = powerLevelState.content["users"][userId];
|
2019-08-07 09:23:57 +00:00
|
|
|
|
return powerLevel;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-08 09:41:42 +00:00
|
|
|
|
/// Returns the user's own power level.
|
|
|
|
|
int get ownPowerLevel => getPowerLevelByUserId(client.userID);
|
|
|
|
|
|
2019-08-07 09:23:57 +00:00
|
|
|
|
/// Returns the power levels from all users for this room or null if not given.
|
|
|
|
|
Map<String, int> get powerLevels {
|
2019-08-08 10:51:07 +00:00
|
|
|
|
RoomState powerLevelState = states["m.room.power_levels"];
|
2019-08-07 09:23:57 +00:00
|
|
|
|
if (powerLevelState.content["users"] is Map<String, int>)
|
|
|
|
|
return powerLevelState.content["users"];
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2019-09-09 13:22:02 +00:00
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
/// Uploads a new user avatar for this room. Returns the event ID of the new
|
|
|
|
|
/// m.room.avatar event.
|
|
|
|
|
Future<String> setAvatar(MatrixFile file) async {
|
|
|
|
|
final String uploadResp = await client.connection.upload(file);
|
|
|
|
|
final Map<String, dynamic> setAvatarResp = await client.connection
|
|
|
|
|
.jsonRequest(
|
|
|
|
|
type: HTTPType.PUT,
|
|
|
|
|
action: "/client/r0/rooms/$id/state/m.room.avatar/",
|
|
|
|
|
data: {"url": uploadResp});
|
2019-09-09 13:22:02 +00:00
|
|
|
|
return setAvatarResp["event_id"];
|
|
|
|
|
}
|
2019-11-26 06:38:44 +00:00
|
|
|
|
|
|
|
|
|
bool _hasPermissionFor(String action) {
|
|
|
|
|
if (getState("m.room.power_levels") == null ||
|
|
|
|
|
getState("m.room.power_levels").content[action] == null) return false;
|
|
|
|
|
return ownPowerLevel >= getState("m.room.power_levels").content[action];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The level required to ban a user.
|
|
|
|
|
bool get canBan => _hasPermissionFor("ban");
|
|
|
|
|
|
|
|
|
|
/// The default level required to send message events. Can be overridden by the events key.
|
|
|
|
|
bool get canSendDefaultMessages => _hasPermissionFor("events_default");
|
|
|
|
|
|
|
|
|
|
/// The level required to invite a user.
|
|
|
|
|
bool get canInvite => _hasPermissionFor("invite");
|
|
|
|
|
|
|
|
|
|
/// The level required to kick a user.
|
|
|
|
|
bool get canKick => _hasPermissionFor("kick");
|
|
|
|
|
|
|
|
|
|
/// The level required to redact an event.
|
|
|
|
|
bool get canRedact => _hasPermissionFor("redact");
|
|
|
|
|
|
|
|
|
|
/// The default level required to send state events. Can be overridden by the events key.
|
|
|
|
|
bool get canSendDefaultStates => _hasPermissionFor("state_default");
|
|
|
|
|
|
|
|
|
|
bool get canChangePowerLevel => canSendEvent("m.room.power_levels");
|
|
|
|
|
|
|
|
|
|
bool canSendEvent(String eventType) {
|
2019-12-05 09:06:23 +00:00
|
|
|
|
if (getState("m.room.power_levels") == null) return false;
|
|
|
|
|
if (getState("m.room.power_levels").content["events"] == null ||
|
|
|
|
|
getState("m.room.power_levels").content["events"][eventType] == null)
|
2019-11-26 06:38:44 +00:00
|
|
|
|
return eventType == "m.room.message"
|
|
|
|
|
? canSendDefaultMessages
|
|
|
|
|
: canSendDefaultStates;
|
|
|
|
|
return ownPowerLevel >=
|
|
|
|
|
getState("m.room.power_levels").content["events"][eventType];
|
|
|
|
|
}
|
2019-12-04 09:58:47 +00:00
|
|
|
|
|
|
|
|
|
/// Returns the [PushRuleState] for this room, based on the m.push_rules stored in
|
|
|
|
|
/// the account_data.
|
|
|
|
|
PushRuleState get pushRuleState {
|
|
|
|
|
if (!client.accountData.containsKey("m.push_rules") ||
|
|
|
|
|
!(client.accountData["m.push_rules"].content["global"] is Map))
|
|
|
|
|
return PushRuleState.notify;
|
|
|
|
|
final Map<String, dynamic> globalPushRules =
|
|
|
|
|
client.accountData["m.push_rules"].content["global"];
|
|
|
|
|
if (globalPushRules == null) return PushRuleState.notify;
|
|
|
|
|
|
|
|
|
|
if (globalPushRules["override"] is List) {
|
|
|
|
|
for (var i = 0; i < globalPushRules["override"].length; i++) {
|
|
|
|
|
if (globalPushRules["override"][i]["rule_id"] == id) {
|
|
|
|
|
if (globalPushRules["override"][i]["actions"]
|
|
|
|
|
.indexOf("dont_notify") !=
|
|
|
|
|
-1) {
|
|
|
|
|
return PushRuleState.dont_notify;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (globalPushRules["room"] is List) {
|
|
|
|
|
for (var i = 0; i < globalPushRules["room"].length; i++) {
|
|
|
|
|
if (globalPushRules["room"][i]["rule_id"] == id) {
|
|
|
|
|
if (globalPushRules["room"][i]["actions"].indexOf("dont_notify") !=
|
|
|
|
|
-1) {
|
|
|
|
|
return PushRuleState.mentions_only;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return PushRuleState.notify;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sends a request to the homeserver to set the [PushRuleState] for this room.
|
|
|
|
|
/// Returns ErrorResponse if something goes wrong.
|
|
|
|
|
Future<dynamic> setPushRuleState(PushRuleState newState) async {
|
|
|
|
|
if (newState == pushRuleState) return null;
|
|
|
|
|
dynamic resp;
|
|
|
|
|
switch (newState) {
|
|
|
|
|
// All push notifications should be sent to the user
|
|
|
|
|
case PushRuleState.notify:
|
|
|
|
|
if (pushRuleState == PushRuleState.dont_notify)
|
|
|
|
|
resp = await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.DELETE,
|
|
|
|
|
action: "/client/r0/pushrules/global/override/$id",
|
|
|
|
|
data: {});
|
|
|
|
|
else if (pushRuleState == PushRuleState.mentions_only)
|
|
|
|
|
resp = await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.DELETE,
|
|
|
|
|
action: "/client/r0/pushrules/global/room/$id",
|
|
|
|
|
data: {});
|
|
|
|
|
break;
|
|
|
|
|
// Only when someone mentions the user, a push notification should be sent
|
|
|
|
|
case PushRuleState.mentions_only:
|
|
|
|
|
if (pushRuleState == PushRuleState.dont_notify) {
|
|
|
|
|
resp = await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.DELETE,
|
|
|
|
|
action: "/client/r0/pushrules/global/override/$id",
|
|
|
|
|
data: {});
|
|
|
|
|
resp = await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.PUT,
|
|
|
|
|
action: "/client/r0/pushrules/global/room/$id",
|
|
|
|
|
data: {
|
|
|
|
|
"actions": ["dont_notify"]
|
|
|
|
|
});
|
|
|
|
|
} else if (pushRuleState == PushRuleState.notify)
|
|
|
|
|
resp = await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.PUT,
|
|
|
|
|
action: "/client/r0/pushrules/global/room/$id",
|
|
|
|
|
data: {
|
|
|
|
|
"actions": ["dont_notify"]
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
// No push notification should be ever sent for this room.
|
|
|
|
|
case PushRuleState.dont_notify:
|
|
|
|
|
if (pushRuleState == PushRuleState.mentions_only) {
|
|
|
|
|
resp = await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.DELETE,
|
|
|
|
|
action: "/client/r0/pushrules/global/room/$id",
|
|
|
|
|
data: {});
|
|
|
|
|
}
|
|
|
|
|
resp = await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.PUT,
|
|
|
|
|
action: "/client/r0/pushrules/global/override/$id",
|
|
|
|
|
data: {
|
|
|
|
|
"actions": ["dont_notify"],
|
|
|
|
|
"conditions": [
|
|
|
|
|
{"key": "room_id", "kind": "event_match", "pattern": id}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return resp;
|
|
|
|
|
}
|
2019-12-12 12:19:18 +00:00
|
|
|
|
|
|
|
|
|
/// Redacts this event. Returns [ErrorResponse] on error.
|
|
|
|
|
Future<dynamic> redactEvent(String eventId,
|
|
|
|
|
{String reason, String txid}) async {
|
|
|
|
|
// Create new transaction id
|
|
|
|
|
String messageID;
|
|
|
|
|
final int now = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
|
if (txid == null) {
|
|
|
|
|
messageID = "msg$now";
|
|
|
|
|
} else
|
|
|
|
|
messageID = txid;
|
|
|
|
|
Map<String, dynamic> data = {};
|
|
|
|
|
if (reason != null) data["reason"] = reason;
|
|
|
|
|
final dynamic resp = await client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.PUT,
|
|
|
|
|
action: "/client/r0/rooms/$id/redact/$eventId/$messageID",
|
|
|
|
|
data: data);
|
|
|
|
|
return resp;
|
|
|
|
|
}
|
2019-12-16 11:55:13 +00:00
|
|
|
|
|
|
|
|
|
Future<dynamic> sendTypingInfo(bool isTyping, {int timeout}) {
|
2019-12-17 11:07:25 +00:00
|
|
|
|
Map<String, dynamic> data = {
|
2019-12-16 11:55:13 +00:00
|
|
|
|
"typing": isTyping,
|
|
|
|
|
};
|
|
|
|
|
if (timeout != null) data["timeout"] = timeout;
|
|
|
|
|
return client.connection.jsonRequest(
|
|
|
|
|
type: HTTPType.PUT,
|
|
|
|
|
action: "/client/r0/rooms/${this.id}/typing/${client.userID}",
|
|
|
|
|
data: data,
|
|
|
|
|
);
|
|
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
2019-12-04 09:58:47 +00:00
|
|
|
|
|
|
|
|
|
enum PushRuleState { notify, mentions_only, dont_notify }
|