From f1bdcb21bd5519d5db7c8fa1382f0c5c05fca92c Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sat, 29 Mar 2025 21:11:25 +1030 Subject: [PATCH 1/6] refactoring emoticon code --- .../components/emoticon/emoji_pack.dart | 37 +- .../client/components/emoticon/emoticon.dart | 16 +- .../emoticon/emoticon_component.dart | 2 +- .../components/emoticon/matrix_emoticon.dart | 50 +- .../emoticon/matrix_emoticon_component.dart | 135 ++- .../emoticon/matrix_emoticon_pack.dart | 247 ++--- .../matrix_emoticon_state_manager.dart | 17 +- ...matrix_timeline_event_mixin_reactions.dart | 3 +- .../atoms/rich_text/matrix_html_parser.dart | 5 +- commet/lib/ui/molecules/account_selector.dart | 23 +- .../add_space_or_room_view.dart | 1 + .../matrix_room_address_settings_view.dart | 19 +- .../account_emoji/account_emoji_tab.dart | 17 +- .../room_emoji_pack_settings_page.dart | 31 +- .../room_emoji_pack_settings_view.dart | 941 ++++++------------ .../space/space_emoji_pack_settings.dart | 34 +- commet/lib/utils/emoji/unicode_emoji.dart | 66 +- tiamat/lib/atoms/dropdown_selector.dart | 143 +-- tiamat/lib/atoms/icon_toggle.dart | 4 +- tiamat/pubspec.lock | 24 +- 20 files changed, 632 insertions(+), 1183 deletions(-) diff --git a/commet/lib/client/components/emoticon/emoji_pack.dart b/commet/lib/client/components/emoticon/emoji_pack.dart index 1ff2caa2b..e7b0fc056 100644 --- a/commet/lib/client/components/emoticon/emoji_pack.dart +++ b/commet/lib/client/components/emoticon/emoji_pack.dart @@ -24,31 +24,30 @@ abstract class EmoticonPack { ImageProvider? get image; IconData? get icon; - bool get isStickerPack; - bool get isEmojiPack; - bool get isGloballyAvailable; + EmoticonUsage get usage; Future deleteEmoticon(Emoticon emoticon); - Future renameEmoticon(Emoticon emoticon, String name); - - Future markEmoticonAsSticker(Emoticon emoticon, bool isSticker); - - Future markEmoticonAsEmoji(Emoticon emoticon, bool isEmoji); - - Future markAsEmoji(bool isEmojiPack); - - Future markAsSticker(bool isStickerPack); + Future setPackUsage(EmoticonUsage usage); Emoticon? getByShortcode(String shortcode); - Future addEmoticon( - {required String slug, - String? shortcode, - required Uint8List data, - String? mimeType, - bool? isEmoji, - bool? isSticker}); + Future updateEmoticon({ + String? slug, + String? shortcode, + Uint8List? data, + String? mimeType, + EmoticonUsage? usage, + required Emoticon previous, + }); + + Future addEmoticon({ + required String slug, + String? shortcode, + required Uint8List data, + String? mimeType, + EmoticonUsage usage, + }); Future markAsGlobal(bool isGlobal); diff --git a/commet/lib/client/components/emoticon/emoticon.dart b/commet/lib/client/components/emoticon/emoticon.dart index 773342456..021f880a1 100644 --- a/commet/lib/client/components/emoticon/emoticon.dart +++ b/commet/lib/client/components/emoticon/emoticon.dart @@ -1,15 +1,23 @@ import 'dart:core'; import 'package:flutter/material.dart'; +enum EmoticonUsage { + sticker, + emoji, + all, +} + abstract class Emoticon { ImageProvider? get image; String get slug; String? get shortcode; String get key; - bool get isMarkedEmoji; - bool get isMarkedSticker; + EmoticonUsage get usage; + + bool get isSticker => + usage == EmoticonUsage.sticker || usage == EmoticonUsage.all; - bool get isSticker; - bool get isEmoji; + bool get isEmoji => + usage == EmoticonUsage.emoji || usage == EmoticonUsage.all; } diff --git a/commet/lib/client/components/emoticon/emoticon_component.dart b/commet/lib/client/components/emoticon/emoticon_component.dart index df75a9761..dc6f5dfb6 100644 --- a/commet/lib/client/components/emoticon/emoticon_component.dart +++ b/commet/lib/client/components/emoticon/emoticon_component.dart @@ -14,7 +14,7 @@ abstract class EmoticonComponent implements Component { List globalPacks(); List get ownedPacks; bool get canCreatePack; - Stream get onOwnedPackAdded; + Stream get onStateChanged; Future createEmoticonPack(String name, Uint8List? avatarData); Future importEmoticonPack(String name, int avatarIndex, diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart index 79141ed9f..8e7e8dac3 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart @@ -18,34 +18,12 @@ class MatrixEmoticon implements Emoticon { Uri emojiUrl; - late bool _isEmojiPack; - late bool _isStickerPack; - late bool _isMarkedEmoji; - late bool _isMarkedSticker; - - @override - bool get isEmoji => _isEmojiPack || _isMarkedEmoji; - - @override - bool get isSticker => _isStickerPack || _isMarkedSticker; - @override - bool get isMarkedEmoji => _isMarkedEmoji; - - @override - bool get isMarkedSticker => _isMarkedSticker; + EmoticonUsage usage; MatrixEmoticon(this.emojiUrl, matrix.Client client, - {required String shortcode, - bool isEmojiPack = true, - bool isStickerPack = true, - bool isMarkedSticker = false, - bool isMarkedEmoji = false}) { + {required String shortcode, required this.usage}) { _shortcode = shortcode; - _isEmojiPack = isEmojiPack; - _isStickerPack = isStickerPack; - _isMarkedEmoji = isMarkedEmoji; - _isMarkedSticker = isMarkedSticker; _image = MatrixMxcImage(emojiUrl, client, doThumbnail: false); } @@ -53,22 +31,6 @@ class MatrixEmoticon implements Emoticon { _shortcode = shortcode; } - void markAsEmoji(bool value) { - _isMarkedEmoji = value; - } - - void markAsSticker(bool value) { - _isMarkedSticker = value; - } - - void markPackAsEmoji(bool value) { - _isEmojiPack = value; - } - - void markPackAsSticker(bool value) { - _isStickerPack = value; - } - void setImage(MatrixMxcImage image) { _image = image; } @@ -91,4 +53,12 @@ class MatrixEmoticon implements Emoticon { int get hashCode { return key.hashCode; } + + @override + bool get isSticker => + usage == EmoticonUsage.sticker || usage == EmoticonUsage.all; + + @override + bool get isEmoji => + usage == EmoticonUsage.emoji || usage == EmoticonUsage.all; } diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart index 1dd89887d..8bdb1c617 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart @@ -1,13 +1,14 @@ +import 'dart:async'; import 'dart:typed_data'; import 'package:commet/client/components/emoticon/emoji_pack.dart'; +import 'package:commet/client/components/emoticon/emoticon.dart'; import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/client/matrix/components/emoticon/matrix_emoticon_pack.dart'; import 'package:commet/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart'; import 'package:commet/client/matrix/components/emoticon/matrix_import_emoticon_pack_task.dart'; import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/main.dart'; -import 'package:commet/utils/notifying_list.dart'; import 'package:flutter/material.dart'; /// Manages custom emoticon packs from the matrix user account state @@ -15,6 +16,8 @@ class MatrixEmoticonComponent extends EmoticonComponent { static const roomEmotesStateKey = "im.ponies.room_emotes"; static const globalEmoteRoomsStateKey = "im.ponies.emote_rooms"; + final StreamController _onStateChanged = StreamController.broadcast(); + @override bool get canCreatePack => ownedPacks.isEmpty; @@ -27,23 +30,37 @@ class MatrixEmoticonComponent extends EmoticonComponent { MatrixEmoticonStateManager state; - final NotifyingList _packs = - NotifyingList.empty(growable: true); - MatrixEmoticonComponent(this.client, this.state) { - loadFromState(state.getAllStates()); + refreshOwnedPacks(); - state.onStateChanged.listen((event) { - var s = state.getAllStates(); - loadFromState(s); + state.onStateChanged.listen((_) { + refreshOwnedPacks(); + _onStateChanged.add(null); }); } + void refreshOwnedPacks() { + final state = this.state.getAllStates(); + + _ownedPacks = state.entries.where((e) { + final val = e.value; + if (val is Map) { + return val.isNotEmpty; + } else { + return false; + } + }).map((e) { + return MatrixEmoticonPack(this, e.key, e.value); + }).toList(); + } + @override - Stream get onOwnedPackAdded => _packs.onAdd; + Stream get onStateChanged => _onStateChanged.stream; + + List _ownedPacks = List.empty(); @override - List get ownedPacks => _packs; + List get ownedPacks => _ownedPacks; String getDefaultDisplayName() { return "Personal"; @@ -61,29 +78,6 @@ class MatrixEmoticonComponent extends EmoticonComponent { return true; } - void loadFromState(Map newState) { - _packs.removeWhere( - (element) => newState.containsKey(element.identifier) == false); - - for (var key in newState.keys) { - var s = newState[key]; - if (s is! Map) continue; - if (s.isEmpty) { - _packs.removeWhere((element) => element.identifier == key); - continue; - } - - var existing = - _packs.where((element) => element.identifier == key).firstOrNull; - if (existing is MatrixEmoticonPack) { - existing.updateFromState(s); - } else { - var pack = MatrixEmoticonPack(this, key, s); - _packs.add(pack); - } - } - } - @override Future createEmoticonPack(String name, Uint8List? avatarData) async { Uri? avatar; @@ -150,7 +144,6 @@ class MatrixEmoticonComponent extends EmoticonComponent { @override Future deleteEmoticonPack(EmoticonPack pack) { - _packs.remove(pack); var matrixPack = pack as MatrixEmoticonPack; return state.setState(matrixPack.stateKey, {}); } @@ -219,62 +212,58 @@ class MatrixEmoticonComponent extends EmoticonComponent { return state.setState(packKey, content); } - Future renameEmoticon( - String packKey, String emoteName, String newName) async { + Future setPackUsages(String packKey, List? usages) async { var content = state.getState(packKey); - if (content.containsKey('images')) { - var images = content['images'] as Map; - var image = images[emoteName] as Map?; - images.remove(emoteName); + var pack = content['pack'] as Map?; - if (image != null) { - image['display_name'] = newName; - images[newName] = image; - } + if (pack == null) return; - content['images'] = images; - } + pack['usage'] = usages?.isEmpty == true ? null : usages; + content['pack'] = pack; return state.setState(packKey, content); } - Future setEmoticonUsages( - String packKey, String emoteName, List? usages) async { + Future updateEmoticon( + String packKey, + String emoteName, { + Uint8List? data, + String? mimeType, + EmoticonUsage? usage, + required Emoticon previous, + }) async { var content = state.getState(packKey); - if (content.containsKey('images')) { - var images = content['images'] as Map; + var emoteState = content['images'][previous.shortcode!]; - if (images.containsKey(emoteName)) { - var emote = images[emoteName] as Map; - - emote.remove('usage'); - - if (usages != null && usages.isNotEmpty) { - emote['usage'] = usages; - } - - images[emoteName] = emote; - } - - content['images'] = images; + if (usage != null) { + emoteState['usage'] = switch (usage) { + EmoticonUsage.sticker => ["sticker"], + EmoticonUsage.emoji => ["emoticon"], + EmoticonUsage.all => ["emoticon", "sticker"], + }; } - return state.setState(packKey, content); - } - - Future setPackUsages(String packKey, List? usages) async { - var content = state.getState(packKey); + if (data != null) { + Uri url = await client.getMatrixClient().uploadContent(data); + emoteState['url'] = url.toString(); + } - var pack = content['pack'] as Map?; + var pack = {"pack": content['pack'], "images": {}}; - if (pack == null) return; + var keys = content['images'].keys.toList(); - pack['usage'] = usages?.isEmpty == true ? null : usages; - content['pack'] = pack; + // construct a new map this way, to keep ordering :O + for (var key in keys) { + if (key == previous.shortcode) { + pack['images'][emoteName] = emoteState; + } else { + pack['images'][key] = content['images'][key]; + } + } - return state.setState(packKey, content); + await state.setState(packKey, pack); } Future>? createEmoticon( diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart index 5ceb0d145..5a952327d 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart @@ -8,7 +8,6 @@ import 'package:commet/client/matrix/components/emoticon/matrix_room_emoticon_co import 'package:commet/client/matrix/components/emoticon/matrix_space_emoticon_component.dart'; import 'package:commet/client/matrix/extensions/matrix_client_extensions.dart'; import 'package:commet/client/matrix/matrix_mxc_image_provider.dart'; -import 'package:commet/utils/notifying_list.dart'; import 'package:flutter/widgets.dart'; import 'package:fuzzy/fuzzy.dart'; import 'package:matrix/matrix.dart'; @@ -18,116 +17,96 @@ class MatrixEmoticonPack implements EmoticonPack { String stateKey; @override - final NotifyingList emotes = - NotifyingList.empty(growable: true); + List get emotes { + final images = state.tryGetMap>("images"); - late Map shortcodeToEmoticon; + if (images == null) { + return List.empty(); + } - @override - late String displayName; + return images.entries.map((e) { + final shortCode = e.key; + final url = e.value.tryGet("url"); + final usages = e.value.tryGetList("usage"); - @override - ImageProvider? image; + if (url == null) { + throw UnimplementedError; + } - @override - IconData? icon; - - MatrixEmoticonPack( - this.component, - this.stateKey, - Map initialState, - ) { - updateFromState(initialState); + final usage = usagesArrayToUsage(usages); + + return MatrixEmoticon(Uri.parse(url), component.client.getMatrixClient(), + shortcode: shortCode, usage: usage); + }).toList(); } - void updateFromState(Map initialState) { - var info = initialState['pack']; - displayName = info?['display_name'] ?? component.getDefaultDisplayName(); - shortcodeToEmoticon = {}; - if (info?['avatar_url'] != null) { - try { - var uri = Uri.parse(info!['avatar_url']!); - image = MatrixMxcImage(uri, component.client.getMatrixClient()); - } catch (_) {} - } + late Map shortcodeToEmoticon; - image ??= component.getDefaultImage(); - icon = component.getDefaultIcon(); + late Map state; - var images = initialState['images'] as Map?; - if (images == null) return; + @override + String get displayName { + return state + .tryGetMap("pack") + ?.tryGet("display_name") ?? + "Unnamed Pack"; + } - bool isStickerPackCache = isStickerPack; - bool isEmojiPackCache = isEmojiPack; + @override + ImageProvider? get image { + final pack = state.tryGetMap("pack"); - emotes.removeWhere((element) => images.containsKey(element.key) == false); - shortcodeToEmoticon.removeWhere((key, value) => - emotes.any((element) => element.shortcode == key) == false); + final url = pack?.tryGet("avatar_url"); - for (var image in images.keys) { - var url = images[image]['url']; - if (url == null) { - continue; - } - var uri = Uri.parse(url); + if (url != null) { + return MatrixMxcImage(Uri.parse(url), component.client.getMatrixClient()); + } - var usages = images[image]['usage'] as List?; + return null; + } - bool markedSticker = false; - bool markedEmoji = false; - if (usages != null) { - markedSticker = usages.contains("sticker"); - markedEmoji = usages.contains("emoticon"); - } + @override + IconData? get icon { + return component.getDefaultIcon(); + } - var existing = - emotes.where((element) => element.key == image).firstOrNull; - - if (existing != null) { - existing.markAsSticker(markedSticker); - existing.markAsEmoji(markedEmoji); - existing.markPackAsEmoji(isEmojiPackCache); - existing.markPackAsSticker(isStickerPackCache); - - if (uri != existing.emojiUrl) { - existing.emojiUrl = uri; - existing.setImage( - MatrixMxcImage(uri, component.client.getMatrixClient())); - } - } else { - var emote = MatrixEmoticon(uri, component.client.getMatrixClient(), - shortcode: image, - isEmojiPack: isEmojiPackCache, - isStickerPack: isStickerPackCache, - isMarkedEmoji: markedEmoji, - isMarkedSticker: markedSticker); - emotes.add(emote); - - if (emote.shortcode != null) { - shortcodeToEmoticon[emote.shortcode!] = emote; - } - } + MatrixEmoticonPack(this.component, this.stateKey, this.state); + + EmoticonUsage usagesArrayToUsage(List? usages) { + if ((usages?.contains("emoticon") == true) && + (usages?.contains("sticker") == false)) { + return EmoticonUsage.emoji; + } + + if ((usages?.contains("sticker") == true) && + (usages?.contains("emoticon") == false)) { + return EmoticonUsage.sticker; } + + return EmoticonUsage.all; } @override - Future addEmoticon( - {required String slug, - String? shortcode, - required Uint8List data, - String? mimeType, - bool? isEmoji, - bool? isSticker}) async { + Future addEmoticon({ + required String slug, + String? shortcode, + required Uint8List data, + String? mimeType, + EmoticonUsage? usage, + }) async { await component.createEmoticon(identifier, shortcode!, data); } - List? _getUsage() { - var info = - component.state.getState(identifier)['pack'] as Map?; - if (info == null) return null; - - var usage = info.tryGet("usage") as List?; - return usage; + @override + Future updateEmoticon( + {String? slug, + String? shortcode, + Uint8List? data, + String? mimeType, + EmoticonUsage? usage, + required Emoticon previous}) async { + await component.updateEmoticon(identifier, shortcode!, + data: data, usage: usage, previous: previous); } @override @@ -136,33 +115,23 @@ class MatrixEmoticonPack implements EmoticonPack { @override Future deleteEmoticon(Emoticon emoticon) async { await component.deleteEmoticon(identifier, emoticon.shortcode!); - emotes.remove(emoticon); - - if (emoticon.shortcode != null) { - shortcodeToEmoticon.remove(emoticon.shortcode); - } } @override - List get emoji => - emotes.where((element) => element.isEmoji).toList(); + List get emoji => emotes + .where((element) => + [EmoticonUsage.all, EmoticonUsage.emoji].contains(element.usage)) + .toList(); @override - List get stickers => - emotes.where((element) => element.isSticker).toList(); + List get stickers => emotes + .where((element) => + [EmoticonUsage.all, EmoticonUsage.sticker].contains(element.usage)) + .toList(); @override String get identifier => stateKey; - @override - bool get isEmojiPack => _getUsage()?.contains("emoticon") ?? true; - - @override - bool get isGloballyAvailable => component.isGloballyAvailable(identifier); - - @override - bool get isStickerPack => _getUsage()?.contains("sticker") ?? true; - @override Future markAsGlobal(bool isGlobal) async { late Room room; @@ -181,49 +150,24 @@ class MatrixEmoticonPack implements EmoticonPack { } } - @override - Future markAsEmoji(bool isEmojiPack) async { - await component.setPackUsages(identifier, - [if (isEmojiPack) 'emoticon', if (isStickerPack) 'sticker']); + List _emoticonUsageToArray(EmoticonUsage usage) { + final usages = switch (usage) { + EmoticonUsage.sticker => ["sticker"], + EmoticonUsage.emoji => ["emoticon"], + EmoticonUsage.all => ["sticker", "emoticon"], + }; - for (var emote in emotes) { - emote.markPackAsEmoji(isEmojiPack); - } + return usages; } @override - Future markAsSticker(bool isStickerPack) async { - await component.setPackUsages(identifier, - [if (isEmojiPack) 'emoticon', if (isStickerPack) 'sticker']); - - for (var emote in emotes) { - emote.markPackAsSticker(isStickerPack); - } + Future setPackUsage(EmoticonUsage usage) { + final usages = _emoticonUsageToArray(usage); + return component.setPackUsages(identifier, usages); } @override - Future markEmoticonAsEmoji(Emoticon emoticon, bool isEmoji) async { - await component.setEmoticonUsages(identifier, emoticon.shortcode!, - [if (isEmoji) 'emoticon', if (emoticon.isMarkedSticker) 'sticker']); - - (emoticon as MatrixEmoticon).markAsEmoji(isEmoji); - } - - @override - Future markEmoticonAsSticker(Emoticon emoticon, bool isSticker) async { - await component.setEmoticonUsages(identifier, emoticon.shortcode!, - [if (emoticon.isMarkedEmoji) 'emoticon', if (isSticker) 'sticker']); - - (emoticon as MatrixEmoticon).markAsSticker(isSticker); - } - - @override - Stream get onEmoticonAdded => emotes.onAdd; - - @override - Future renameEmoticon(Emoticon emoticon, String name) { - return component.renameEmoticon(identifier, emoticon.shortcode!, name); - } + Stream get onEmoticonAdded => throw UnimplementedError(); @override List getShortcodes() { @@ -255,4 +199,19 @@ class MatrixEmoticonPack implements EmoticonPack { @override String get ownerDisplayName => component.ownerDisplayName; + + @override + bool operator ==(Object other) { + if (other is! MatrixEmoticonPack) return false; + if (other.component != component) return false; + + return (other.stateKey == stateKey && + other.component.state.id == component.state.id); + } + + @override + int get hashCode => stateKey.hashCode; + + @override + EmoticonUsage get usage => throw UnimplementedError(); } diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart index 8df1f1410..d09eeddd6 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart @@ -12,6 +12,8 @@ abstract class MatrixEmoticonStateManager { Future setState(String packKey, Map content); Stream get onStateChanged; + + String get id; } class MatrixEmoticonPersonalStateManager implements MatrixEmoticonStateManager { @@ -33,9 +35,12 @@ class MatrixEmoticonPersonalStateManager implements MatrixEmoticonStateManager { onStateChangedController.add(null); }); - mx.onSync.stream - .where((e) => e.accountData != null) - .listen((_) => onStateChangedController.add(null)); + mx.onSync.stream.where((e) => e.accountData != null).listen((update) { + if (update.accountData?.any((e) => e.type == "im.ponies.user_emotes") == + true) { + onStateChangedController.add(null); + } + }); } @override @@ -62,6 +67,9 @@ class MatrixEmoticonPersonalStateManager implements MatrixEmoticonStateManager { ?.content ?? {}; } + + @override + String get id => client.identifier; } class MatrixEmoticonRoomStateManager implements MatrixEmoticonStateManager { @@ -119,4 +127,7 @@ class MatrixEmoticonRoomStateManager implements MatrixEmoticonStateManager { @override Stream get onStateChanged => onStateChangedController.stream; + + @override + String get id => room.id; } diff --git a/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart b/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart index 887b1a109..17e0523ee 100644 --- a/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart +++ b/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart @@ -70,7 +70,8 @@ mixin MatrixTimelineEventReactions on MatrixTimelineEvent if (key.startsWith("mxc://")) { return MatrixEmoticon(Uri.parse(key), timeline.room.client, - shortcode: event.content.tryGet("shortcode") ?? ""); + shortcode: event.content.tryGet("shortcode") ?? "", + usage: EmoticonUsage.all); } return UnicodeEmoticon(key, shortcode: content['shortcode'] as String?); diff --git a/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart b/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart index 9a6dd948a..a4eca085e 100644 --- a/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart +++ b/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart @@ -1,3 +1,4 @@ +import 'package:commet/client/components/emoticon/emoticon.dart'; import 'package:commet/client/matrix/components/emoticon/matrix_emoticon.dart'; import 'package:commet/client/matrix/matrix_mxc_image_provider.dart'; import 'package:commet/ui/atoms/code_block.dart'; @@ -218,7 +219,9 @@ class MatrixEmoticonHtmlExtension extends HtmlExtension { return WidgetSpan( child: EmojiWidget( - MatrixEmoticon(uri, client, shortcode: context.attributes["alt"] ?? ""), + MatrixEmoticon(uri, client, + shortcode: context.attributes["alt"] ?? "", + usage: EmoticonUsage.emoji), height: emojiSize, )); } diff --git a/commet/lib/ui/molecules/account_selector.dart b/commet/lib/ui/molecules/account_selector.dart index e1b0fa2dc..e3ff21a97 100644 --- a/commet/lib/ui/molecules/account_selector.dart +++ b/commet/lib/ui/molecules/account_selector.dart @@ -4,18 +4,35 @@ import 'package:flutter/widgets.dart'; import 'package:tiamat/tiamat.dart' as tiamat; -class AccountSelector extends StatelessWidget { +class AccountSelector extends StatefulWidget { const AccountSelector(this.clients, {super.key, this.onClientSelected}); final List clients; final Function(Client client)? onClientSelected; + @override + State createState() => _AccountSelectorState(); +} + +class _AccountSelectorState extends State { + late Client selectedClient; + + @override + void initState() { + selectedClient = widget.clients.first; + super.initState(); + } + @override Widget build(BuildContext context) { return tiamat.DropdownSelector( - items: clients, + items: widget.clients, + value: selectedClient, itemHeight: 65, onItemSelected: (item) { - onClientSelected?.call(item); + setState(() { + selectedClient = item; + }); + widget.onClientSelected?.call(item); }, itemBuilder: (item) { return Padding( diff --git a/commet/lib/ui/pages/add_space_or_room/add_space_or_room_view.dart b/commet/lib/ui/pages/add_space_or_room/add_space_or_room_view.dart index 6eacd0a89..28195ffe2 100644 --- a/commet/lib/ui/pages/add_space_or_room/add_space_or_room_view.dart +++ b/commet/lib/ui/pages/add_space_or_room/add_space_or_room_view.dart @@ -275,6 +275,7 @@ class _AddSpaceOrRoomViewState extends State { child: tiamat.DropdownSelector( items: const [RoomVisibility.private, RoomVisibility.public], itemHeight: 90, + value: visibility, onItemSelected: (item) { setState(() { visibility = item; diff --git a/commet/lib/ui/pages/matrix/room_address_settings/matrix_room_address_settings_view.dart b/commet/lib/ui/pages/matrix/room_address_settings/matrix_room_address_settings_view.dart index dc496c7d0..deb9486c4 100644 --- a/commet/lib/ui/pages/matrix/room_address_settings/matrix_room_address_settings_view.dart +++ b/commet/lib/ui/pages/matrix/room_address_settings/matrix_room_address_settings_view.dart @@ -44,19 +44,11 @@ class MatrixRoomAddressSettingsView extends StatefulWidget { class _MatrixRoomAddressSettingsViewState extends State { - int? mainAliasIndex; String? errorMessage; StreamSubscription? subscription; - GlobalKey stateKey = GlobalKey(); @override void initState() { - if (widget.mainAlias != null) { - mainAliasIndex = widget.knownAliases.indexOf(widget.mainAlias!); - if (mainAliasIndex == -1) { - mainAliasIndex = null; - } - } subscription = widget.mainAliasChangedStream.listen(onMainAliasChanged); super.initState(); @@ -69,9 +61,7 @@ class _MatrixRoomAddressSettingsViewState } void onMainAliasChanged(String? value) { - stateKey.currentState?.setState(() { - stateKey.currentState?.value = value; - }); + setState(() {}); } @override @@ -219,21 +209,20 @@ class _MatrixRoomAddressSettingsViewState crossAxisAlignment: CrossAxisAlignment.start, children: [ tiamat.DropdownSelector( - key: stateKey, items: widget.knownAliases, - defaultIndex: mainAliasIndex, + value: widget.mainAlias, itemHeight: 60, hint: tiamat.Text.labelLow(widget.canChangeMainAlias ? "Select a main room address" : "This room does not have a set main alias"), - onItemSelected: (item) => widget.setMainAlias(item), + onItemSelected: (item) => widget.setMainAlias(item!), itemBuilder: (item) { return Row( children: [ Flexible( child: Padding( padding: const EdgeInsets.all(8.0), - child: tiamat.Text.label(item), + child: tiamat.Text.label(item!), ), ), if (item == widget.mainAlias) const TinyPill("Main"), diff --git a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart index a8415d591..fb7ded1a1 100644 --- a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart +++ b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart @@ -62,14 +62,10 @@ class _AccountEmojiTabState extends State { // I dont love using a key here, is there a better way to do this? i dont know key: ValueKey("account_emoji_editor_key_${selectedClient!.identifier}"), children: [ - RoomEmojiPackSettingsView(component!.ownedPacks, - createNewPack: createPack, - defaultExpanded: true, - canCreatePack: component!.canCreatePack, - deleteEmoticon: deleteEmoticon, - deletePack: deletePack, - renameEmoticon: renameEmoticon, - onPackCreated: component!.onOwnedPackAdded), + RoomEmojiPackSettingsView( + component: component!, + editable: true, + ), const SizedBox( height: 5, ), @@ -83,11 +79,6 @@ class _AccountEmojiTabState extends State { return component!.createEmoticonPack(name, avatarData); } - Future renameEmoticon( - EmoticonPack pack, Emoticon emoticon, String name) { - return pack.renameEmoticon(emoticon, name); - } - Future deleteEmoticon(EmoticonPack pack, Emoticon emoticon) { return pack.deleteEmoticon(emoticon); } diff --git a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart index cc645bdef..c7e6453d7 100644 --- a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart @@ -28,37 +28,8 @@ class _RoomEmojiPackSettingsPageState extends State { @override Widget build(BuildContext context) { return RoomEmojiPackSettingsView( - component.ownedPacks, - createNewPack: createNewPack, - onPackCreated: component.onOwnedPackAdded, - deletePack: deletePack, - deleteEmoticon: deleteEmoticon, - canCreatePack: component.canCreatePack, - renameEmoticon: renameEmoticon, + component: component, editable: widget.room.permissions.canEditRoomEmoticons, - importPack: importPack, ); } - - Future createNewPack(String name, Uint8List? avatarData) async { - await component.createEmoticonPack(name, avatarData); - } - - Future deletePack(EmoticonPack pack) async { - await component.deleteEmoticonPack(pack); - } - - Future deleteEmoticon(EmoticonPack pack, Emoticon emoticon) async { - await pack.deleteEmoticon(emoticon); - } - - Future renameEmoticon( - EmoticonPack pack, Emoticon emoticon, String name) async { - await pack.renameEmoticon(emoticon, name); - } - - Future importPack(String name, int avatarIndex, List names, - List imageDatas) async { - component.importEmoticonPack(name, avatarIndex, names, imageDatas); - } } diff --git a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart index 9923516ed..274d841b9 100644 --- a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart +++ b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart @@ -1,615 +1,239 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:commet/ui/atoms/emoji_widget.dart'; -import 'package:commet/ui/molecules/editable_label.dart'; +import 'package:commet/client/components/emoticon/emoji_pack.dart'; +import 'package:commet/client/components/emoticon/emoticon.dart'; +import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/ui/molecules/image_picker.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; -import 'package:commet/ui/pages/settings/categories/room/emoji_packs/bulk_import_view.dart'; -import 'package:commet/utils/common_animation.dart'; -import 'package:commet/client/components/emoticon/emoticon.dart'; -import 'package:commet/client/components/emoticon/emoji_pack.dart'; import 'package:commet/utils/common_strings.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tiamat/atoms/circle_button.dart'; import 'package:tiamat/tiamat.dart' as tiamat; import 'package:path/path.dart' as path; class RoomEmojiPackSettingsView extends StatefulWidget { - final List packs; - final Stream? onPackCreated; - final Future Function(String name, Uint8List? avatarData)? - createNewPack; - final Future Function(EmoticonPack pack)? deletePack; - final Future Function(EmoticonPack pack, Emoticon emoticon)? - deleteEmoticon; - - final Future Function( - EmoticonPack pack, Emoticon emoticon, String name)? renameEmoticon; - - final Function(String name, int avatarIndex, List names, - List imageDatas)? importPack; - + const RoomEmojiPackSettingsView( + {required this.component, this.editable = true, super.key}); + final EmoticonComponent component; final bool editable; - final bool canCreatePack; - final bool defaultExpanded; - final bool showBulkImport; - const RoomEmojiPackSettingsView(this.packs, - {this.createNewPack, - super.key, - this.onPackCreated, - this.deletePack, - this.editable = true, - this.canCreatePack = true, - this.defaultExpanded = false, - this.showBulkImport = true, - this.importPack, - this.renameEmoticon, - this.deleteEmoticon}); - @override State createState() => _RoomEmojiPackSettingsViewState(); } class _RoomEmojiPackSettingsViewState extends State { - int itemCount = 0; - final GlobalKey _listKey = GlobalKey(); - StreamSubscription? onItemAddedSubscription; - String get promptCreateEmoticonPack => Intl.message("Create pack", - name: "promptCreateEmoticonPack", - desc: "Prompt to create a new emoticon pack, for emoji or stickers"); + late List packs; - String get promptImportPack => Intl.message("Import pack", - name: "promptImportPack", - desc: "Prompt to import a set of emoticons from an existing pack"); + StreamSubscription? sub; @override void initState() { - itemCount = widget.packs.length; - onItemAddedSubscription = widget.onPackCreated?.listen(onPackAdded); super.initState(); + + sub = widget.component.onStateChanged.listen((_) => setState(() { + packs = widget.component.ownedPacks; + })); + + packs = widget.component.ownedPacks; } @override void dispose() { - onItemAddedSubscription?.cancel(); + sub?.cancel; super.dispose(); } - void onPackAdded(int index) { - setState(() { - _listKey.currentState?.insertItem(index); - }); - } - @override Widget build(BuildContext context) { return Column( - mainAxisSize: MainAxisSize.min, - children: [ - AnimatedList( - initialItemCount: itemCount, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - key: _listKey, - itemBuilder: (context, index, animation) { - return SizeTransition( - sizeFactor: CommonAnimations.easeOut(animation), - child: EmojiPackEditor( - widget.packs[index], - deletePack: () => deletePack(index), - deleteEmoticon: (emoticon) => deleteEmoticon(index, emoticon), - editable: widget.editable, - initiallyExpanded: widget.defaultExpanded, - renameEmoticon: (emoticon, name) => - renameEmoticon(index, emoticon, name), - ), - ); - }, - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (widget.editable && - widget.canCreatePack && - widget.showBulkImport) - Padding( - padding: const EdgeInsets.all(4.0), - child: Align( - alignment: Alignment.centerRight, - child: CircleButton( - radius: 20, - icon: Icons.auto_awesome_motion, - onPressed: promptBulkImport, - ), - ), - ), - if (widget.editable && widget.canCreatePack) - Padding( - padding: const EdgeInsets.all(4.0), - child: Align( - alignment: Alignment.centerRight, - child: CircleButton( - radius: 20, - icon: Icons.add, - onPressed: promptNewPack, - ), + children: packs + .map( + (e) => Padding( + padding: const EdgeInsets.fromLTRB(0, 2, 0, 2), + child: ExpansionTile( + collapsedBackgroundColor: + Theme.of(context).colorScheme.surfaceContainer, + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + title: Row( + children: [ + if (e.image != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 8, 0), + child: SizedBox( + width: 40, + height: 40, + child: Image(image: e.image!)), + ), + tiamat.Text.label(e.displayName), + ], ), + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), + child: EmoticonPackEditor( + pack: e, + editable: widget.editable, + ), + ) + ], ), - ], - ) - ], - ); - } - - void deletePack(int index) { - var pack = widget.packs[index]; - widget.deletePack?.call(pack).then((_) { - setState(() { - itemCount--; - _listKey.currentState?.removeItem( - index, - (context, animation) => SizeTransition( - sizeFactor: CommonAnimations.easeOut(animation), - child: EmojiPackEditor( - pack, - ), - )); - }); - }); - } - - Future deleteEmoticon(int index, Emoticon emoticon) async { - var pack = widget.packs[index]; - await widget.deleteEmoticon?.call(pack, emoticon); - } - - Future renameEmoticon(int index, Emoticon emoticon, String name) async { - var pack = widget.packs[index]; - await widget.renameEmoticon?.call(pack, emoticon, name); - } - - void promptNewPack() async { - await AdaptiveDialog.show( - context, - title: promptCreateEmoticonPack, - builder: (context) { - return EmoticonCreator( - pack: true, - create: widget.createNewPack, - ); - }, - ); - } - - void promptBulkImport() async { - await AdaptiveDialog.show( - context, - title: promptImportPack, - builder: (context) { - return EmoticonBulkImportDialog( - importPack: (name, avatarIndex, names, imageDatas) { - widget.importPack?.call(name, avatarIndex, names, imageDatas); - Navigator.pop(context); - }, - ); - }, + ), + ) + .toList(), ); } } -class EmojiPackEditor extends StatefulWidget { - const EmojiPackEditor(this.pack, - {super.key, - this.deletePack, - this.deleteEmoticon, - this.renameEmoticon, - this.initiallyExpanded = false, - this.showDeleteButton = true, - this.editable = false}); +class EmoticonPackEditor extends StatelessWidget { + const EmoticonPackEditor( + {required this.pack, this.editable = false, super.key}); final EmoticonPack pack; - final Function()? deletePack; final bool editable; - final bool initiallyExpanded; - final bool showDeleteButton; - final Future Function(Emoticon)? deleteEmoticon; - final Future Function(Emoticon, String)? renameEmoticon; - - @override - State createState() => _EmojiPackEditorState(); -} - -class _EmojiPackEditorState extends State { - final GlobalKey _listKey = GlobalKey(); - StreamSubscription? onCreate; - late int _itemCount; - late bool isPackEmoji; - late bool isPackSticker; - late bool isGlobalPack; - - String promptConfirmDeleteEmoticonPack(packName) => Intl.message( - "Are you sure you want to delete the **$packName** pack?", - args: [packName], - name: "promptConfirmDeleteEmoticonPack", - desc: - "Prompt to confirm deletion of an emoticon pack, supports markdown to emphasise the pack name"); String get createEmoticonDialogTitle => Intl.message("Create Emote", name: "createEmoticonDialogTitle", desc: "Title of a dialog that pops up when choosing to create a new emoticon"); - @override - void initState() { - widget.pack.onEmoticonAdded.listen(onEmojiInsert); - _itemCount = widget.pack.emotes.length; - isPackEmoji = widget.pack.isEmojiPack; - isPackSticker = widget.pack.isStickerPack; - isGlobalPack = widget.pack.isGloballyAvailable; - super.initState(); - } - - void onEmojiInsert(int index) { - setState(() { - _itemCount++; - _listKey.currentState?.insertItem(index); - }); - } - - void setIsEmojiPack(bool isEmoji) { - //if (!(isEmoji || isPackSticker)) return; - - setState(() { - isPackEmoji = isEmoji; - }); - - widget.pack.markAsEmoji(isEmoji); - } - - void setIsStickerPack(bool isSticker) { - //if (!(isSticker || isPackEmoji)) return; - - setState(() { - isPackSticker = isSticker; - }); - - widget.pack.markAsSticker(isSticker); - } - - void setIsGlobal(bool isGlobal) { - setState(() { - isGlobalPack = isGlobal; - }); - - widget.pack.markAsGlobal(isGlobal); - } - - void deleteEmoji(int index) { - var emoji = widget.pack.emotes[index]; - widget.deleteEmoticon?.call(emoji).then((value) { - setState(() { - _itemCount--; - _listKey.currentState?.removeItem( - index, - (context, animation) => SizeTransition( - sizeFactor: CommonAnimations.easeOut(animation), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 2, 0, 2), - child: EmojiEditor( - emoji, - editable: widget.editable, - ), - ), - )); - }); - }); - } - - void renameEmoji(int index, String name) { - var emoji = widget.pack.emotes[index]; - widget.renameEmoticon?.call(emoji, name); - } + String get editEmoticonDialogTitle => Intl.message("Edit Emote", + name: "editEmoticonDialogTitle", + desc: + "Title of a dialog that pops up when choosing to edit an existing emoticon"); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(2.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: ExpansionTile( - initiallyExpanded: widget.initiallyExpanded, - backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, - collapsedBackgroundColor: - Theme.of(context).colorScheme.surfaceContainerLow, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - if (widget.pack.image != null || widget.pack.icon != null) - SizedBox( - width: 30, - height: 30, - child: widget.pack.image != null - ? Image( - image: widget.pack.image!, - filterQuality: FilterQuality.medium, + return Column( + children: [ + Column( + children: pack.emotes + .map((e) => Padding( + padding: const EdgeInsets.fromLTRB(0, 2, 0, 2), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Material( + child: InkWell( + onTap: !editable + ? null + : () => AdaptiveDialog.show(context, + title: editEmoticonDialogTitle, + builder: (context) => EmoticonCreator( + pack, + initialEmoticon: e, + onCreate: + (name, usage, newImageData) async { + await pack.updateEmoticon( + previous: e, + shortcode: name, + usage: usage, + data: newImageData, + ); + return true; + }, + onDelete: () async { + await pack.deleteEmoticon(e); + }, + ), + dismissible: true), + child: Padding( + padding: const EdgeInsets.all(4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SizedBox( + width: 40, + height: 40, + child: Image(image: e.image!)), + const SizedBox( + width: 10, + ), + tiamat.Text.label(e.shortcode!) + ], + ), + Row( + children: [ + if (e.isEmoji) + Icon( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + size: 20, + Icons.emoji_emotions), + if (e.isSticker) + Icon( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + Icons.sticky_note_2_rounded) + ], ) - : Icon( - widget.pack.icon!, - )), - const SizedBox( - width: 10, - ), - tiamat.Text.labelEmphasised(widget.pack.displayName), - ], - ), - Row( - children: [ - SizedBox( - width: 40, - height: 40, - child: tiamat.IconToggle( - icon: Icons.public, - size: 20, - state: isGlobalPack, - onPressed: (newState) => setIsGlobal(newState), - ), - ), - if (widget.editable) - SizedBox( - width: 40, - height: 40, - child: tiamat.IconToggle( - icon: Icons.sticky_note_2_rounded, - size: 20, - state: isPackSticker, - onPressed: (newState) => setIsStickerPack(newState), + ], + ), + ), ), ), - if (widget.editable) - const SizedBox( - width: 4, - ), - if (widget.editable) - SizedBox( - width: 40, - height: 40, - child: tiamat.IconToggle( - size: 20, - icon: Icons.emoji_emotions, - state: isPackEmoji, - onPressed: (newState) => setIsEmojiPack(newState), - ), - ) - ], - ), - ], - ), - children: [ - AnimatedList( - shrinkWrap: true, - key: _listKey, - physics: const NeverScrollableScrollPhysics(), - initialItemCount: _itemCount, - itemBuilder: (context, index, animation) { - return SizeTransition( - sizeFactor: CommonAnimations.easeOut(animation), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 2, 9, 2), - child: index >= widget.pack.emotes.length - ? Container() - : EmojiEditor(widget.pack.emotes[index], - deleteEmoji: () => deleteEmoji(index), - editable: widget.editable, - setIsEmoji: (value) => widget.pack - .markEmoticonAsEmoji( - widget.pack.emotes[index], value), - setIsSticker: (value) => widget.pack - .markEmoticonAsSticker( - widget.pack.emotes[index], value), - renameEmoji: (name) => renameEmoji(index, name)), ), - ); - }, - ), - if (widget.editable) - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (widget.showDeleteButton) - tiamat.Button.danger( - text: CommonStrings.promptDelete, - onTap: () async { - var result = await AdaptiveDialog.confirmation( - context, - dangerous: true, - prompt: promptConfirmDeleteEmoticonPack( - widget.pack.displayName)); - - if (result == true) widget.deletePack?.call(); - }), - //Just putting a widget here to make the circle button stay on the right - if (!widget.showDeleteButton) const SizedBox(), - CircleButton( - radius: 20, - icon: Icons.add, - onPressed: createEmoticon, - ), - ], - ), - ) - ]), - ), - ); - } - - void createEmoticon() async { - await AdaptiveDialog.show(context, - builder: (context) => EmoticonCreator( - emoji: true, - create: (name, data) async { - await widget.pack - .addEmoticon(slug: name, shortcode: name, data: data!); - }, - ), - title: createEmoticonDialogTitle); - } -} - -class EmojiEditor extends StatefulWidget { - final Emoticon emoji; - const EmojiEditor(this.emoji, - {super.key, - this.deleteEmoji, - this.editable = false, - this.renameEmoji, - this.setIsEmoji, - this.setIsSticker}); - final void Function()? deleteEmoji; - final void Function(String)? renameEmoji; - final bool editable; - final void Function(bool)? setIsEmoji; - final void Function(bool)? setIsSticker; - - @override - State createState() => _EmojiEditorState(); -} - -class _EmojiEditorState extends State { - late bool isSticker; - late bool isEmoji; - - String promptConfirmDeleteEmoticon(emoticon) => Intl.message( - "Are you sure you want to delete **$emoticon**?", - args: [emoticon], - name: "promptConfirmDeleteEmoticon", - desc: - "Prompt to confirm deletion of an emoticon pack, supports markdown to emphasise the emote name"); - - String get promptRenameEmoticon => Intl.message("Rename emote", - name: "promptRenameEmoticon", - desc: "Tooltip for button to rename emoticon"); - - @override - void initState() { - isSticker = widget.emoji.isMarkedSticker; - isEmoji = widget.emoji.isMarkedEmoji; - super.initState(); - } - - void setSticker(bool newValue) { - setState(() { - isSticker = newValue; - }); - - widget.setIsSticker?.call(newValue); - } - - void setEmoji(bool newValue) { - setState(() { - isEmoji = newValue; - }); - - widget.setIsEmoji?.call(newValue); - } - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.editable) - tiamat.IconButton( - icon: Icons.remove_circle_outline, - size: 20, - onPressed: () async { - var result = await AdaptiveDialog.confirmation(context, - prompt: - promptConfirmDeleteEmoticon(widget.emoji.shortcode!), - dangerous: true); - - if (result == true) { - widget.deleteEmoji?.call(); - } - }, - ), - SizedBox( - height: 50, - width: 50, - child: EmojiWidget(widget.emoji), - ), - const SizedBox( - width: 10, - ), - widget.editable - ? EditableLabel( - initialText: widget.emoji.shortcode!, - changeTooltip: promptRenameEmoticon, - onTextConfirmed: (newText) => - widget.renameEmoji?.call(newText!), - ) - : tiamat.Text.label(widget.emoji.shortcode!), - ], + )) + .toList(), ), - if (widget.editable) - Row( - children: [ - SizedBox( - width: 40, - height: 40, - child: tiamat.IconToggle( - icon: Icons.sticky_note_2_rounded, - size: 20, - state: isSticker, - onPressed: setSticker, - ), - ), - const SizedBox( - width: 4, - ), - SizedBox( - width: 40, - height: 40, - child: tiamat.IconToggle( - size: 20, - icon: Icons.emoji_emotions, - state: isEmoji, - onPressed: setEmoji, - ), + if (editable) + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 0, 8), + child: tiamat.CircleButton( + icon: Icons.add, + onPressed: () => AdaptiveDialog.show(context, + title: createEmoticonDialogTitle, + builder: (context) => EmoticonCreator( + pack, + onCreate: (name, usage, newImageData) async { + await pack.addEmoticon( + slug: name, + shortcode: name, + data: newImageData!, + usage: usage); + + return true; + }, + ), + dismissible: true), ), - const SizedBox(width: 43) - ], - ), + ), + ) ], ); } } class EmoticonCreator extends StatefulWidget { - const EmoticonCreator( - {super.key, this.create, this.emoji, this.pack, this.sticker}); - final bool? pack; - final bool? sticker; - final bool? emoji; - final Future Function(String name, Uint8List? data)? create; + const EmoticonCreator(this.pack, + {this.initialEmoticon, + this.createPack = false, + this.onCreate, + this.onDelete, + super.key}); + + final Emoticon? initialEmoticon; + final EmoticonPack pack; + final bool createPack; + + final Future Function( + String name, EmoticonUsage usage, Uint8List? newImageData)? onCreate; + + final Future Function()? onDelete; @override State createState() => _EmoticonCreatorState(); } class _EmoticonCreatorState extends State { + late EmoticonUsage usage; + late ImageProvider? image; + Uint8List? imageData; - ImageProvider? pickedImage; TextEditingController controller = TextEditingController(); bool loading = false; @@ -617,100 +241,169 @@ class _EmoticonCreatorState extends State { name: "promptEmoticonPackName", desc: "Prompt for the input of the name of an emoticon pack"); - String get promptEmojiName => Intl.message("Emoji name", - name: "promptEmojiName", + String get promptEmoteName => Intl.message("Emote name", + name: "promptEmoteName", desc: "Prompt for the input of the name of an emoji"); - String get promptStickerName => Intl.message("Sticker name", - name: "promptStickerName", desc: "Prompt for the input of a sticker"); - - String get promptConfirmCreateEmoticon => Intl.message("Create!", - name: "promptConfirmCreateEmoticon", + String get promptConfirmSaveEmoticon => Intl.message("Save!", + name: "promptConfirmSaveEmoticon", desc: "Prompt to confirm the creation of an Emoticon Pack, Emoji, or Sticker"); @override - Widget build(BuildContext context) { - if (loading) - return const Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 50, height: 50, child: CircularProgressIndicator()), - ], - ); + void initState() { + super.initState(); - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 64, - height: 64, - child: ImagePicker( - size: 64, - icon: Icons.add_a_photo, - withData: true, - currentImage: pickedImage, - onImageRead: (bytes, mimeType, filepath) { - imageData = bytes; - var name = path.basename(filepath).split('.').first; - if (controller.text.isEmpty && !(widget.pack == true)) { - controller.text = name; - } + usage = widget.initialEmoticon?.usage ?? EmoticonUsage.all; + controller.text = widget.initialEmoticon?.shortcode ?? ""; + image = widget.initialEmoticon?.image; + } - pickedImage = Image.memory(bytes).image; - }, - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 300), - child: tiamat.TextInput( - placeholder: widget.pack == true - ? promptEmoticonPackName - : widget.emoji == true - ? promptEmojiName - : promptStickerName, - controller: controller, + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + AnimatedOpacity( + opacity: loading ? 0.5 : 1, + duration: Durations.short2, + child: IgnorePointer( + ignoring: loading, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 300, maxHeight: 300), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 50, + height: 50, + child: ImagePicker( + size: 50, + icon: Icons.add_a_photo, + withData: true, + currentImage: image, + onImageRead: (bytes, mimeType, filepath) { + imageData = bytes; + var name = + path.basename(filepath).split('.').first; + if (controller.text.isEmpty && + !widget.createPack) { + controller.text = name; + } + + image = Image.memory(bytes).image; + }, + ), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 300), + child: tiamat.TextInput( + maxLines: 1, + placeholder: widget.createPack + ? promptEmoticonPackName + : promptEmoteName, + controller: controller, + ), + ), + ) + ], ), - ), + const SizedBox( + height: 4, + ), + SizedBox( + height: 40, + width: 40, + child: tiamat.DropdownSelector( + itemHeight: 40, + items: const [ + EmoticonUsage.emoji, + EmoticonUsage.sticker, + EmoticonUsage.all, + ], + value: usage, + onItemSelected: (item) { + setState(() { + usage = item; + }); + }, + itemBuilder: (item) { + return Row( + children: [ + Icon(switch (item) { + EmoticonUsage.sticker => Icons.sticky_note_2, + EmoticonUsage.emoji => Icons.emoji_emotions, + EmoticonUsage.all => Icons.star + }), + const SizedBox( + width: 8, + ), + tiamat.Text.label(switch (item) { + EmoticonUsage.sticker => "Sticker", + EmoticonUsage.emoji => "Emoji", + EmoticonUsage.all => "Emoji & Sticker", + }) + ], + ); + }, + ), + ), + const SizedBox( + height: 4, + ), + SizedBox( + height: 48, + child: tiamat.Button( + text: promptConfirmSaveEmoticon, + onTap: () { + if (controller.text.isNotEmpty) { + setState(() { + loading = true; + }); + + widget.onCreate + ?.call(controller.text, usage, imageData) + .then((e) => Navigator.of(context).pop()); + } + }, + ), + ), + const SizedBox( + height: 4, + ), + if (widget.initialEmoticon != null) + tiamat.Button.danger( + text: CommonStrings.promptDelete, + onTap: () { + if (controller.text.isNotEmpty) { + setState(() { + loading = true; + }); + + widget.onDelete + ?.call() + .then((e) => Navigator.of(context).pop()); + } + }, + ) + ], ), - ) - ], - ), - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), - child: SizedBox( - height: 48, - child: tiamat.Button( - text: promptConfirmCreateEmoticon, - onTap: () { - if (controller.text.isNotEmpty) { - setState(() { - loading = true; - }); - - if (widget.create != null) { - widget.create! - .call(controller.text, imageData) - .then((value) { - Navigator.pop(context); - }); - } - } - }, ), ), - ) - ], - ), + ), + ), + if (loading) const Center(child: CircularProgressIndicator()) + ], ); } } diff --git a/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart b/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart index 2a14e357d..81c9afaaa 100644 --- a/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart +++ b/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart @@ -27,40 +27,8 @@ class _SpaceEmojiPackSettingsState extends State { @override Widget build(BuildContext context) { return RoomEmojiPackSettingsView( - component.ownedPacks, - createNewPack: createNewPack, - onPackCreated: component.onOwnedPackAdded, - deletePack: deletePack, - deleteEmoticon: deleteEmoticon, - canCreatePack: component.canCreatePack, + component: component, editable: widget.space.permissions.canEditRoomEmoticons, - renameEmoticon: renameEmoticon, - importPack: importPack, ); } - - Future createNewPack(String name, Uint8List? avatarData) async { - await component.createEmoticonPack( - name, - avatarData, - ); - } - - Future deletePack(EmoticonPack pack) async { - await component.deleteEmoticonPack(pack); - } - - Future deleteEmoticon(EmoticonPack pack, Emoticon emoticon) async { - await pack.deleteEmoticon(emoticon); - } - - Future renameEmoticon( - EmoticonPack pack, Emoticon emoticon, String name) async { - await pack.renameEmoticon(emoticon, name); - } - - Future importPack(String name, int avatarIndex, List names, - List imageDatas) async { - component.importEmoticonPack(name, avatarIndex, names, imageDatas); - } } diff --git a/commet/lib/utils/emoji/unicode_emoji.dart b/commet/lib/utils/emoji/unicode_emoji.dart index 49aff1e6d..40e89933c 100644 --- a/commet/lib/utils/emoji/unicode_emoji.dart +++ b/commet/lib/utils/emoji/unicode_emoji.dart @@ -149,17 +149,6 @@ class UnicodeEmoticonPack implements EmoticonPack { } } - @override - Future addEmoticon( - {required String slug, - String? shortcode, - required Uint8List data, - String? mimeType, - bool? isEmoji, - bool? isSticker}) { - throw UnimplementedError(); - } - @override Future deleteEmoticon(Emoticon emoticon) { throw UnimplementedError(); @@ -170,26 +159,6 @@ class UnicodeEmoticonPack implements EmoticonPack { throw UnimplementedError(); } - @override - Future markEmoticonAsEmoji(Object emoticon, bool isEmoji) { - throw UnimplementedError(); - } - - @override - Future markEmoticonAsSticker(Emoticon emoticon, bool isSticker) { - throw UnimplementedError(); - } - - @override - Future markAsEmoji(bool isEmojiPack) { - throw UnimplementedError(); - } - - @override - Future markAsSticker(bool isStickerPack) { - throw UnimplementedError(); - } - @override Future markAsGlobal(bool isGlobal) { throw UnimplementedError(); @@ -228,6 +197,38 @@ class UnicodeEmoticonPack implements EmoticonPack { @override String get ownerId => ""; + + @override + Future setEmoticonUsage(Emoticon emoticon, EmoticonUsage usage) async {} + + @override + Future setPackUsage(EmoticonUsage usage) async {} + + @override + EmoticonUsage get usage => EmoticonUsage.emoji; + + @override + Future addEmoticon( + {required String slug, + String? shortcode, + required Uint8List data, + String? mimeType, + EmoticonUsage? usage}) { + // TODO: implement addEmoticon + throw UnimplementedError(); + } + + @override + Future updateEmoticon( + {String? slug, + String? shortcode, + Uint8List? data, + String? mimeType, + EmoticonUsage? usage, + required Emoticon previous}) { + // TODO: implement updateEmoticon + throw UnimplementedError(); + } } class UnicodeEmoticon extends Emoticon { @@ -305,4 +306,7 @@ class UnicodeEmoticon extends Emoticon { int get hashCode { return slug.hashCode; } + + @override + EmoticonUsage get usage => EmoticonUsage.emoji; } diff --git a/tiamat/lib/atoms/dropdown_selector.dart b/tiamat/lib/atoms/dropdown_selector.dart index 880c73a2e..5ad71befe 100644 --- a/tiamat/lib/atoms/dropdown_selector.dart +++ b/tiamat/lib/atoms/dropdown_selector.dart @@ -1,145 +1,23 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; -import 'package:tiamat/atoms/text.dart'; -import 'package:tiamat/config/config.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart'; -import 'package:tiamat/tiamat.dart' as tiamat; - -@UseCase(name: 'String Selector', type: DropdownSelector) -Widget wbDropdownSelector(BuildContext context) { - return tiamat.Tile.low2( - child: Padding( - padding: EdgeInsets.all(10.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: DropdownSelector( - items: ["Alpha", "Bravo", "Charlie", "Delta"], - itemBuilder: (item) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: tiamat.Text(item), - ); - }, - ), - ), - ], - ), - ), - ), - ); -} - -@UseCase(name: 'Multi Line Text', type: DropdownSelector) -Widget wbDropdownSelectorMultiLine(BuildContext context) { - return tiamat.Tile.low2( - child: Padding( - padding: EdgeInsets.all(10.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: DropdownSelector( - itemHeight: 80, - items: [loremIpsum, "Bravo", loremIpsum + " ", "Delta"], - itemBuilder: (item) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: tiamat.Text(item), - ); - }, - ), - ), - ], - ), - ), - ), - ); -} - -@UseCase(name: 'Avatar Selector', type: DropdownSelector) -Widget wbDropdownAvatarSelector(BuildContext context) { - return tiamat.Tile( - child: Padding( - padding: EdgeInsets.all(10.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: SizedBox( - child: DropdownSelector( - itemHeight: 70, - items: [ - AssetImage( - "assets/images/placeholder/generic/checker_purple.png"), - AssetImage( - "assets/images/placeholder/generic/checker_red.png"), - AssetImage( - "assets/images/placeholder/generic/checker_green.png"), - AssetImage( - "assets/images/placeholder/generic/checker_orange.png") - ], - itemBuilder: (item) { - return Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: tiamat.Avatar.medium(image: item), - ), - tiamat.Text.labelEmphasised("Avatar with text") - ], - ); - }, - ), - ), - ), - ], - ), - ), - ), - ); -} - -class DropdownSelector extends StatefulWidget { +class DropdownSelector extends StatelessWidget { const DropdownSelector( {required this.items, required this.itemBuilder, this.itemHeight = 50, this.onItemSelected, - this.defaultIndex = 0, this.hint, + required this.value, super.key}); + final List items; final Widget Function(T item) itemBuilder; final void Function(T item)? onItemSelected; - final int? defaultIndex; final double itemHeight; final Widget? hint; - @override - State> createState() => DropdownSelectorState(); -} - -class DropdownSelectorState extends State> { - T? value; - - @override - void initState() { - if (widget.defaultIndex != null) { - value = widget.items[widget.defaultIndex!]; - } - super.initState(); - } + final T value; @override Widget build(BuildContext context) { @@ -156,29 +34,26 @@ class DropdownSelectorState extends State> { builder: (BuildContext context, BoxConstraints constraints) { return DropdownButtonHideUnderline( child: DropdownButton2( - menuItemStyleData: MenuItemStyleData(height: widget.itemHeight), + menuItemStyleData: MenuItemStyleData(height: itemHeight), value: value, - hint: widget.hint, + hint: hint, dropdownStyleData: DropdownStyleData( decoration: BoxDecoration( borderRadius: BorderRadius.only( bottomRight: Radius.circular(10), bottomLeft: Radius.circular(10)), color: Theme.of(context).colorScheme.surfaceContainerHigh)), - items: widget.items.map((value) { + items: items.map((value) { return DropdownMenuItem( alignment: Alignment.centerLeft, value: value, child: SizedBox( width: constraints.maxWidth - 60, - child: widget.itemBuilder(value)), + child: itemBuilder(value)), ); }).toList(), onChanged: (newValue) { - setState(() { - value = newValue!; - }); - widget.onItemSelected?.call(newValue!); + onItemSelected?.call(newValue!); }, )); }), diff --git a/tiamat/lib/atoms/icon_toggle.dart b/tiamat/lib/atoms/icon_toggle.dart index a5a5f81ae..93d555aad 100644 --- a/tiamat/lib/atoms/icon_toggle.dart +++ b/tiamat/lib/atoms/icon_toggle.dart @@ -76,8 +76,8 @@ class _IconToggleState extends State { icon: widget.icon, size: widget.size, iconColor: widget.state - ? m.Theme.of(context).colorScheme.onPrimary - : m.Theme.of(context).colorScheme.secondary, + ? m.Theme.of(context).colorScheme.primary + : m.Theme.of(context).colorScheme.onPrimary, onPressed: () => widget.onPressed?.call(!widget.state), backgroundColor: widget.backgroundColor); } diff --git a/tiamat/pubspec.lock b/tiamat/pubspec.lock index 3ebc2d3f2..8b756ad06 100644 --- a/tiamat/pubspec.lock +++ b/tiamat/pubspec.lock @@ -344,18 +344,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -392,18 +392,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -549,10 +549,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" timing: dependency: transitive description: @@ -581,10 +581,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: From a70c606e56f1f460c6f174f1da08c7587e0baf42 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sat, 29 Mar 2025 22:27:35 +1030 Subject: [PATCH 2/6] Inherit emoticon usage from pack --- .../components/emoticon/emoji_pack.dart | 7 + .../client/components/emoticon/emoticon.dart | 1 + .../components/emoticon/matrix_emoticon.dart | 16 +- .../emoticon/matrix_emoticon_component.dart | 26 ++ .../emoticon/matrix_emoticon_pack.dart | 50 ++- ...matrix_timeline_event_mixin_reactions.dart | 1 + .../atoms/rich_text/matrix_html_parser.dart | 1 + .../room_emoji_pack_settings_page.dart | 4 - .../room_emoji_pack_settings_view.dart | 414 +++++++++++------- .../space/space_emoji_pack_settings.dart | 4 - commet/lib/utils/emoji/unicode_emoji.dart | 25 +- 11 files changed, 347 insertions(+), 202 deletions(-) diff --git a/commet/lib/client/components/emoticon/emoji_pack.dart b/commet/lib/client/components/emoticon/emoji_pack.dart index e7b0fc056..3f67d725e 100644 --- a/commet/lib/client/components/emoticon/emoji_pack.dart +++ b/commet/lib/client/components/emoticon/emoji_pack.dart @@ -30,8 +30,15 @@ abstract class EmoticonPack { Future setPackUsage(EmoticonUsage usage); + Future updatePack( + {EmoticonUsage? usage, String? name, Uint8List? imageData}); + Emoticon? getByShortcode(String shortcode); + bool get isStickerPack; + + bool get isEmojiPack; + Future updateEmoticon({ String? slug, String? shortcode, diff --git a/commet/lib/client/components/emoticon/emoticon.dart b/commet/lib/client/components/emoticon/emoticon.dart index 021f880a1..d9935ab2f 100644 --- a/commet/lib/client/components/emoticon/emoticon.dart +++ b/commet/lib/client/components/emoticon/emoticon.dart @@ -5,6 +5,7 @@ enum EmoticonUsage { sticker, emoji, all, + inherit, } abstract class Emoticon { diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart index 8e7e8dac3..ffdd321e3 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon.dart @@ -21,8 +21,12 @@ class MatrixEmoticon implements Emoticon { @override EmoticonUsage usage; + EmoticonUsage packUsage; + MatrixEmoticon(this.emojiUrl, matrix.Client client, - {required String shortcode, required this.usage}) { + {required this.packUsage, + required String shortcode, + required this.usage}) { _shortcode = shortcode; _image = MatrixMxcImage(emojiUrl, client, doThumbnail: false); } @@ -56,9 +60,15 @@ class MatrixEmoticon implements Emoticon { @override bool get isSticker => - usage == EmoticonUsage.sticker || usage == EmoticonUsage.all; + usage == EmoticonUsage.sticker || + usage == EmoticonUsage.all || + (usage == EmoticonUsage.inherit && + [EmoticonUsage.sticker, EmoticonUsage.all].contains(packUsage)); @override bool get isEmoji => - usage == EmoticonUsage.emoji || usage == EmoticonUsage.all; + usage == EmoticonUsage.emoji || + usage == EmoticonUsage.all || + (usage == EmoticonUsage.inherit && + [EmoticonUsage.emoji, EmoticonUsage.all].contains(packUsage)); } diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart index 8bdb1c617..b19d13a9d 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_component.dart @@ -225,6 +225,31 @@ class MatrixEmoticonComponent extends EmoticonComponent { return state.setState(packKey, content); } + Future updatePack(String packKey, + {EmoticonUsage? usage, String? name, Uint8List? imageData}) async { + var content = state.getState(packKey); + + if (usage != null) { + content['pack']['usage'] = switch (usage) { + EmoticonUsage.sticker => ["sticker"], + EmoticonUsage.emoji => ["emoticon"], + EmoticonUsage.all => ["emoticon", "sticker"], + EmoticonUsage.inherit => null, + }; + } + + if (name != null) { + content['pack']['display_name'] = name; + } + + if (imageData != null) { + Uri url = await client.getMatrixClient().uploadContent(imageData); + content['pack']['avatar_url'] = url.toString(); + } + + return state.setState(packKey, content); + } + Future updateEmoticon( String packKey, String emoteName, { @@ -242,6 +267,7 @@ class MatrixEmoticonComponent extends EmoticonComponent { EmoticonUsage.sticker => ["sticker"], EmoticonUsage.emoji => ["emoticon"], EmoticonUsage.all => ["emoticon", "sticker"], + EmoticonUsage.inherit => null, }; } diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart index 5a952327d..6777b5c6a 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart @@ -36,7 +36,7 @@ class MatrixEmoticonPack implements EmoticonPack { final usage = usagesArrayToUsage(usages); return MatrixEmoticon(Uri.parse(url), component.client.getMatrixClient(), - shortcode: shortCode, usage: usage); + packUsage: this.usage, shortcode: shortCode, usage: usage); }).toList(); } @@ -83,7 +83,12 @@ class MatrixEmoticonPack implements EmoticonPack { return EmoticonUsage.sticker; } - return EmoticonUsage.all; + if ((usages?.contains("sticker") == true) && + (usages?.contains("emoticon") == true)) { + return EmoticonUsage.all; + } + + return EmoticonUsage.inherit; } @override @@ -118,16 +123,12 @@ class MatrixEmoticonPack implements EmoticonPack { } @override - List get emoji => emotes - .where((element) => - [EmoticonUsage.all, EmoticonUsage.emoji].contains(element.usage)) - .toList(); + List get emoji => + emotes.where((element) => element.isEmoji).toList(); @override - List get stickers => emotes - .where((element) => - [EmoticonUsage.all, EmoticonUsage.sticker].contains(element.usage)) - .toList(); + List get stickers => + emotes.where((element) => element.isSticker).toList(); @override String get identifier => stateKey; @@ -150,11 +151,12 @@ class MatrixEmoticonPack implements EmoticonPack { } } - List _emoticonUsageToArray(EmoticonUsage usage) { + List? _emoticonUsageToArray(EmoticonUsage usage) { final usages = switch (usage) { EmoticonUsage.sticker => ["sticker"], EmoticonUsage.emoji => ["emoticon"], EmoticonUsage.all => ["sticker", "emoticon"], + EmoticonUsage.inherit => null, }; return usages; @@ -166,6 +168,13 @@ class MatrixEmoticonPack implements EmoticonPack { return component.setPackUsages(identifier, usages); } + @override + Future updatePack( + {EmoticonUsage? usage, String? name, Uint8List? imageData}) { + return component.updatePack(identifier, + usage: usage, name: name, imageData: imageData); + } + @override Stream get onEmoticonAdded => throw UnimplementedError(); @@ -213,5 +222,22 @@ class MatrixEmoticonPack implements EmoticonPack { int get hashCode => stateKey.hashCode; @override - EmoticonUsage get usage => throw UnimplementedError(); + EmoticonUsage get usage { + final pack = state.tryGetMap("pack"); + var usages = pack?.tryGetList("usage"); + var usage = usagesArrayToUsage(usages); + if (usage == EmoticonUsage.inherit) { + return EmoticonUsage.all; + } else { + return usage; + } + } + + @override + bool get isEmojiPack => + [EmoticonUsage.emoji, EmoticonUsage.all].contains(usage); + + @override + bool get isStickerPack => + [EmoticonUsage.sticker, EmoticonUsage.all].contains(usage); } diff --git a/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart b/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart index 17e0523ee..74a03f83a 100644 --- a/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart +++ b/commet/lib/client/matrix/timeline_events/matrix_timeline_event_mixin_reactions.dart @@ -71,6 +71,7 @@ mixin MatrixTimelineEventReactions on MatrixTimelineEvent if (key.startsWith("mxc://")) { return MatrixEmoticon(Uri.parse(key), timeline.room.client, shortcode: event.content.tryGet("shortcode") ?? "", + packUsage: EmoticonUsage.all, usage: EmoticonUsage.all); } diff --git a/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart b/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart index a4eca085e..a0479d1d0 100644 --- a/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart +++ b/commet/lib/ui/atoms/rich_text/matrix_html_parser.dart @@ -221,6 +221,7 @@ class MatrixEmoticonHtmlExtension extends HtmlExtension { child: EmojiWidget( MatrixEmoticon(uri, client, shortcode: context.attributes["alt"] ?? "", + packUsage: EmoticonUsage.all, usage: EmoticonUsage.emoji), height: emojiSize, )); diff --git a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart index c7e6453d7..48fbc50c8 100644 --- a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_page.dart @@ -1,10 +1,6 @@ -import 'dart:typed_data'; - import 'package:commet/client/client.dart'; import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart'; -import 'package:commet/client/components/emoticon/emoji_pack.dart'; -import 'package:commet/client/components/emoticon/emoticon.dart'; import 'package:flutter/widgets.dart'; class RoomEmojiPackSettingsPage extends StatefulWidget { diff --git a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart index 274d841b9..7f162e5bf 100644 --- a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart +++ b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart @@ -26,16 +26,22 @@ class _RoomEmojiPackSettingsViewState extends State { late List packs; StreamSubscription? sub; + bool canCreatePack = false; @override void initState() { super.initState(); sub = widget.component.onStateChanged.listen((_) => setState(() { - packs = widget.component.ownedPacks; + updateState(); })); + updateState(); + } + + void updateState() { packs = widget.component.ownedPacks; + canCreatePack = widget.component.canCreatePack; } @override @@ -47,40 +53,111 @@ class _RoomEmojiPackSettingsViewState extends State { @override Widget build(BuildContext context) { return Column( - children: packs - .map( - (e) => Padding( - padding: const EdgeInsets.fromLTRB(0, 2, 0, 2), - child: ExpansionTile( - collapsedBackgroundColor: - Theme.of(context).colorScheme.surfaceContainer, - backgroundColor: Theme.of(context).colorScheme.surfaceContainer, - title: Row( - children: [ - if (e.image != null) + children: [ + Column( + children: packs + .map( + (e) => Padding( + padding: const EdgeInsets.fromLTRB(0, 2, 0, 2), + child: ExpansionTile( + collapsedBackgroundColor: + Theme.of(context).colorScheme.surfaceContainer, + backgroundColor: + Theme.of(context).colorScheme.surfaceContainer, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (e.image != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 8, 0), + child: SizedBox( + width: 40, + height: 40, + child: Image(image: e.image!)), + ), + tiamat.Text.label(e.displayName), + ], + ), + Row( + children: [ + Row( + children: [ + if (widget.editable) + tiamat.IconButton( + size: 20, + icon: Icons.edit, + onPressed: () => AdaptiveDialog.show( + context, + builder: (context) => EmoticonCreator( + pack: e, + createPack: true, + onCreate: (name, usage, + newImageData) async { + await e.updatePack( + name: name, + usage: usage, + imageData: newImageData, + ); + return true; + }, + onDelete: () { + return widget.component + .deleteEmoticonPack(e); + }, + ))), + if (e.isEmojiPack) + Icon( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + size: 20, + Icons.emoji_emotions), + if (e.isStickerPack) + Icon( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + Icons.sticky_note_2_rounded) + ], + ), + ], + ) + ], + ), + children: [ Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 8, 0), - child: SizedBox( - width: 40, - height: 40, - child: Image(image: e.image!)), - ), - tiamat.Text.label(e.displayName), - ], + padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), + child: EmoticonPackEditor( + pack: e, + editable: widget.editable, + ), + ) + ], + ), ), - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), - child: EmoticonPackEditor( - pack: e, - editable: widget.editable, - ), - ) - ], - ), - ), + ) + .toList(), + ), + if (canCreatePack) + Align( + alignment: Alignment.topRight, + child: tiamat.CircleButton( + icon: Icons.add, + onPressed: () => AdaptiveDialog.show(context, + builder: (context) => EmoticonCreator( + createPack: true, + creatingNew: true, + onCreate: (name, usage, newImageData) async { + await widget.component + .createEmoticonPack(name, newImageData); + + return true; + }, + ))), ) - .toList(), + ], ); } } @@ -118,7 +195,7 @@ class EmoticonPackEditor extends StatelessWidget { : () => AdaptiveDialog.show(context, title: editEmoticonDialogTitle, builder: (context) => EmoticonCreator( - pack, + pack: pack, initialEmoticon: e, onCreate: (name, usage, newImageData) async { @@ -166,7 +243,13 @@ class EmoticonPackEditor extends StatelessWidget { color: Theme.of(context) .colorScheme .secondaryContainer, - Icons.sticky_note_2_rounded) + Icons.sticky_note_2_rounded), + if (e.usage == EmoticonUsage.inherit) + Icon( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + Icons.arrow_downward) ], ) ], @@ -188,7 +271,8 @@ class EmoticonPackEditor extends StatelessWidget { onPressed: () => AdaptiveDialog.show(context, title: createEmoticonDialogTitle, builder: (context) => EmoticonCreator( - pack, + pack: pack, + creatingNew: true, onCreate: (name, usage, newImageData) async { await pack.addEmoticon( slug: name, @@ -209,16 +293,19 @@ class EmoticonPackEditor extends StatelessWidget { } class EmoticonCreator extends StatefulWidget { - const EmoticonCreator(this.pack, + const EmoticonCreator( {this.initialEmoticon, + this.pack, this.createPack = false, this.onCreate, this.onDelete, + this.creatingNew = false, super.key}); final Emoticon? initialEmoticon; - final EmoticonPack pack; + final EmoticonPack? pack; final bool createPack; + final bool creatingNew; final Future Function( String name, EmoticonUsage usage, Uint8List? newImageData)? onCreate; @@ -254,9 +341,15 @@ class _EmoticonCreatorState extends State { void initState() { super.initState(); - usage = widget.initialEmoticon?.usage ?? EmoticonUsage.all; - controller.text = widget.initialEmoticon?.shortcode ?? ""; - image = widget.initialEmoticon?.image; + if (widget.createPack && widget.pack != null) { + usage = widget.pack!.usage; + controller.text = widget.pack!.displayName; + image = widget.pack!.image; + } else { + usage = widget.initialEmoticon?.usage ?? EmoticonUsage.inherit; + controller.text = widget.initialEmoticon?.shortcode ?? ""; + image = widget.initialEmoticon?.image; + } } @override @@ -271,133 +364,134 @@ class _EmoticonCreatorState extends State { ignoring: loading, child: Padding( padding: const EdgeInsets.all(8.0), - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 300, maxHeight: 300), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 50, - height: 50, - child: ImagePicker( - size: 50, - icon: Icons.add_a_photo, - withData: true, - currentImage: image, - onImageRead: (bytes, mimeType, filepath) { - imageData = bytes; - var name = - path.basename(filepath).split('.').first; - if (controller.text.isEmpty && - !widget.createPack) { - controller.text = name; - } - - image = Image.memory(bytes).image; - }, - ), - ), - const SizedBox( - width: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 50, + height: 50, + child: ImagePicker( + size: 50, + icon: Icons.add_a_photo, + withData: true, + currentImage: image, + onImageRead: (bytes, mimeType, filepath) { + imageData = bytes; + var name = path.basename(filepath).split('.').first; + if (controller.text.isEmpty && !widget.createPack) { + controller.text = name; + } + + image = Image.memory(bytes).image; + }, ), - Expanded( - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 300), - child: tiamat.TextInput( - maxLines: 1, - placeholder: widget.createPack - ? promptEmoticonPackName - : promptEmoteName, - controller: controller, - ), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 300), + child: tiamat.TextInput( + maxLines: 1, + placeholder: widget.createPack + ? promptEmoticonPackName + : promptEmoteName, + controller: controller, ), - ) + ), + ) + ], + ), + const SizedBox( + height: 4, + ), + SizedBox( + height: 40, + width: 40, + child: tiamat.DropdownSelector( + itemHeight: 40, + items: [ + EmoticonUsage.emoji, + EmoticonUsage.sticker, + EmoticonUsage.all, + if (!widget.createPack) EmoticonUsage.inherit, ], + value: usage, + onItemSelected: (item) { + setState(() { + usage = item; + }); + }, + itemBuilder: (item) { + return Row( + children: [ + Icon(switch (item) { + EmoticonUsage.sticker => Icons.sticky_note_2, + EmoticonUsage.emoji => Icons.emoji_emotions, + EmoticonUsage.inherit => Icons.arrow_downward, + EmoticonUsage.all => Icons.star + }), + const SizedBox( + width: 8, + ), + tiamat.Text.label(switch (item) { + EmoticonUsage.sticker => "Sticker", + EmoticonUsage.emoji => "Emoji", + EmoticonUsage.inherit => "Follow Pack", + EmoticonUsage.all => "Emoji & Sticker", + }) + ], + ); + }, ), - const SizedBox( - height: 4, - ), - SizedBox( - height: 40, - width: 40, - child: tiamat.DropdownSelector( - itemHeight: 40, - items: const [ - EmoticonUsage.emoji, - EmoticonUsage.sticker, - EmoticonUsage.all, - ], - value: usage, - onItemSelected: (item) { + ), + const SizedBox( + height: 4, + ), + SizedBox( + height: 48, + child: tiamat.Button( + text: promptConfirmSaveEmoticon, + onTap: () { + if (controller.text.isNotEmpty) { setState(() { - usage = item; + loading = true; }); - }, - itemBuilder: (item) { - return Row( - children: [ - Icon(switch (item) { - EmoticonUsage.sticker => Icons.sticky_note_2, - EmoticonUsage.emoji => Icons.emoji_emotions, - EmoticonUsage.all => Icons.star - }), - const SizedBox( - width: 8, - ), - tiamat.Text.label(switch (item) { - EmoticonUsage.sticker => "Sticker", - EmoticonUsage.emoji => "Emoji", - EmoticonUsage.all => "Emoji & Sticker", - }) - ], - ); - }, - ), - ), - const SizedBox( - height: 4, - ), - SizedBox( - height: 48, - child: tiamat.Button( - text: promptConfirmSaveEmoticon, - onTap: () { - if (controller.text.isNotEmpty) { - setState(() { - loading = true; - }); - - widget.onCreate - ?.call(controller.text, usage, imageData) - .then((e) => Navigator.of(context).pop()); - } - }, - ), - ), - const SizedBox( - height: 4, + + widget.onCreate + ?.call(controller.text, usage, imageData) + .then((e) => Navigator.of(context).pop()); + } + }, ), - if (widget.initialEmoticon != null) - tiamat.Button.danger( - text: CommonStrings.promptDelete, - onTap: () { - if (controller.text.isNotEmpty) { - setState(() { - loading = true; - }); - - widget.onDelete - ?.call() - .then((e) => Navigator.of(context).pop()); - } - }, - ) - ], - ), + ), + const SizedBox( + height: 4, + ), + if (!widget.creatingNew) + tiamat.Button.danger( + text: CommonStrings.promptDelete, + onTap: () async { + final confirm = + await AdaptiveDialog.confirmation(context); + + if (confirm == true) { + setState(() { + loading = true; + }); + + widget.onDelete + ?.call() + .then((e) => Navigator.of(context).pop()); + } + }, + ) + ], ), ), ), diff --git a/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart b/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart index 81c9afaaa..77eacd021 100644 --- a/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart +++ b/commet/lib/ui/pages/settings/categories/space/space_emoji_pack_settings.dart @@ -1,10 +1,6 @@ -import 'dart:typed_data'; - import 'package:commet/client/client.dart'; import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart'; -import 'package:commet/client/components/emoticon/emoji_pack.dart'; -import 'package:commet/client/components/emoticon/emoticon.dart'; import 'package:flutter/widgets.dart'; class SpaceEmojiPackSettings extends StatefulWidget { diff --git a/commet/lib/utils/emoji/unicode_emoji.dart b/commet/lib/utils/emoji/unicode_emoji.dart index 40e89933c..c88614db0 100644 --- a/commet/lib/utils/emoji/unicode_emoji.dart +++ b/commet/lib/utils/emoji/unicode_emoji.dart @@ -154,19 +154,11 @@ class UnicodeEmoticonPack implements EmoticonPack { throw UnimplementedError(); } - @override - Future renameEmoticon(Emoticon emoticon, String name) { - throw UnimplementedError(); - } - @override Future markAsGlobal(bool isGlobal) { throw UnimplementedError(); } - @override - bool get isGloballyAvailable => true; - @override List getShortcodes() { return emoji.map((e) => e.shortcode!).toList(); @@ -198,9 +190,6 @@ class UnicodeEmoticonPack implements EmoticonPack { @override String get ownerId => ""; - @override - Future setEmoticonUsage(Emoticon emoticon, EmoticonUsage usage) async {} - @override Future setPackUsage(EmoticonUsage usage) async {} @@ -214,7 +203,6 @@ class UnicodeEmoticonPack implements EmoticonPack { required Uint8List data, String? mimeType, EmoticonUsage? usage}) { - // TODO: implement addEmoticon throw UnimplementedError(); } @@ -226,7 +214,12 @@ class UnicodeEmoticonPack implements EmoticonPack { String? mimeType, EmoticonUsage? usage, required Emoticon previous}) { - // TODO: implement updateEmoticon + throw UnimplementedError(); + } + + @override + Future updatePack( + {EmoticonUsage? usage, String? name, Uint8List? imageData}) { throw UnimplementedError(); } } @@ -252,12 +245,6 @@ class UnicodeEmoticon extends Emoticon { @override bool get isSticker => false; - @override - bool get isMarkedEmoji => true; - - @override - bool get isMarkedSticker => false; - UnicodeEmoticon(String text, {String? shortcode}) { _shortcode = shortcode; slug = text; From 5cbe5b0b46d7ee7c732d7ced010fa54207064bb4 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 30 Mar 2025 01:39:10 +1030 Subject: [PATCH 3/6] bring back favorite emoticons --- .../components/emoticon/emoji_pack.dart | 2 + .../emoticon/matrix_emoticon_pack.dart | 28 +++++ .../matrix_emoticon_state_manager.dart | 4 +- .../matrix_room_emoticon_component.dart | 5 +- .../account_emoji/account_emoji_tab.dart | 3 +- .../account_emoji/account_emoji_view.dart | 81 ++++++++++---- .../room_emoji_pack_settings_view.dart | 101 +++++++++++++----- commet/lib/utils/emoji/unicode_emoji.dart | 3 + 8 files changed, 173 insertions(+), 54 deletions(-) diff --git a/commet/lib/client/components/emoticon/emoji_pack.dart b/commet/lib/client/components/emoticon/emoji_pack.dart index 3f67d725e..152315e25 100644 --- a/commet/lib/client/components/emoticon/emoji_pack.dart +++ b/commet/lib/client/components/emoticon/emoji_pack.dart @@ -11,6 +11,8 @@ abstract class EmoticonPack { String get ownerId; String get ownerDisplayName; + bool get isGloballyAvailable; + Stream get onEmoticonAdded; List get emotes; diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart index 6777b5c6a..5616583e3 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart @@ -240,4 +240,32 @@ class MatrixEmoticonPack implements EmoticonPack { @override bool get isStickerPack => [EmoticonUsage.sticker, EmoticonUsage.all].contains(usage); + + @override + bool get isGloballyAvailable { + late Room room; + if (component is MatrixRoomEmoticonComponent) { + room = (component as MatrixRoomEmoticonComponent).room.matrixRoom; + } else if (component is MatrixSpaceEmoticonComponent) { + room = (component as MatrixSpaceEmoticonComponent).space.matrixRoom; + } else { + return false; + } + + final data = room + .client.accountData[MatrixEmoticonComponent.globalEmoteRoomsStateKey]; + if (data == null) { + return false; + } + + final rooms = data.content.tryGetMap("rooms"); + + if (rooms == null) { + return false; + } + + final roomData = rooms.tryGetMap(room.id); + + return roomData?.containsKey(identifier) ?? false; + } } diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart index d09eeddd6..bf5de5ffa 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart @@ -36,7 +36,9 @@ class MatrixEmoticonPersonalStateManager implements MatrixEmoticonStateManager { }); mx.onSync.stream.where((e) => e.accountData != null).listen((update) { - if (update.accountData?.any((e) => e.type == "im.ponies.user_emotes") == + if (update.accountData?.any((e) => + e.type == "im.ponies.user_emotes" || + e.type == "im.ponies.emote_rooms") == true) { onStateChangedController.add(null); } diff --git a/commet/lib/client/matrix/components/emoticon/matrix_room_emoticon_component.dart b/commet/lib/client/matrix/components/emoticon/matrix_room_emoticon_component.dart index 165842ff3..298fe2f4b 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_room_emoticon_component.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_room_emoticon_component.dart @@ -131,7 +131,7 @@ class MatrixRoomEmoticonComponent extends MatrixEmoticonComponent .where((element) => element.containsRoom(room.identifier))) { var component = space.getComponent(); if (component != null) { - result.addAll(component.ownedPacks); + result.addAll(component.ownedPacks.where((e) => !result.contains(e))); } } @@ -145,7 +145,8 @@ class MatrixRoomEmoticonComponent extends MatrixEmoticonComponent } if (globalComponent != null) { - result.addAll(globalComponent.ownedPacks); + result + .addAll(globalComponent.ownedPacks.where((e) => !result.contains(e))); } if (includeUnicode) result.addAll(UnicodeEmojis.packs!); diff --git a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart index fb7ded1a1..d9f17f1bf 100644 --- a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart +++ b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart @@ -69,8 +69,7 @@ class _AccountEmojiTabState extends State { const SizedBox( height: 5, ), - if (component!.globalPacks().isNotEmpty) - AccountEmojiView(component!.globalPacks(), component!.ownedPacks), + if (component!.globalPacks().isNotEmpty) AccountEmojiView(component!), ], ); } diff --git a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart index fe4d49479..bd88dc1e9 100644 --- a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart +++ b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart @@ -1,27 +1,45 @@ +import 'dart:async'; + import 'package:commet/client/components/emoticon/emoji_pack.dart'; +import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:tiamat/tiamat.dart' as tiamat; class AccountEmojiView extends StatefulWidget { - const AccountEmojiView(this.globalPacks, this.personalPacks, {super.key}); - final List globalPacks; - final List personalPacks; + const AccountEmojiView(this.component, {super.key}); + final EmoticonComponent component; @override State createState() => _AccountEmojiViewState(); } class _AccountEmojiViewState extends State { + late List globalPacks; + StreamSubscription? sub; + + @override + void initState() { + sub = widget.component.onStateChanged.listen((_) => updateState()); + updateState(); + super.initState(); + } + + void updateState() { + setState(() { + globalPacks = widget.component.globalPacks(); + }); + } + @override Widget build(BuildContext context) { return Column( children: [ tiamat.Panel( - header: "Global Packs", + header: "Favorite Packs", mode: tiamat.TileType.surfaceContainerLow, - child: Column( - children: widget.globalPacks.map((e) => packSummary(e)).toList()), + child: + Column(children: globalPacks.map((e) => packSummary(e)).toList()), ), ], ); @@ -33,25 +51,42 @@ class _AccountEmojiViewState extends State { child: SizedBox( height: 40, child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (pack.image != null) - Image( - image: pack.image!, - filterQuality: FilterQuality.medium, - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 0, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - tiamat.Text.labelEmphasised(pack.displayName), - tiamat.Text.labelLow(preferences.developerMode - ? "${pack.ownerDisplayName} - (${pack.ownerId})" - : pack.ownerDisplayName), - ], - ), + Row( + children: [ + if (pack.image != null) + SizedBox( + width: 40, + height: 40, + child: Image( + image: pack.image!, + filterQuality: FilterQuality.medium, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 0, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + tiamat.Text.labelEmphasised(pack.displayName), + tiamat.Text.labelLow(preferences.developerMode + ? "${pack.ownerDisplayName} - (${pack.ownerId})" + : pack.ownerDisplayName), + ], + ), + ), + ], ), + SizedBox( + width: 40, + height: 40, + child: tiamat.IconButton( + icon: Icons.heart_broken, + onPressed: () => pack.markAsGlobal(false), + ), + ) ], ), ), diff --git a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart index 7f162e5bf..2d5440b4b 100644 --- a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart +++ b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart @@ -6,6 +6,7 @@ import 'package:commet/client/components/emoticon/emoticon.dart'; import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/ui/molecules/image_picker.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; +import 'package:commet/ui/pages/settings/categories/room/emoji_packs/bulk_import_view.dart'; import 'package:commet/utils/common_strings.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -28,20 +29,24 @@ class _RoomEmojiPackSettingsViewState extends State { StreamSubscription? sub; bool canCreatePack = false; + String get promptImportPack => Intl.message("Import pack", + name: "promptImportPack", + desc: "Prompt to import a set of emoticons from an existing pack"); + @override void initState() { super.initState(); - sub = widget.component.onStateChanged.listen((_) => setState(() { - updateState(); - })); + sub = widget.component.onStateChanged.listen((_) => updateState()); updateState(); } void updateState() { - packs = widget.component.ownedPacks; - canCreatePack = widget.component.canCreatePack; + setState(() { + packs = widget.component.ownedPacks; + canCreatePack = widget.component.canCreatePack; + }); } @override @@ -50,6 +55,22 @@ class _RoomEmojiPackSettingsViewState extends State { super.dispose(); } + void promptBulkImport() async { + await AdaptiveDialog.show( + context, + title: promptImportPack, + builder: (context) { + return EmoticonBulkImportDialog( + importPack: (name, avatarIndex, names, imageDatas) { + widget.component + .importEmoticonPack(name, avatarIndex, names, imageDatas); + Navigator.pop(context); + }, + ); + }, + ); + } + @override Widget build(BuildContext context) { return Column( @@ -119,7 +140,16 @@ class _RoomEmojiPackSettingsViewState extends State { color: Theme.of(context) .colorScheme .secondaryContainer, - Icons.sticky_note_2_rounded) + Icons.sticky_note_2_rounded), + tiamat.IconToggle( + icon: Icons.favorite, + size: 17, + state: e.isGloballyAvailable, + onPressed: (newState) async { + await e.markAsGlobal(newState); + updateState(); + }, + ), ], ), ], @@ -143,19 +173,30 @@ class _RoomEmojiPackSettingsViewState extends State { if (canCreatePack) Align( alignment: Alignment.topRight, - child: tiamat.CircleButton( - icon: Icons.add, - onPressed: () => AdaptiveDialog.show(context, - builder: (context) => EmoticonCreator( - createPack: true, - creatingNew: true, - onCreate: (name, usage, newImageData) async { - await widget.component - .createEmoticonPack(name, newImageData); - - return true; - }, - ))), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + tiamat.CircleButton( + icon: Icons.auto_awesome_motion, + onPressed: promptBulkImport), + SizedBox( + width: 10, + ), + tiamat.CircleButton( + icon: Icons.add, + onPressed: () => AdaptiveDialog.show(context, + builder: (context) => EmoticonCreator( + createPack: true, + creatingNew: true, + onCreate: (name, usage, newImageData) async { + await widget.component + .createEmoticonPack(name, newImageData); + + return true; + }, + ))), + ], + ), ) ], ); @@ -318,7 +359,7 @@ class EmoticonCreator extends StatefulWidget { class _EmoticonCreatorState extends State { late EmoticonUsage usage; - late ImageProvider? image; + ImageProvider? image; Uint8List? imageData; TextEditingController controller = TextEditingController(); @@ -341,12 +382,20 @@ class _EmoticonCreatorState extends State { void initState() { super.initState(); - if (widget.createPack && widget.pack != null) { - usage = widget.pack!.usage; - controller.text = widget.pack!.displayName; - image = widget.pack!.image; + if (widget.createPack) { + if (widget.pack != null) { + usage = widget.pack!.usage; + controller.text = widget.pack!.displayName; + image = widget.pack!.image; + } else { + usage = EmoticonUsage.all; + } } else { - usage = widget.initialEmoticon?.usage ?? EmoticonUsage.inherit; + if (widget.initialEmoticon != null) { + usage = widget.initialEmoticon!.usage; + } else { + usage = EmoticonUsage.inherit; + } controller.text = widget.initialEmoticon?.shortcode ?? ""; image = widget.initialEmoticon?.image; } @@ -442,7 +491,7 @@ class _EmoticonCreatorState extends State { tiamat.Text.label(switch (item) { EmoticonUsage.sticker => "Sticker", EmoticonUsage.emoji => "Emoji", - EmoticonUsage.inherit => "Follow Pack", + EmoticonUsage.inherit => "Follow Pack Settings", EmoticonUsage.all => "Emoji & Sticker", }) ], diff --git a/commet/lib/utils/emoji/unicode_emoji.dart b/commet/lib/utils/emoji/unicode_emoji.dart index c88614db0..5357eaa6e 100644 --- a/commet/lib/utils/emoji/unicode_emoji.dart +++ b/commet/lib/utils/emoji/unicode_emoji.dart @@ -222,6 +222,9 @@ class UnicodeEmoticonPack implements EmoticonPack { {EmoticonUsage? usage, String? name, Uint8List? imageData}) { throw UnimplementedError(); } + + @override + bool get isGloballyAvailable => false; } class UnicodeEmoticon extends Emoticon { From 97634e2d54e93b04ae90809b5c68049f7676a235 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 30 Mar 2025 11:05:16 +1030 Subject: [PATCH 4/6] remove unused stream --- commet/lib/client/components/emoticon/emoji_pack.dart | 2 -- .../matrix/components/emoticon/matrix_emoticon_pack.dart | 3 --- 2 files changed, 5 deletions(-) diff --git a/commet/lib/client/components/emoticon/emoji_pack.dart b/commet/lib/client/components/emoticon/emoji_pack.dart index 152315e25..8ac0fb264 100644 --- a/commet/lib/client/components/emoticon/emoji_pack.dart +++ b/commet/lib/client/components/emoticon/emoji_pack.dart @@ -13,8 +13,6 @@ abstract class EmoticonPack { bool get isGloballyAvailable; - Stream get onEmoticonAdded; - List get emotes; List get emoji; diff --git a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart index 5616583e3..9973c7920 100644 --- a/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart +++ b/commet/lib/client/matrix/components/emoticon/matrix_emoticon_pack.dart @@ -175,9 +175,6 @@ class MatrixEmoticonPack implements EmoticonPack { usage: usage, name: name, imageData: imageData); } - @override - Stream get onEmoticonAdded => throw UnimplementedError(); - @override List getShortcodes() { return emoji.map((e) => e.shortcode!).toList(); From 3c15265c3185b58b78028169c8c0be7d5d96ec28 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 30 Mar 2025 11:23:32 +1030 Subject: [PATCH 5/6] Update unicode_emoji.dart --- commet/lib/utils/emoji/unicode_emoji.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/commet/lib/utils/emoji/unicode_emoji.dart b/commet/lib/utils/emoji/unicode_emoji.dart index 5357eaa6e..93fa3aee7 100644 --- a/commet/lib/utils/emoji/unicode_emoji.dart +++ b/commet/lib/utils/emoji/unicode_emoji.dart @@ -91,9 +91,6 @@ class UnicodeEmoticonPack implements EmoticonPack { @override String get identifier => throw UnimplementedError(); - @override - Stream get onEmoticonAdded => throw UnimplementedError(); - @override List get emotes => _emoji!; From c61ac3b23cb0f927237ab818002d76c590bdb04c Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 30 Mar 2025 11:25:14 +1030 Subject: [PATCH 6/6] clean up some warnings --- .../account_emoji/account_emoji_view.dart | 1 - .../room_emoji_pack_settings_view.dart | 17 +++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart index bd88dc1e9..f02c8c710 100644 --- a/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart +++ b/commet/lib/ui/pages/settings/categories/account/account_emoji/account_emoji_view.dart @@ -4,7 +4,6 @@ import 'package:commet/client/components/emoticon/emoji_pack.dart'; import 'package:commet/client/components/emoticon/emoticon_component.dart'; import 'package:commet/main.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:tiamat/tiamat.dart' as tiamat; class AccountEmojiView extends StatefulWidget { diff --git a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart index 2d5440b4b..2071efdbd 100644 --- a/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart +++ b/commet/lib/ui/pages/settings/categories/room/emoji_packs/room_emoji_pack_settings_view.dart @@ -179,7 +179,7 @@ class _RoomEmojiPackSettingsViewState extends State { tiamat.CircleButton( icon: Icons.auto_awesome_motion, onPressed: promptBulkImport), - SizedBox( + const SizedBox( width: 10, ), tiamat.CircleButton( @@ -506,15 +506,16 @@ class _EmoticonCreatorState extends State { height: 48, child: tiamat.Button( text: promptConfirmSaveEmoticon, - onTap: () { + onTap: () async { if (controller.text.isNotEmpty) { setState(() { loading = true; }); - widget.onCreate - ?.call(controller.text, usage, imageData) - .then((e) => Navigator.of(context).pop()); + await widget.onCreate + ?.call(controller.text, usage, imageData); + + if (context.mounted) Navigator.of(context).pop(); } }, ), @@ -534,9 +535,9 @@ class _EmoticonCreatorState extends State { loading = true; }); - widget.onDelete - ?.call() - .then((e) => Navigator.of(context).pop()); + await widget.onDelete?.call(); + + if (context.mounted) Navigator.of(context).pop(); } }, )