famedlysdk/lib/src/Room.dart

754 lines
26 KiB
Dart
Raw Normal View History

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
* 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';
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-09 10:16:48 +00:00
import 'package:famedlysdk/src/responses/ErrorResponse.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';
import 'package:famedlysdk/src/utils/ChatTime.dart';
2019-10-18 11:05:07 +00:00
import 'package:famedlysdk/src/utils/MatrixFile.dart';
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-06-09 11:57:33 +00:00
import './User.dart';
import 'Connection.dart';
2019-06-21 10:18:54 +00:00
import 'Timeline.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.
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-09-03 14:34:38 +00:00
/// Key-Value store for room states.
2019-08-08 10:51:07 +00:00
Map<String, RoomState> states = {};
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-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.
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];
2019-08-28 10:32:50 +00:00
if (mHeroes != null && mHeroes.length > 0) {
2019-08-07 08:17:03 +00:00
String displayname = "";
2019-08-08 09:41:42 +00:00
for (int i = 0; i < mHeroes.length; i++) {
User hero = states[mHeroes[i]] != null
? states[mHeroes[i]].asUser
2019-08-08 09:54:39 +00:00
: User(mHeroes[i]);
2019-08-08 09:41:42 +00:00
displayname += hero.calcDisplayname() + ", ";
}
2019-08-07 08:17:03 +00:00
return displayname.substring(0, displayname.length - 2);
}
2019-09-30 08:19:28 +00:00
if (membership == Membership.invite && states.containsKey(client.userID)) {
return states[client.userID].sender.calcDisplayname();
}
2019-08-07 08:17:03 +00:00
return "Empty chat";
}
/// 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-09-30 08:19:28 +00:00
if (membership == Membership.invite && states.containsKey(client.userID)) {
return states[client.userID].sender.avatarUrl;
}
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-08-29 08:05:17 +00:00
Event lastEvent = null;
2019-08-29 07:52:37 +00:00
states.forEach((String key, RoomState state) {
2019-08-29 09:21:10 +00:00
if (state.time != null && state.time > lastTime) {
2019-08-29 07:52:37 +00:00
lastTime = state.time;
lastEvent = state.timelineEvent;
}
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++)
typingUsers.add(
states[typingMxid[i]]?.asUser ?? User(typingMxid[i], room: this));
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.states = const {},
this.roomAccountData = const {},
2019-06-09 10:16:48 +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 {
if (name != null && !name.isEmpty) return name;
if (canonicalAlias != null &&
!canonicalAlias.isEmpty &&
canonicalAlias.length > 3)
return canonicalAlias.substring(1, canonicalAlias.length).split(":")[0];
if (mHeroes.length > 0) {
String displayname = "";
for (int i = 0; i < mHeroes.length; i++)
2019-08-08 09:54:39 +00:00
displayname += User(mHeroes[i]).calcDisplayname() + ", ";
2019-08-06 09:47:09 +00:00
return displayname.substring(0, displayname.length - 2);
}
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-06-11 08:51:45 +00:00
/// Call the Matrix API to change the name of this room.
2019-06-11 11:44:25 +00:00
Future<dynamic> setName(String newName) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
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-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to change the topic of this room.
2019-06-11 11:44:25 +00:00
Future<dynamic> setDescription(String newName) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
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-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-09-09 13:22:02 +00:00
Future<dynamic> _sendRawEventNow(Map<String, dynamic> content,
{String txid = null}) async {
if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}";
final dynamic res = await client.connection.jsonRequest(
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-06-26 14:36:34 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
return res;
}
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-10-18 11:05:07 +00:00
Future<String> sendFileEvent(MatrixFile file, String msgType,
2019-09-09 13:22:02 +00:00
{String txid = null}) async {
2019-10-02 11:33:01 +00:00
String fileName = file.path.split("/").last;
2019-09-09 13:22:02 +00:00
// Try to get the size of the file
int size;
try {
2019-10-18 11:05:07 +00:00
size = file.size;
2019-09-09 13:22:02 +00:00
} catch (e) {
print("[UPLOAD] Could not get size. Reason: ${e.toString()}");
}
// Upload file
2019-10-02 11:33:01 +00:00
String mimeType = mime(file.path);
2019-09-09 13:22:02 +00:00
final dynamic uploadResp = await client.connection.upload(file);
if (uploadResp is ErrorResponse) return null;
// Send event
Map<String, dynamic> content = {
"msgtype": msgType,
"body": fileName,
"filename": fileName,
"url": uploadResp,
"info": {
"mimetype": mimeType,
}
};
if (size != null)
content["info"] = {
"size": size,
"mimetype": mimeType,
};
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-09-09 13:22:02 +00:00
final dynamic uploadResp = await client.connection.upload(file);
if (uploadResp is ErrorResponse) return null;
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);
}
Future<String> sendEvent(Map<String, dynamic> content,
{String txid = null}) async {
final String type = "m.room.message";
// Create new transaction id
2019-06-26 14:36:34 +00:00
String messageID;
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-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
});
client.connection.onEvent.add(eventUpdate);
2019-06-26 14:36:34 +00:00
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
});
2019-06-26 14:36:34 +00:00
// Send the text and on success, store and display a *sent* event.
2019-09-09 13:22:02 +00:00
final dynamic res = await _sendRawEventNow(content, txid: messageID);
2019-06-26 14:36:34 +00:00
2019-06-27 07:44:37 +00:00
if (res is ErrorResponse || !(res["event_id"] is String)) {
2019-06-26 14:36:34 +00:00
// 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);
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
});
2019-06-12 09:46:57 +00:00
} else {
2019-06-26 14:36:34 +00:00
eventUpdate.content["status"] = 1;
2019-07-23 09:09:13 +00:00
eventUpdate.content["unsigned"] = {"transaction_id": messageID};
2019-06-27 07:44:37 +00:00
eventUpdate.content["event_id"] = res["event_id"];
2019-06-26 14:36:34 +00:00
client.connection.onEvent.add(eventUpdate);
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
});
2019-06-27 07:44:37 +00:00
return res["event_id"];
}
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.
Future<dynamic> join() async {
dynamic res = await client.connection.jsonRequest(
type: HTTPType.POST, action: "/client/r0/rooms/${id}/join");
if (res is ErrorResponse) client.connection.onError.add(res);
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);
return res;
}
/// Call the Matrix API to leave this room. If this room is set as a direct
/// chat, this will be removed too.
2019-06-09 10:16:48 +00:00
Future<dynamic> leave() async {
2019-09-30 08:19:28 +00:00
if (directChatMatrixID != "") await removeFromDirectChat();
dynamic res = await client.connection.jsonRequest(
type: HTTPType.POST, action: "/client/r0/rooms/${id}/leave");
2019-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to forget this room if you already left it.
2019-06-09 10:16:48 +00:00
Future<dynamic> forget() async {
2019-06-12 11:22:16 +00:00
client.store.forgetRoom(id);
dynamic res = await client.connection.jsonRequest(
type: HTTPType.POST, action: "/client/r0/rooms/${id}/forget");
2019-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to kick a user from this room.
2019-06-09 10:16:48 +00:00
Future<dynamic> kick(String userID) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
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-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to ban a user from this room.
2019-06-09 10:16:48 +00:00
Future<dynamic> ban(String userID) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
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-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to unban a banned user from this room.
2019-06-09 10:16:48 +00:00
Future<dynamic> unban(String userID) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
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-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
2019-08-08 09:41:42 +00:00
/// Set the power level of the user with the [userID] to the value [power].
2019-06-11 11:32:14 +00:00
Future<dynamic> setPower(String userID, int power) async {
2019-08-08 09:41:42 +00:00
if (states["m.room.power_levels"] == null) return null;
2019-08-07 09:38:51 +00:00
Map<String, int> powerMap = states["m.room.power_levels"].content["users"];
2019-06-11 11:32:14 +00:00
powerMap[userID] = power;
dynamic res = await client.connection.jsonRequest(
type: HTTPType.PUT,
2019-07-26 08:05:08 +00:00
action: "/client/r0/rooms/$id/state/m.room.power_levels",
2019-06-11 11:32:14 +00:00
data: {"users": powerMap});
if (res is ErrorResponse) client.connection.onError.add(res);
return res;
}
2019-06-11 08:51:45 +00:00
/// Call the Matrix API to invite a user to this room.
2019-06-09 10:16:48 +00:00
Future<dynamic> invite(String userID) async {
2019-06-11 08:51:45 +00:00
dynamic res = await client.connection.jsonRequest(
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-06-11 08:51:45 +00:00
if (res is ErrorResponse) client.connection.onError.add(res);
2019-06-09 10:16:48 +00:00
return res;
}
/// 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 {
final dynamic resp = await client.connection.jsonRequest(
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
if (resp is ErrorResponse) return;
if (onHistoryReceived != null) onHistoryReceived();
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"];
client.store?.transaction(() {
2019-06-11 11:44:25 +00:00
for (int i = 0; i < history.length; i++) {
EventUpdate eventUpdate = EventUpdate(
type: "history",
2019-06-11 11:44:25 +00:00
roomID: id,
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
}
return;
2019-06-11 11:44:25 +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-06-26 14:39:52 +00:00
/// Sets this room as a direct chat for this user.
2019-06-12 09:46:57 +00:00
Future<dynamic> 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
return null; // Is already in direct chats
else
directChats[userID] = [id];
final resp = await client.connection.jsonRequest(
type: HTTPType.PUT,
2019-06-12 09:46:57 +00:00
action: "/client/r0/user/${client.userID}/account_data/m.direct",
data: directChats);
return resp;
}
2019-09-30 08:19:28 +00:00
/// Sets this room as a direct chat for this user.
Future<dynamic> removeFromDirectChat() async {
Map<String, dynamic> directChats = client.directChats;
if (directChats.containsKey(directChatMatrixID) &&
directChats[directChatMatrixID].contains(id))
directChats[directChatMatrixID].remove(id);
else
return null; // Nothing to do here
final resp = await client.connection.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/user/${client.userID}/account_data/m.direct",
data: directChats);
return resp;
}
2019-06-26 14:39:52 +00:00
/// Sends *m.fully_read* and *m.read* for the given event ID.
2019-06-11 12:13:30 +00:00
Future<dynamic> sendReadReceipt(String eventID) async {
final dynamic resp = client.connection.jsonRequest(
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-06-11 12:13:30 +00:00
return resp;
}
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
states: {},
roomAccountData: {},
2019-06-09 10:16:48 +00:00
);
2019-08-07 08:17:03 +00:00
2019-08-08 10:51:07 +00:00
Map<String, RoomState> newStates = {};
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-08-07 08:17:03 +00:00
newStates[newState.key] = newState;
}
newRoom.states = newStates;
}
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 = [];
for (var entry in states.entries)
if (entry.value.type == EventTypes.RoomMember)
userList.add(entry.value.asUser);
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 = [];
dynamic res = await client.connection.jsonRequest(
type: HTTPType.GET, action: "/client/r0/rooms/${id}/members");
2019-06-09 10:16:48 +00:00
if (res is ErrorResponse || !(res["chunk"] is List<dynamic>))
return participants;
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;
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
}
Future<User> getUserByMXID(String mxID) async {
2019-08-08 09:41:42 +00:00
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;
2019-09-17 12:21:16 +00:00
return User(mxID,
displayName: resp["displayname"],
avatarUrl: resp["avatar_url"],
room: this);
}
/// Searches for the event in the store. If it isn't found, try to request it
/// from the server. Returns null if not found.
Future<Event> getEventById(String eventID) async {
if (client.store != null) {
final Event storeEvent = await client.store.getEventById(eventID, this);
if (storeEvent != null) return storeEvent;
}
final dynamic resp = await client.connection.jsonRequest(
type: HTTPType.GET, action: "/client/r0/rooms/$id/event/$eventID");
if (resp is ErrorResponse) return null;
2019-08-07 08:17:03 +00:00
return Event.fromJson(resp, this);
}
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
/// Uploads a new user avatar for this room. Returns ErrorResponse if something went wrong
/// and the event ID otherwise.
2019-10-18 11:05:07 +00:00
Future<dynamic> setAvatar(MatrixFile file) async {
2019-09-09 13:22:02 +00:00
final uploadResp = await client.connection.upload(file);
if (uploadResp is ErrorResponse) return uploadResp;
final setAvatarResp = await client.connection.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/rooms/$id/state/m.room.avatar/",
data: {"url": uploadResp});
if (setAvatarResp is ErrorResponse) return setAvatarResp;
return setAvatarResp["event_id"];
}
2019-06-09 10:16:48 +00:00
}