feat: introduce new MSC library architecture
- migrated to more useful MSC directory structure - migrate Widgets API into new structure - add recent emoji API into new structure The recent emoji API is non-standard and should be compatible with Element. Signed-off-by: Lanna Michalke <l.michalke@famedly.com>
This commit is contained in:
parent
ac16724841
commit
ecdbb06118
|
|
@ -0,0 +1,21 @@
|
||||||
|
# MSC extensions
|
||||||
|
|
||||||
|
This folder contains non-spec feature implementations, usually proposed in Matrix Specification Changes (MSCs).
|
||||||
|
|
||||||
|
Please try to cover the following conventions:
|
||||||
|
|
||||||
|
- name your implementation `/lib/msc_extensions/msc_NUMER_short_name/whatsoever.dart`,
|
||||||
|
e.g. `/lib/msc_extensions/msc_3588_stories/stories.dart`
|
||||||
|
- please link the MSC in a comment in the first line:
|
||||||
|
```dart
|
||||||
|
/// MSC3588: Stories As Rooms (https://github.com/matrix-org/matrix-spec-proposals/blob/d818877504cfda00ac52430ba5b9e8423c878b77/proposals/3588-stories-as-rooms.md)
|
||||||
|
```
|
||||||
|
- the implementation should provide an `extension NAME on ...` (usually `Client`)
|
||||||
|
- proprietary implementations without MSC should be given a useful name and
|
||||||
|
corresponding, useful documentation comments, e.g. `/lib/msc_extensions/extension_recent_emoji/recent_emoji.dart`
|
||||||
|
- Moreover, all implemented non-spec features should be listed below:
|
||||||
|
|
||||||
|
## Implemented non-spec features
|
||||||
|
|
||||||
|
- MSC 1236 - Widget API V2
|
||||||
|
- `io.element.recent_emoji` - recent emoji sync in account data
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
library extension_recent_emoji;
|
||||||
|
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
/// Syncs recent emojis in account data
|
||||||
|
///
|
||||||
|
/// Keeps recently used emojis stored in account data by
|
||||||
|
///
|
||||||
|
/// ```js
|
||||||
|
/// { // the account data
|
||||||
|
/// "io.element.recent_emoji": {
|
||||||
|
/// "recent_emoji" : {
|
||||||
|
/// "emoji character": n, // number used
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Proprietary extension by New Vector Ltd.
|
||||||
|
extension RecentEmojiExtension on Client {
|
||||||
|
/// returns the recently used emojis from the account data
|
||||||
|
///
|
||||||
|
/// There's no corresponding standard or MSC, it's just the reverse-engineered
|
||||||
|
/// API from New Vector Ltd.
|
||||||
|
Map<String, int> get recentEmojis => Map.fromEntries(
|
||||||
|
(accountData['io.element.recent_emoji']?.content['recent_emoji']
|
||||||
|
as List<dynamic>? ??
|
||||||
|
[])
|
||||||
|
.map(
|
||||||
|
(e) => MapEntry(e[0] as String, e[1] as int),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// +1 the stated emoji in the account data
|
||||||
|
Future<void> addRecentEmoji(String emoji) async {
|
||||||
|
final data = recentEmojis;
|
||||||
|
if (data.containsKey(emoji)) {
|
||||||
|
data[emoji] = data[emoji]! + 1;
|
||||||
|
} else {
|
||||||
|
data[emoji] = 1;
|
||||||
|
}
|
||||||
|
return setRecentEmojiData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the raw recent emoji account data. Use [addRecentEmoji] instead
|
||||||
|
Future<void> setRecentEmojiData(Map<String, int> data) async {
|
||||||
|
final content = List.from(data.entries.map((e) => [e.key, e.value]));
|
||||||
|
return setAccountData(
|
||||||
|
userID!, 'io.element.recent_emoji', {'recent_emoji': content});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
library msc_1236_widgets;
|
||||||
|
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
export 'src/widget.dart';
|
||||||
|
|
||||||
|
extension MatrixWidgets on Room {
|
||||||
|
/// Returns all present Widgets in the room.
|
||||||
|
List<MatrixWidget> get widgets => {
|
||||||
|
...states['m.widget'] ?? states['im.vector.modular.widgets'] ?? {},
|
||||||
|
}.values.expand((e) {
|
||||||
|
try {
|
||||||
|
return [MatrixWidget.fromJson(e.content, this)];
|
||||||
|
} catch (_) {
|
||||||
|
return <MatrixWidget>[];
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
Future<String> addWidget(MatrixWidget widget) {
|
||||||
|
final user = client.userID;
|
||||||
|
final widgetId =
|
||||||
|
widget.name!.toLowerCase().replaceAll(RegExp(r'\W'), '_') + '_' + user!;
|
||||||
|
|
||||||
|
final json = widget.toJson();
|
||||||
|
json['creatorUserId'] = user;
|
||||||
|
json['id'] = widgetId;
|
||||||
|
return client.setRoomStateWithKey(
|
||||||
|
id,
|
||||||
|
'im.vector.modular.widgets',
|
||||||
|
widgetId,
|
||||||
|
json,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> deleteWidget(String widgetId) {
|
||||||
|
return client.setRoomStateWithKey(
|
||||||
|
id,
|
||||||
|
'im.vector.modular.widgets',
|
||||||
|
widgetId,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
import 'package:matrix/src/room.dart';
|
||||||
|
|
||||||
|
class MatrixWidget {
|
||||||
|
final Room room;
|
||||||
|
final String? creatorUserId;
|
||||||
|
final Map<String, dynamic>? data;
|
||||||
|
final String? id;
|
||||||
|
final String? name;
|
||||||
|
final String type;
|
||||||
|
|
||||||
|
/// use [buildWidgetUrl] instead
|
||||||
|
final String url;
|
||||||
|
final bool waitForIframeLoad;
|
||||||
|
|
||||||
|
MatrixWidget({
|
||||||
|
required this.room,
|
||||||
|
this.creatorUserId,
|
||||||
|
this.data = const {},
|
||||||
|
this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.type,
|
||||||
|
required this.url,
|
||||||
|
this.waitForIframeLoad = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MatrixWidget.fromJson(Map<String, dynamic> json, Room room) =>
|
||||||
|
MatrixWidget(
|
||||||
|
room: room,
|
||||||
|
creatorUserId:
|
||||||
|
json.containsKey('creatorUserId') ? json['creatorUserId'] : null,
|
||||||
|
data: json.containsKey('data') ? json['data'] : {},
|
||||||
|
id: json.containsKey('id') ? json['id'] : null,
|
||||||
|
name: json['name'],
|
||||||
|
type: json['type'],
|
||||||
|
url: json['url'],
|
||||||
|
waitForIframeLoad: json.containsKey('waitForIframeLoad')
|
||||||
|
? json['waitForIframeLoad']
|
||||||
|
: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// creates an `m.etherpad` [MatrixWidget]
|
||||||
|
factory MatrixWidget.etherpad(Room room, String name, Uri url) =>
|
||||||
|
MatrixWidget(
|
||||||
|
room: room,
|
||||||
|
name: name,
|
||||||
|
type: 'm.etherpad',
|
||||||
|
url: url.toString(),
|
||||||
|
data: {
|
||||||
|
'url': url.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/// creates an `m.jitsi` [MatrixWidget]
|
||||||
|
factory MatrixWidget.jitsi(Room room, String name, Uri url,
|
||||||
|
{bool isAudioOnly = false}) =>
|
||||||
|
MatrixWidget(
|
||||||
|
room: room,
|
||||||
|
name: name,
|
||||||
|
type: 'm.jitsi',
|
||||||
|
url: url.toString(),
|
||||||
|
data: {
|
||||||
|
'domain': url.host,
|
||||||
|
'conferenceId': url.pathSegments.last,
|
||||||
|
'isAudioOnly': isAudioOnly,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/// creates an `m.video` [MatrixWidget]
|
||||||
|
factory MatrixWidget.video(Room room, String name, Uri url) => MatrixWidget(
|
||||||
|
room: room,
|
||||||
|
name: name,
|
||||||
|
type: 'm.video',
|
||||||
|
url: url.toString(),
|
||||||
|
data: {
|
||||||
|
'url': url.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/// creates an `m.custom` [MatrixWidget]
|
||||||
|
factory MatrixWidget.custom(Room room, String name, Uri url) => MatrixWidget(
|
||||||
|
room: room,
|
||||||
|
name: name,
|
||||||
|
type: 'm.custom',
|
||||||
|
url: url.toString(),
|
||||||
|
data: {
|
||||||
|
'url': url.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<Uri> buildWidgetUrl() async {
|
||||||
|
// See https://github.com/matrix-org/matrix-doc/issues/1236 for a
|
||||||
|
// description, specifically the section
|
||||||
|
// `What does the other stuff in content mean?`
|
||||||
|
final userProfile = await room.client.ownProfile;
|
||||||
|
var parsedUri = url;
|
||||||
|
|
||||||
|
// a key-value map with the strings to be replaced
|
||||||
|
final replaceMap = {
|
||||||
|
r'$matrix_user_id': userProfile.userId,
|
||||||
|
r'$matrix_room_id': room.id,
|
||||||
|
r'$matrix_display_name': userProfile.displayName ?? '',
|
||||||
|
r'$matrix_avatar_url': userProfile.avatarUrl?.toString() ?? '',
|
||||||
|
// removing potentially dangerous keys containing anything but
|
||||||
|
// `[a-zA-Z0-9_-]` as well as non string values
|
||||||
|
if (data != null)
|
||||||
|
...Map.from(data!)
|
||||||
|
..removeWhere((key, value) =>
|
||||||
|
!RegExp(r'^[\w-]+$').hasMatch(key) || !value is String)
|
||||||
|
..map((key, value) => MapEntry('\$key', value)),
|
||||||
|
};
|
||||||
|
|
||||||
|
replaceMap.forEach((key, value) {
|
||||||
|
parsedUri = parsedUri.replaceAll(key, Uri.encodeComponent(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Uri.parse(parsedUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'creatorUserId': creatorUserId,
|
||||||
|
'data': data,
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'type': type,
|
||||||
|
'url': url,
|
||||||
|
'waitForIframeLoad': waitForIframeLoad,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -322,6 +322,15 @@ void main() {
|
||||||
await matrix.setAvatar(testFile);
|
await matrix.setAvatar(testFile);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('recentEmoji', () async {
|
||||||
|
final client = await getClient();
|
||||||
|
final emojis = client.recentEmojis;
|
||||||
|
expect(emojis.isEmpty, isTrue);
|
||||||
|
|
||||||
|
await client.addRecentEmoji('🦙');
|
||||||
|
expect(client.recentEmojis['🦙'], 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('setMuteAllPushNotifications', () async {
|
test('setMuteAllPushNotifications', () async {
|
||||||
await matrix.setMuteAllPushNotifications(false);
|
await matrix.setMuteAllPushNotifications(false);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2121,6 +2121,8 @@ class FakeMatrixApi extends MockClient {
|
||||||
'/client/unstable/room_keys/version': (var reqI) => {'version': '5'},
|
'/client/unstable/room_keys/version': (var reqI) => {'version': '5'},
|
||||||
},
|
},
|
||||||
'PUT': {
|
'PUT': {
|
||||||
|
'/client/r0/user/${Uri.encodeComponent('@alice:example.com')}/account_data/io.element.recent_emoji}':
|
||||||
|
(var req) => {},
|
||||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/m.ignored_user_list':
|
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/m.ignored_user_list':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/r0/presence/${Uri.encodeComponent('@alice:example.com')}/status':
|
'/client/r0/presence/${Uri.encodeComponent('@alice:example.com')}/status':
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue