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); | ||||
|     }); | ||||
| 
 | ||||
|     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 { | ||||
|       await matrix.setMuteAllPushNotifications(false); | ||||
|     }); | ||||
|  |  | |||
|  | @ -2121,6 +2121,8 @@ class FakeMatrixApi extends MockClient { | |||
|       '/client/unstable/room_keys/version': (var reqI) => {'version': '5'}, | ||||
|     }, | ||||
|     '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': | ||||
|           (var req) => {}, | ||||
|       '/client/r0/presence/${Uri.encodeComponent('@alice:example.com')}/status': | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue