feat: Implement spaces

This commit is contained in:
Christian Pauly 2021-03-31 14:22:57 +02:00
parent d7b7619a63
commit fb0177ac5f
6 changed files with 258 additions and 1 deletions

View File

@ -526,6 +526,34 @@ class Client extends MatrixApi {
return roomId; return roomId;
} }
/// Creates a new space and returns the Room ID. The parameters are mostly
/// the same like in [createRoom()].
/// Be aware that spaces appear in the [rooms] list. You should check if a
/// room is a space by using the `room.isSpace` getter and then just use the
/// room as a space with `room.toSpace()`.
///
/// https://github.com/matrix-org/matrix-doc/blob/matthew/msc1772/proposals/1772-groups-as-rooms.md
Future<String> createSpace({
String name,
String topic,
Visibility visibility = Visibility.public,
String spaceAliasName,
List<String> invite,
List<Map<String, dynamic>> invite3pid,
String roomVersion,
}) =>
createRoom(
name: name,
topic: topic,
visibility: visibility,
roomAliasName: spaceAliasName,
creationContent: {'type': 'm.space'},
powerLevelContentOverride: {'events_default': 100},
invite: invite,
invite3pid: invite3pid,
roomVersion: roomVersion,
);
/// Returns the user's own displayname and avatar url. In Matrix it is possible that /// Returns the user's own displayname and avatar url. In Matrix it is possible that
/// one user can have different displaynames and avatar urls in different rooms. So /// one user can have different displaynames and avatar urls in different rooms. So
/// this endpoint first checks if the profile is the same in all rooms. If not, the /// this endpoint first checks if the profile is the same in all rooms. If not, the

View File

@ -19,6 +19,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:famedlysdk/src/utils/space_child.dart';
import 'package:html_unescape/html_unescape.dart'; import 'package:html_unescape/html_unescape.dart';
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
@ -1806,6 +1807,73 @@ class Room {
? getState(EventTypes.RoomTombstone).parsedTombstoneContent ? getState(EventTypes.RoomTombstone).parsedTombstoneContent
: null; : null;
/// Checks if the `m.room.create` state has a `type` key with the value
/// `m.space`.
bool get isSpace =>
getState(EventTypes.RoomCreate)?.content?.tryGet<String>('type') ==
RoomCreationTypes.mSpace; // TODO: Magic string!
/// The parents of this room. Currently this SDK doesn't yet set the canonical
/// flag and is not checking if this room is in fact a child of this space.
/// You should therefore not rely on this and always check the children of
/// the space.
List<SpaceParent> get spaceParents =>
states[EventTypes.spaceParent]
?.values
?.map((state) => SpaceParent.fromState(state))
?.where((child) => child.via?.isNotEmpty ?? false)
?.toList() ??
[];
/// List all children of this space. Children without a `via` domain will be
/// ignored.
/// Children are sorted by the `order` while those without this field will be
/// sorted at the end of the list.
List<SpaceChild> get spaceChildren => !isSpace
? throw Exception('Room is not a space!')
: (states[EventTypes.spaceChild]
?.values
?.map((state) => SpaceChild.fromState(state))
?.where((child) => child.via?.isNotEmpty ?? false)
?.toList() ??
[])
..sort((a, b) => a.order.isEmpty || b.order.isEmpty
? b.order.compareTo(a.order)
: a.order.compareTo(b.order));
/// Adds or edits a child of this space.
Future<void> setSpaceChild(
String roomId, {
List<String> via,
String order,
bool suggested,
}) async {
if (!isSpace) throw Exception('Room is not a space!');
via ??= [roomId.domain];
await client.sendState(
id,
EventTypes.spaceChild,
{
'via': via,
if (order != null) 'order': order,
if (suggested != null) 'suggested': suggested,
},
roomId);
await client.sendState(
roomId,
EventTypes.spaceParent,
{
'via': via,
},
id);
return;
}
/// Remove a child from this space by setting the `via` to an empty list.
Future<void> removeSpaceChild(String roomId) => !isSpace
? throw Exception('Room is not a space!')
: setSpaceChild(roomId, via: const []);
@override @override
bool operator ==(dynamic other) => (other is Room && other.id == id); bool operator ==(dynamic other) => (other is Room && other.id == id);
} }

View File

@ -0,0 +1,47 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import 'package:matrix_api_lite/matrix_api_lite.dart';
import '../event.dart';
class SpaceChild {
final String roomId;
final List<String> via;
final String order;
final bool suggested;
SpaceChild.fromState(Event state)
: assert(state.type == EventTypes.spaceChild),
roomId = state.stateKey,
via = state.content.tryGetList<String>('via'),
order = state.content.tryGet<String>('order', ''),
suggested = state.content.tryGet<bool>('suggested');
}
class SpaceParent {
final String roomId;
final List<String> via;
final bool canonical;
SpaceParent.fromState(Event state)
: assert(state.type == EventTypes.spaceParent),
roomId = state.stateKey,
via = state.content.tryGetList<String>('via'),
canonical = state.content.tryGet<bool>('canonical');
}

View File

@ -23,7 +23,7 @@ dependencies:
matrix_file_e2ee: ^1.1.0 matrix_file_e2ee: ^1.1.0
isolate: ^2.0.3 isolate: ^2.0.3
logger: ^1.0.0 logger: ^1.0.0
matrix_api_lite: ^0.2.2 matrix_api_lite: ^0.2.4
dev_dependencies: dev_dependencies:
test: ^1.15.7 test: ^1.15.7

View File

@ -340,6 +340,16 @@ void main() {
await matrix.setMuteAllPushNotifications(false); await matrix.setMuteAllPushNotifications(false);
}); });
test('createSpace', () async {
await matrix.createSpace(
name: 'space',
topic: 'My test space',
spaceAliasName: '#myspace:example.invalid',
invite: ['@alice:example.invalid'],
roomVersion: '3',
);
});
test('get archive', () async { test('get archive', () async {
var archive = await matrix.archive; var archive = await matrix.archive;

View File

@ -768,6 +768,110 @@ void main() {
expect(room.getState('m.room.message') != null, true); expect(room.getState('m.room.message') != null, true);
}); });
test('Spaces', () async {
expect(room.isSpace, false);
room.states['m.room.create'] = {
'': Event.fromJson({
'content': {'type': 'm.space'},
'event_id': '\$143273582443PhrSn:example.org',
'origin_server_ts': 1432735824653,
'room_id': '!jEsUZKDJdhlrceRyVU:example.org',
'sender': '@example:example.org',
'type': 'm.room.create',
'unsigned': {'age': 1234},
'state_key': '',
}, room, 1432735824653.0),
};
expect(room.isSpace, true);
expect(room.spaceParents.isEmpty, true);
room.states[EventTypes.spaceParent] = {
'!1234:example.invalid': Event.fromJson({
'content': {
'via': ['example.invalid']
},
'event_id': '\$143273582443PhrSn:example.org',
'origin_server_ts': 1432735824653,
'room_id': '!jEsUZKDJdhlrceRyVU:example.org',
'sender': '@example:example.org',
'type': EventTypes.spaceParent,
'unsigned': {'age': 1234},
'state_key': '!1234:example.invalid',
}, room, 1432735824653.0),
};
expect(room.spaceParents.length, 1);
expect(room.spaceChildren.isEmpty, true);
room.states[EventTypes.spaceChild] = {
'!b:example.invalid': Event.fromJson({
'content': {
'via': ['example.invalid'],
'order': 'b',
},
'event_id': '\$143273582443PhrSn:example.org',
'origin_server_ts': 1432735824653,
'room_id': '!jEsUZKDJdhlrceRyVU:example.org',
'sender': '@example:example.org',
'type': EventTypes.spaceChild,
'unsigned': {'age': 1234},
'state_key': '!b:example.invalid',
}, room, 1432735824653.0),
'!c:example.invalid': Event.fromJson({
'content': {
'via': ['example.invalid'],
'order': 'c',
},
'event_id': '\$143273582443PhrSn:example.org',
'origin_server_ts': 1432735824653,
'room_id': '!jEsUZKDJdhlrceRyVU:example.org',
'sender': '@example:example.org',
'type': EventTypes.spaceChild,
'unsigned': {'age': 1234},
'state_key': '!c:example.invalid',
}, room, 1432735824653.0),
'!noorder:example.invalid': Event.fromJson({
'content': {
'via': ['example.invalid'],
},
'event_id': '\$143273582443PhrSn:example.org',
'origin_server_ts': 1432735824653,
'room_id': '!jEsUZKDJdhlrceRyVU:example.org',
'sender': '@example:example.org',
'type': EventTypes.spaceChild,
'unsigned': {'age': 1234},
'state_key': '!noorder:example.invalid',
}, room, 1432735824653.0),
'!a:example.invalid': Event.fromJson({
'content': {
'via': ['example.invalid'],
'order': 'a',
},
'event_id': '\$143273582443PhrSn:example.org',
'origin_server_ts': 1432735824653,
'room_id': '!jEsUZKDJdhlrceRyVU:example.org',
'sender': '@example:example.org',
'type': EventTypes.spaceChild,
'unsigned': {'age': 1234},
'state_key': '!a:example.invalid',
}, room, 1432735824653.0),
};
expect(room.spaceChildren.length, 4);
expect(room.spaceChildren[0].roomId, '!a:example.invalid');
expect(room.spaceChildren[1].roomId, '!b:example.invalid');
expect(room.spaceChildren[2].roomId, '!c:example.invalid');
expect(room.spaceChildren[3].roomId, '!noorder:example.invalid');
// TODO: Implement a more generic fake api
/*await room.setSpaceChild(
'!jEsUZKDJdhlrceRyVU:example.org',
via: ['example.invalid'],
order: '5',
suggested: true,
);
await room.removeSpaceChild('!1234:example.invalid');*/
});
test('logout', () async { test('logout', () async {
await matrix.logout(); await matrix.logout();
}); });