From cafd639c24dbcd2398271845aefbade283a8f279 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 3 Oct 2020 12:31:29 +0200 Subject: [PATCH] feat: Enhance emote experience --- CHANGELOG.md | 7 +- lib/components/input_bar.dart | 20 +--- lib/l10n/intl_en.arb | 12 ++- lib/views/chat_details.dart | 31 ++++-- lib/views/settings_emotes.dart | 120 ++++++++++++++++++++---- lib/views/settings_multiple_emotes.dart | 74 +++++++++++++++ pubspec.lock | 56 +++++------ pubspec.yaml | 2 +- 8 files changed, 252 insertions(+), 70 deletions(-) create mode 100644 lib/views/settings_multiple_emotes.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 46ee3b8..e4d83a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# Version 0.19.0 - 2020-??-?? +# Version 0.20.0 - 2020-??-?? +### Features +- Add ability to enable / disable emotes globally +- Add ability to manage emote packs with different state keys + +# Version 0.19.0 - 2020-09-21 ### Features - Implemented ignore list - Jump to events in timeline: When tapping on a reply and when tapping a matrix.to link diff --git a/lib/components/input_bar.dart b/lib/components/input_bar.dart index 18107c7..3daebaa 100644 --- a/lib/components/input_bar.dart +++ b/lib/components/input_bar.dart @@ -237,22 +237,10 @@ class InputBar extends StatelessWidget { } if (insertText.isNotEmpty && startText.isNotEmpty) { controller.text = startText + afterText; - if (startText == insertText) { - // stupid fix for now - FocusScope.of(context).requestFocus(FocusNode()); - Future.delayed(Duration(milliseconds: 1)).then((res) { - focusNode.requestFocus(); - controller.selection = TextSelection( - baseOffset: startText.length, - extentOffset: startText.length, - ); - }); - } else { - controller.selection = TextSelection( - baseOffset: startText.length, - extentOffset: startText.length, - ); - } + controller.selection = TextSelection( + baseOffset: startText.length, + extentOffset: startText.length, + ); } } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index cd75f56..5ac25ce 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -529,11 +529,21 @@ "type": "text", "placeholders": {} }, + "emotePacks": "Emote packs for room", + "@emotePacks": { + "type": "text", + "placeholders": {} + }, "emptyChat": "Empty chat", "@emptyChat": { "type": "text", "placeholders": {} }, + "enableEmotesGlobally": "Enable emote pack globally", + "@enableEmotesGlobally": { + "type": "text", + "placeholders": {} + }, "enableEncryptionWarning": "You won't be able to disable the encryption anymore. Are you sure?", "@enableEncryptionWarning": { "type": "text", @@ -1708,4 +1718,4 @@ "type": "text", "placeholders": {} } -} \ No newline at end of file +} diff --git a/lib/views/chat_details.dart b/lib/views/chat_details.dart index a60f81c..fd4e423 100644 --- a/lib/views/chat_details.dart +++ b/lib/views/chat_details.dart @@ -19,6 +19,7 @@ import 'package:matrix_link_text/link_text.dart'; import 'package:memoryfilepicker/memoryfilepicker.dart'; import './settings_emotes.dart'; +import './settings_multiple_emotes.dart'; import '../utils/url_launcher.dart'; class ChatDetails extends StatefulWidget { @@ -285,13 +286,31 @@ class _ChatDetailsState extends State { child: Icon(Icons.insert_emoticon), ), title: Text(L10n.of(context).emoteSettings), - onTap: () async => + onTap: () async { + // okay, we need to test if there are any emote state events other than the default one + // if so, we need to be directed to a selection screen for which pack we want to look at + // otherwise, we just open the normal one. + if ((widget.room.states + .states['im.ponies.room_emotes'] ?? + {}) + .keys + .any((String s) => s.isNotEmpty)) { await Navigator.of(context).push( - AppRoute.defaultRoute( - context, - EmotesSettingsView(room: widget.room), - ), - ), + AppRoute.defaultRoute( + context, + MultipleEmotesSettingsView( + room: widget.room), + ), + ); + } else { + await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + EmotesSettingsView(room: widget.room), + ), + ); + } + }, ), PopupMenuButton( child: ListTile( diff --git a/lib/views/settings_emotes.dart b/lib/views/settings_emotes.dart index 78af06d..1f088e1 100644 --- a/lib/views/settings_emotes.dart +++ b/lib/views/settings_emotes.dart @@ -14,23 +14,25 @@ import 'chat_list.dart'; class EmotesSettingsView extends StatelessWidget { final Room room; + final String stateKey; - EmotesSettingsView({this.room}); + EmotesSettingsView({this.room, this.stateKey}); @override Widget build(BuildContext context) { return AdaptivePageLayout( primaryPage: FocusPage.SECOND, firstScaffold: ChatList(), - secondScaffold: EmotesSettings(room: room), + secondScaffold: EmotesSettings(room: room, stateKey: stateKey), ); } } class EmotesSettings extends StatefulWidget { final Room room; + final String stateKey; - EmotesSettings({this.room}); + EmotesSettings({this.room, this.stateKey}); @override _EmotesSettingsState createState() => _EmotesSettingsState(); @@ -59,21 +61,42 @@ class _EmotesSettingsState extends State { // be sure to preserve any data not in "short" Map content; if (widget.room != null) { - content = widget.room.getState('im.ponies.room_emotes')?.content ?? + content = widget.room + .getState('im.ponies.room_emotes', widget.stateKey ?? '') + ?.content ?? {}; } else { content = client.accountData['im.ponies.user_emotes']?.content ?? {}; } debugPrint(content.toString()); - content['short'] = {}; - for (final emote in emotes) { - content['short'][emote.emote] = emote.mxc; + if (!(content['emoticons'] is Map)) { + content['emoticons'] = {}; } + // add / update changed emotes + final allowedShortcodes = {}; + for (final emote in emotes) { + allowedShortcodes.add(emote.emote); + if (!(content['emoticons'][emote.emote] is Map)) { + content['emoticons'][emote.emote] = {}; + } + content['emoticons'][emote.emote]['url'] = emote.mxc; + } + // remove emotes no more needed + // we make the iterator .toList() here so that we don't get into trouble modifying the very + // thing we are iterating over + for (final shortcode in content['emoticons'].keys.toList()) { + if (!allowedShortcodes.contains(shortcode)) { + content['emoticons'].remove(shortcode); + } + } + // remove the old "short" key + content.remove('short'); debugPrint(content.toString()); if (widget.room != null) { await SimpleDialogs(context).tryRequestWithLoadingDialog( - client.sendState(widget.room.id, 'im.ponies.room_emotes', content), + client.sendState(widget.room.id, 'im.ponies.room_emotes', content, + widget.stateKey ?? ''), ); } else { await SimpleDialogs(context).tryRequestWithLoadingDialog( @@ -82,6 +105,43 @@ class _EmotesSettingsState extends State { } } + Future _setIsGloballyActive(BuildContext context, bool active) async { + if (widget.room == null) { + return; + } + final client = Matrix.of(context).client; + final content = client.accountData['im.ponies.emote_rooms']?.content ?? + {}; + if (active) { + if (!(content['rooms'] is Map)) { + content['rooms'] = {}; + } + if (!(content['rooms'][widget.room.id] is Map)) { + content['rooms'][widget.room.id] = {}; + } + if (!(content['rooms'][widget.room.id][widget.stateKey ?? ''] is Map)) { + content['rooms'][widget.room.id] + [widget.stateKey ?? ''] = {}; + } + } else if (content['rooms'] is Map && + content['rooms'][widget.room.id] is Map) { + content['rooms'][widget.room.id].remove(widget.stateKey ?? ''); + } + // and save + await SimpleDialogs(context).tryRequestWithLoadingDialog( + client.setAccountData(client.userID, 'im.ponies.emote_rooms', content), + ); + } + + bool isGloballyActive(Client client) => + widget.room != null && + client.accountData['im.ponies.emote_rooms']?.content is Map && + client.accountData['im.ponies.emote_rooms'].content['rooms'] is Map && + client.accountData['im.ponies.emote_rooms'].content['rooms'] + [widget.room.id] is Map && + client.accountData['im.ponies.emote_rooms'].content['rooms'] + [widget.room.id][widget.stateKey ?? ''] is Map; + bool get readonly => widget.room == null ? false : !(widget.room.canSendEvent('im.ponies.room_emotes')); @@ -93,16 +153,31 @@ class _EmotesSettingsState extends State { emotes = <_EmoteEntry>[]; Map emoteSource; if (widget.room != null) { - emoteSource = widget.room.getState('im.ponies.room_emotes')?.content; + emoteSource = widget.room + .getState('im.ponies.room_emotes', widget.stateKey ?? '') + ?.content; } else { emoteSource = client.accountData['im.ponies.user_emotes']?.content; } - if (emoteSource != null && emoteSource['short'] is Map) { - emoteSource['short'].forEach((key, value) { - if (key is String && value is String && value.startsWith('mxc://')) { - emotes.add(_EmoteEntry(emote: key, mxc: value)); - } - }); + if (emoteSource != null) { + if (emoteSource['emoticons'] is Map) { + emoteSource['emoticons'].forEach((key, value) { + if (key is String && + value is Map && + value['url'] is String && + value['url'].startsWith('mxc://')) { + emotes.add(_EmoteEntry(emote: key, mxc: value['url'])); + } + }); + } else if (emoteSource['short'] is Map) { + emoteSource['short'].forEach((key, value) { + if (key is String && + value is String && + value.startsWith('mxc://')) { + emotes.add(_EmoteEntry(emote: key, mxc: value)); + } + }); + } } } return Scaffold( @@ -166,7 +241,6 @@ class _EmotesSettingsState extends State { size: 32.0, ), onTap: () async { - debugPrint('blah'); if (newEmoteController.text == null || newEmoteController.text.isEmpty || newMxcController.text == null || @@ -204,7 +278,19 @@ class _EmotesSettingsState extends State { vertical: 8.0, ), ), - if (!readonly) + if (widget.room != null) + ListTile( + title: Text(L10n.of(context).enableEmotesGlobally), + trailing: Switch( + value: isGloballyActive(client), + activeColor: Theme.of(context).primaryColor, + onChanged: (bool newValue) async { + await _setIsGloballyActive(context, newValue); + setState(() => null); + }, + ), + ), + if (!readonly || widget.room != null) Divider( height: 2, thickness: 2, diff --git a/lib/views/settings_multiple_emotes.dart b/lib/views/settings_multiple_emotes.dart new file mode 100644 index 0000000..76aa358 --- /dev/null +++ b/lib/views/settings_multiple_emotes.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import '../components/adaptive_page_layout.dart'; +import '../utils/app_route.dart'; +import 'chat_list.dart'; +import 'settings_emotes.dart'; + +class MultipleEmotesSettingsView extends StatelessWidget { + final Room room; + + MultipleEmotesSettingsView({this.room}); + + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList(), + secondScaffold: MultipleEmotesSettings(room: room), + ); + } +} + +class MultipleEmotesSettings extends StatelessWidget { + final Room room; + + MultipleEmotesSettings({this.room}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context).emotePacks), + ), + body: StreamBuilder( + stream: room.onUpdate.stream, + builder: (context, snapshot) { + final packs = + room.states.states['im.ponies.room_emotes'] ?? {}; + if (!packs.containsKey('')) { + packs[''] = null; + } + final keys = packs.keys.toList(); + keys.sort(); + return ListView.separated( + separatorBuilder: (BuildContext context, int i) => Container(), + itemCount: keys.length, + itemBuilder: (BuildContext context, int i) { + final event = packs[keys[i]]; + var packName = keys[i].isNotEmpty ? keys[i] : 'Default Pack'; + if (event != null && event.content['pack'] is Map) { + if (event.content['pack']['displayname'] is String) { + packName = event.content['pack']['displayname']; + } else if (event.content['pack']['name'] is String) { + packName = event.content['pack']['name']; + } + } + return ListTile( + title: Text(packName), + onTap: () async { + await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + EmotesSettingsView(room: room, stateKey: keys[i]), + ), + ); + }, + ); + }); + }, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 87bfa7d..ae41de7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.1" + version: "2.4.2" base58check: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.0.0" bot_toast: dependency: "direct main" description: @@ -91,14 +91,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.0.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.1.3" cli_util: dependency: transitive description: @@ -112,14 +112,14 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.0.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.3" + version: "1.14.13" convert: dependency: transitive description: @@ -175,13 +175,13 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.1.0" famedlysdk: dependency: "direct main" description: path: "." - ref: "5019ebfeb56f0789ab4cc8d27ccda663156b5d68" - resolved-ref: "5019ebfeb56f0789ab4cc8d27ccda663156b5d68" + ref: "84cc925b08e97098d00c54fff9c1244f91055de3" + resolved-ref: "84cc925b08e97098d00c54fff9c1244f91055de3" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" @@ -456,7 +456,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3-nullsafety.1" + version: "0.6.2" localstorage: dependency: "direct main" description: @@ -484,7 +484,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.1" + version: "0.12.8" matrix_file_e2ee: dependency: transitive description: @@ -512,7 +512,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.1.8" mime: dependency: transitive description: @@ -605,7 +605,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.1" + version: "1.7.0" path_provider: dependency: "direct main" description: @@ -647,7 +647,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.1" + version: "1.9.0" petitparser: dependency: transitive description: @@ -689,7 +689,7 @@ packages: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.5.0-nullsafety.1" + version: "1.4.0" process: dependency: transitive description: @@ -792,21 +792,21 @@ packages: name: source_map_stack_trace url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.2" + version: "2.0.0" source_maps: dependency: transitive description: name: source_maps url: "https://pub.dartlang.org" source: hosted - version: "0.10.10-nullsafety.1" + version: "0.10.9" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.2" + version: "1.7.0" sqflite: dependency: "direct main" description: @@ -841,21 +841,21 @@ packages: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.1" + version: "1.9.5" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.0.5" synchronized: dependency: transitive description: @@ -869,35 +869,35 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.1.0" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.16.0-nullsafety.5" + version: "1.15.2" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.2" + version: "0.2.17" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.12-nullsafety.5" + version: "0.3.10" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.2.0" universal_html: dependency: "direct main" description: @@ -981,7 +981,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.0.8" vm_service: dependency: transitive description: @@ -1053,5 +1053,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.10.0-110 <2.11.0" + dart: ">=2.9.0 <3.0.0" flutter: ">=1.20.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index d133ad9..3ce587e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: 5019ebfeb56f0789ab4cc8d27ccda663156b5d68 + ref: 84cc925b08e97098d00c54fff9c1244f91055de3 localstorage: ^3.0.1+4 memoryfilepicker: ^0.1.3