feat: Implement spaces
This commit is contained in:
parent
d7b7619a63
commit
fb0177ac5f
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue