diff --git a/lib/i18n/aria/aria.i18n.yaml b/lib/i18n/aria/aria.i18n.yaml index d948e20d..7109c7d2 100644 --- a/lib/i18n/aria/aria.i18n.yaml +++ b/lib/i18n/aria/aria.i18n.yaml @@ -5,6 +5,7 @@ addTab: "Add tab" alwaysExpandCw: "Always expand content warning" alwaysExpandLongNote: "Always expand long note" alwaysExpandMediaInSubNote: "Always expand media in sub note" +alwaysShowAllReactions: "Always show all reactions" alwaysShowTabHeader: "Always show tab info" authenticate: "Authenticate" authenticated: "Authenticated" @@ -61,6 +62,7 @@ loginWithAccessToken: "Sign in with an access token" margin: "Margin" media: "Media" mentionToRemoteWarning: "This note includes mentions to remote users" +mergeReactionsByName: "Merge reactions with the same name" moved: "Moved" muted: "Muted" mutedEmojis: "Muted emojis" diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 7fa3bea9..adb1c9f9 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 31 -/// Strings: 54618 (1761 per locale) +/// Strings: 54620 (1761 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_en_US.g.dart b/lib/i18n/strings_en_US.g.dart index 970c69f8..5828650b 100644 --- a/lib/i18n/strings_en_US.g.dart +++ b/lib/i18n/strings_en_US.g.dart @@ -57,6 +57,7 @@ class TranslationsAriaEnUs { String get alwaysExpandCw => 'Always expand content warning'; String get alwaysExpandLongNote => 'Always expand long note'; String get alwaysExpandMediaInSubNote => 'Always expand media in sub note'; + String get alwaysShowAllReactions => 'Always show all reactions'; String get alwaysShowTabHeader => 'Always show tab info'; String get authenticate => 'Authenticate'; String get authenticated => 'Authenticated'; @@ -129,6 +130,7 @@ class TranslationsAriaEnUs { String get margin => 'Margin'; String get media => 'Media'; String get mentionToRemoteWarning => 'This note includes mentions to remote users'; + String get mergeReactionsByName => 'Merge reactions with the same name'; String get moved => 'Moved'; String get muted => 'Muted'; String get mutedEmojis => 'Muted emojis'; diff --git a/lib/model/general_settings.dart b/lib/model/general_settings.dart index 6642e47f..1b2f5f35 100644 --- a/lib/model/general_settings.dart +++ b/lib/model/general_settings.dart @@ -61,6 +61,8 @@ class GeneralSettings with _$GeneralSettings { @Default(false) bool alwaysExpandCw, @Default(false) bool alwaysExpandLongNote, @Default(false) bool alwaysExpandMediaInSubNote, + @Default(false) bool mergeReactionsByName, + @Default(false) bool alwaysShowAllReactions, MediaListWithOneImageAppearance? mediaListWithOneImageAppearance, @Default(BoxFit.contain) BoxFit thumbnailBoxFit, @Default(EmojiStyle.twemoji) EmojiStyle emojiStyle, diff --git a/lib/model/general_settings.freezed.dart b/lib/model/general_settings.freezed.dart index fe064fb6..0dbc9a34 100644 --- a/lib/model/general_settings.freezed.dart +++ b/lib/model/general_settings.freezed.dart @@ -49,6 +49,8 @@ mixin _$GeneralSettings { bool get alwaysExpandCw => throw _privateConstructorUsedError; bool get alwaysExpandLongNote => throw _privateConstructorUsedError; bool get alwaysExpandMediaInSubNote => throw _privateConstructorUsedError; + bool get mergeReactionsByName => throw _privateConstructorUsedError; + bool get alwaysShowAllReactions => throw _privateConstructorUsedError; MediaListWithOneImageAppearance? get mediaListWithOneImageAppearance => throw _privateConstructorUsedError; BoxFit get thumbnailBoxFit => throw _privateConstructorUsedError; @@ -158,6 +160,8 @@ abstract class $GeneralSettingsCopyWith<$Res> { bool alwaysExpandCw, bool alwaysExpandLongNote, bool alwaysExpandMediaInSubNote, + bool mergeReactionsByName, + bool alwaysShowAllReactions, MediaListWithOneImageAppearance? mediaListWithOneImageAppearance, BoxFit thumbnailBoxFit, EmojiStyle emojiStyle, @@ -256,6 +260,8 @@ class _$GeneralSettingsCopyWithImpl<$Res, $Val extends GeneralSettings> Object? alwaysExpandCw = null, Object? alwaysExpandLongNote = null, Object? alwaysExpandMediaInSubNote = null, + Object? mergeReactionsByName = null, + Object? alwaysShowAllReactions = null, Object? mediaListWithOneImageAppearance = freezed, Object? thumbnailBoxFit = null, Object? emojiStyle = null, @@ -414,6 +420,14 @@ class _$GeneralSettingsCopyWithImpl<$Res, $Val extends GeneralSettings> ? _value.alwaysExpandMediaInSubNote : alwaysExpandMediaInSubNote // ignore: cast_nullable_to_non_nullable as bool, + mergeReactionsByName: null == mergeReactionsByName + ? _value.mergeReactionsByName + : mergeReactionsByName // ignore: cast_nullable_to_non_nullable + as bool, + alwaysShowAllReactions: null == alwaysShowAllReactions + ? _value.alwaysShowAllReactions + : alwaysShowAllReactions // ignore: cast_nullable_to_non_nullable + as bool, mediaListWithOneImageAppearance: freezed == mediaListWithOneImageAppearance ? _value.mediaListWithOneImageAppearance @@ -679,6 +693,8 @@ abstract class _$$GeneralSettingsImplCopyWith<$Res> bool alwaysExpandCw, bool alwaysExpandLongNote, bool alwaysExpandMediaInSubNote, + bool mergeReactionsByName, + bool alwaysShowAllReactions, MediaListWithOneImageAppearance? mediaListWithOneImageAppearance, BoxFit thumbnailBoxFit, EmojiStyle emojiStyle, @@ -775,6 +791,8 @@ class __$$GeneralSettingsImplCopyWithImpl<$Res> Object? alwaysExpandCw = null, Object? alwaysExpandLongNote = null, Object? alwaysExpandMediaInSubNote = null, + Object? mergeReactionsByName = null, + Object? alwaysShowAllReactions = null, Object? mediaListWithOneImageAppearance = freezed, Object? thumbnailBoxFit = null, Object? emojiStyle = null, @@ -933,6 +951,14 @@ class __$$GeneralSettingsImplCopyWithImpl<$Res> ? _value.alwaysExpandMediaInSubNote : alwaysExpandMediaInSubNote // ignore: cast_nullable_to_non_nullable as bool, + mergeReactionsByName: null == mergeReactionsByName + ? _value.mergeReactionsByName + : mergeReactionsByName // ignore: cast_nullable_to_non_nullable + as bool, + alwaysShowAllReactions: null == alwaysShowAllReactions + ? _value.alwaysShowAllReactions + : alwaysShowAllReactions // ignore: cast_nullable_to_non_nullable + as bool, mediaListWithOneImageAppearance: freezed == mediaListWithOneImageAppearance ? _value.mediaListWithOneImageAppearance @@ -1193,6 +1219,8 @@ class _$GeneralSettingsImpl implements _GeneralSettings { this.alwaysExpandCw = false, this.alwaysExpandLongNote = false, this.alwaysExpandMediaInSubNote = false, + this.mergeReactionsByName = false, + this.alwaysShowAllReactions = false, this.mediaListWithOneImageAppearance, this.thumbnailBoxFit = BoxFit.contain, this.emojiStyle = EmojiStyle.twemoji, @@ -1332,6 +1360,12 @@ class _$GeneralSettingsImpl implements _GeneralSettings { @JsonKey() final bool alwaysExpandMediaInSubNote; @override + @JsonKey() + final bool mergeReactionsByName; + @override + @JsonKey() + final bool alwaysShowAllReactions; + @override final MediaListWithOneImageAppearance? mediaListWithOneImageAppearance; @override @JsonKey() @@ -1504,7 +1538,7 @@ class _$GeneralSettingsImpl implements _GeneralSettings { @override String toString() { - return 'GeneralSettings(locale: $locale, collapseRenotes: $collapseRenotes, sensitive: $sensitive, highlightSensitiveMedia: $highlightSensitiveMedia, animatedMfm: $animatedMfm, advancedMfm: $advancedMfm, showReactionsCount: $showReactionsCount, loadRawImages: $loadRawImages, instanceTicker: $instanceTicker, showNoteCreatedAt: $showNoteCreatedAt, showAvatarsInNote: $showAvatarsInNote, showAvatarsInSubNote: $showAvatarsInSubNote, squareAvatars: $squareAvatars, showAvatarDecorations: $showAvatarDecorations, showQuoteButtonInNoteFooter: $showQuoteButtonInNoteFooter, showLikeButtonInNoteFooter: $showLikeButtonInNoteFooter, showClipButtonInNoteFooter: $showClipButtonInNoteFooter, showTranslateButtonInNoteFooter: $showTranslateButtonInNoteFooter, showNoteReactionsViewer: $showNoteReactionsViewer, showSubNoteReactionsViewer: $showSubNoteReactionsViewer, showNoteFooter: $showNoteFooter, showSubNoteFooter: $showSubNoteFooter, alwaysExpandCw: $alwaysExpandCw, alwaysExpandLongNote: $alwaysExpandLongNote, alwaysExpandMediaInSubNote: $alwaysExpandMediaInSubNote, mediaListWithOneImageAppearance: $mediaListWithOneImageAppearance, thumbnailBoxFit: $thumbnailBoxFit, emojiStyle: $emojiStyle, fontFamily: $fontFamily, fontSize: $fontSize, lineHeight: $lineHeight, avatarScale: $avatarScale, reactionsDisplayScale: $reactionsDisplayScale, limitWidthOfReaction: $limitWidthOfReaction, noteFooterScale: $noteFooterScale, noteVerticalPadding: $noteVerticalPadding, noteHorizontalPadding: $noteHorizontalPadding, publicNoteBackgroundColor: $publicNoteBackgroundColor, homeNoteBackgroundColor: $homeNoteBackgroundColor, followersNoteBackgroundColor: $followersNoteBackgroundColor, specifiedNoteBackgroundColor: $specifiedNoteBackgroundColor, emojiPickerUseDialog: $emojiPickerUseDialog, emojiPickerScale: $emojiPickerScale, emojiPickerAutofocus: $emojiPickerAutofocus, emojiPickerKeepOpen: $emojiPickerKeepOpen, dataSaverMedia: $dataSaverMedia, dataSaverAvatar: $dataSaverAvatar, dataSaverUrlPreview: $dataSaverUrlPreview, disableDataSaverWhenOnWifi: $disableDataSaverWhenOnWifi, reduceAnimation: $reduceAnimation, disableShowingAnimatedImages: $disableShowingAnimatedImages, enableEmojiFadeIn: $enableEmojiFadeIn, forceShowAds: $forceShowAds, useGroupedNotifications: $useGroupedNotifications, showTimelineTabBarAtBottom: $showTimelineTabBarAtBottom, showMenuButtonInTabBar: $showMenuButtonInTabBar, showHomeFAB: $showHomeFAB, showNotificationsFAB: $showNotificationsFAB, showShowPostFormFAB: $showShowPostFormFAB, showTabHeaderInOneLine: $showTabHeaderInOneLine, alwaysShowTabHeader: $alwaysShowTabHeader, showTimelineLastViewedAt: $showTimelineLastViewedAt, showPopupOnNewNote: $showPopupOnNewNote, vibrateNote: $vibrateNote, vibrateNotification: $vibrateNotification, enableInfiniteScroll: $enableInfiniteScroll, keepScreenOn: $keepScreenOn, enableHorizontalSwipe: $enableHorizontalSwipe, openSensitiveMediaOnDoubleTap: $openSensitiveMediaOnDoubleTap, noteTapAction: $noteTapAction, noteDoubleTapAction: $noteDoubleTapAction, noteLongPressAction: $noteLongPressAction, confirmBeforePost: $confirmBeforePost, confirmBeforeReact: $confirmBeforeReact, confirmBeforeFollow: $confirmBeforeFollow, confirmWhenRevealingSensitiveMedia: $confirmWhenRevealingSensitiveMedia, launchMode: $launchMode, enablePredictiveBack: $enablePredictiveBack, themeMode: $themeMode, lightThemeId: $lightThemeId, darkThemeId: $darkThemeId)'; + return 'GeneralSettings(locale: $locale, collapseRenotes: $collapseRenotes, sensitive: $sensitive, highlightSensitiveMedia: $highlightSensitiveMedia, animatedMfm: $animatedMfm, advancedMfm: $advancedMfm, showReactionsCount: $showReactionsCount, loadRawImages: $loadRawImages, instanceTicker: $instanceTicker, showNoteCreatedAt: $showNoteCreatedAt, showAvatarsInNote: $showAvatarsInNote, showAvatarsInSubNote: $showAvatarsInSubNote, squareAvatars: $squareAvatars, showAvatarDecorations: $showAvatarDecorations, showQuoteButtonInNoteFooter: $showQuoteButtonInNoteFooter, showLikeButtonInNoteFooter: $showLikeButtonInNoteFooter, showClipButtonInNoteFooter: $showClipButtonInNoteFooter, showTranslateButtonInNoteFooter: $showTranslateButtonInNoteFooter, showNoteReactionsViewer: $showNoteReactionsViewer, showSubNoteReactionsViewer: $showSubNoteReactionsViewer, showNoteFooter: $showNoteFooter, showSubNoteFooter: $showSubNoteFooter, alwaysExpandCw: $alwaysExpandCw, alwaysExpandLongNote: $alwaysExpandLongNote, alwaysExpandMediaInSubNote: $alwaysExpandMediaInSubNote, mergeReactionsByName: $mergeReactionsByName, alwaysShowAllReactions: $alwaysShowAllReactions, mediaListWithOneImageAppearance: $mediaListWithOneImageAppearance, thumbnailBoxFit: $thumbnailBoxFit, emojiStyle: $emojiStyle, fontFamily: $fontFamily, fontSize: $fontSize, lineHeight: $lineHeight, avatarScale: $avatarScale, reactionsDisplayScale: $reactionsDisplayScale, limitWidthOfReaction: $limitWidthOfReaction, noteFooterScale: $noteFooterScale, noteVerticalPadding: $noteVerticalPadding, noteHorizontalPadding: $noteHorizontalPadding, publicNoteBackgroundColor: $publicNoteBackgroundColor, homeNoteBackgroundColor: $homeNoteBackgroundColor, followersNoteBackgroundColor: $followersNoteBackgroundColor, specifiedNoteBackgroundColor: $specifiedNoteBackgroundColor, emojiPickerUseDialog: $emojiPickerUseDialog, emojiPickerScale: $emojiPickerScale, emojiPickerAutofocus: $emojiPickerAutofocus, emojiPickerKeepOpen: $emojiPickerKeepOpen, dataSaverMedia: $dataSaverMedia, dataSaverAvatar: $dataSaverAvatar, dataSaverUrlPreview: $dataSaverUrlPreview, disableDataSaverWhenOnWifi: $disableDataSaverWhenOnWifi, reduceAnimation: $reduceAnimation, disableShowingAnimatedImages: $disableShowingAnimatedImages, enableEmojiFadeIn: $enableEmojiFadeIn, forceShowAds: $forceShowAds, useGroupedNotifications: $useGroupedNotifications, showTimelineTabBarAtBottom: $showTimelineTabBarAtBottom, showMenuButtonInTabBar: $showMenuButtonInTabBar, showHomeFAB: $showHomeFAB, showNotificationsFAB: $showNotificationsFAB, showShowPostFormFAB: $showShowPostFormFAB, showTabHeaderInOneLine: $showTabHeaderInOneLine, alwaysShowTabHeader: $alwaysShowTabHeader, showTimelineLastViewedAt: $showTimelineLastViewedAt, showPopupOnNewNote: $showPopupOnNewNote, vibrateNote: $vibrateNote, vibrateNotification: $vibrateNotification, enableInfiniteScroll: $enableInfiniteScroll, keepScreenOn: $keepScreenOn, enableHorizontalSwipe: $enableHorizontalSwipe, openSensitiveMediaOnDoubleTap: $openSensitiveMediaOnDoubleTap, noteTapAction: $noteTapAction, noteDoubleTapAction: $noteDoubleTapAction, noteLongPressAction: $noteLongPressAction, confirmBeforePost: $confirmBeforePost, confirmBeforeReact: $confirmBeforeReact, confirmBeforeFollow: $confirmBeforeFollow, confirmWhenRevealingSensitiveMedia: $confirmWhenRevealingSensitiveMedia, launchMode: $launchMode, enablePredictiveBack: $enablePredictiveBack, themeMode: $themeMode, lightThemeId: $lightThemeId, darkThemeId: $darkThemeId)'; } @override @@ -1567,6 +1601,8 @@ class _$GeneralSettingsImpl implements _GeneralSettings { (identical(other.alwaysExpandMediaInSubNote, alwaysExpandMediaInSubNote) || other.alwaysExpandMediaInSubNote == alwaysExpandMediaInSubNote) && + (identical(other.mergeReactionsByName, mergeReactionsByName) || other.mergeReactionsByName == mergeReactionsByName) && + (identical(other.alwaysShowAllReactions, alwaysShowAllReactions) || other.alwaysShowAllReactions == alwaysShowAllReactions) && (identical(other.mediaListWithOneImageAppearance, mediaListWithOneImageAppearance) || other.mediaListWithOneImageAppearance == mediaListWithOneImageAppearance) && (identical(other.thumbnailBoxFit, thumbnailBoxFit) || other.thumbnailBoxFit == thumbnailBoxFit) && (identical(other.emojiStyle, emojiStyle) || other.emojiStyle == emojiStyle) && @@ -1654,6 +1690,8 @@ class _$GeneralSettingsImpl implements _GeneralSettings { alwaysExpandCw, alwaysExpandLongNote, alwaysExpandMediaInSubNote, + mergeReactionsByName, + alwaysShowAllReactions, mediaListWithOneImageAppearance, thumbnailBoxFit, emojiStyle, @@ -1757,6 +1795,8 @@ abstract class _GeneralSettings implements GeneralSettings { final bool alwaysExpandCw, final bool alwaysExpandLongNote, final bool alwaysExpandMediaInSubNote, + final bool mergeReactionsByName, + final bool alwaysShowAllReactions, final MediaListWithOneImageAppearance? mediaListWithOneImageAppearance, final BoxFit thumbnailBoxFit, final EmojiStyle emojiStyle, @@ -1871,6 +1911,10 @@ abstract class _GeneralSettings implements GeneralSettings { @override bool get alwaysExpandMediaInSubNote; @override + bool get mergeReactionsByName; + @override + bool get alwaysShowAllReactions; + @override MediaListWithOneImageAppearance? get mediaListWithOneImageAppearance; @override BoxFit get thumbnailBoxFit; diff --git a/lib/model/general_settings.g.dart b/lib/model/general_settings.g.dart index 07e576f5..33cc1791 100644 --- a/lib/model/general_settings.g.dart +++ b/lib/model/general_settings.g.dart @@ -46,6 +46,8 @@ _$GeneralSettingsImpl _$$GeneralSettingsImplFromJson( alwaysExpandLongNote: json['alwaysExpandLongNote'] as bool? ?? false, alwaysExpandMediaInSubNote: json['alwaysExpandMediaInSubNote'] as bool? ?? false, + mergeReactionsByName: json['mergeReactionsByName'] as bool? ?? false, + alwaysShowAllReactions: json['alwaysShowAllReactions'] as bool? ?? false, mediaListWithOneImageAppearance: $enumDecodeNullable( _$MediaListWithOneImageAppearanceEnumMap, json['mediaListWithOneImageAppearance']), @@ -175,6 +177,8 @@ Map _$$GeneralSettingsImplToJson( val['alwaysExpandCw'] = instance.alwaysExpandCw; val['alwaysExpandLongNote'] = instance.alwaysExpandLongNote; val['alwaysExpandMediaInSubNote'] = instance.alwaysExpandMediaInSubNote; + val['mergeReactionsByName'] = instance.mergeReactionsByName; + val['alwaysShowAllReactions'] = instance.alwaysShowAllReactions; writeNotNull( 'mediaListWithOneImageAppearance', _$MediaListWithOneImageAppearanceEnumMap[ diff --git a/lib/provider/general_settings_notifier_provider.dart b/lib/provider/general_settings_notifier_provider.dart index 5ea3dec9..c2d7d9f2 100644 --- a/lib/provider/general_settings_notifier_provider.dart +++ b/lib/provider/general_settings_notifier_provider.dart @@ -184,6 +184,16 @@ class GeneralSettingsNotifier extends _$GeneralSettingsNotifier { await _save(); } + Future setMergeReactionsByName(bool mergeReactionsByName) async { + state = state.copyWith(mergeReactionsByName: mergeReactionsByName); + await _save(); + } + + Future setAlwaysShowAllReactions(bool alwaysShowAllReactions) async { + state = state.copyWith(alwaysShowAllReactions: alwaysShowAllReactions); + await _save(); + } + Future setMediaListWithOneImageAppearance( MediaListWithOneImageAppearance? mediaListWithOneImageAppearance, ) async { diff --git a/lib/provider/general_settings_notifier_provider.g.dart b/lib/provider/general_settings_notifier_provider.g.dart index 43917375..8abc00cc 100644 --- a/lib/provider/general_settings_notifier_provider.g.dart +++ b/lib/provider/general_settings_notifier_provider.g.dart @@ -7,7 +7,7 @@ part of 'general_settings_notifier_provider.dart'; // ************************************************************************** String _$generalSettingsNotifierHash() => - r'dcd0837658085a5c5334a06fcbe4a5538f5e360d'; + r'a2d10ce1e4992efed6f3c23e2ba9b08c10e0b546'; /// See also [GeneralSettingsNotifier]. @ProviderFor(GeneralSettingsNotifier) diff --git a/lib/view/page/settings/note_display_page.dart b/lib/view/page/settings/note_display_page.dart index 4d27128b..476f762e 100644 --- a/lib/view/page/settings/note_display_page.dart +++ b/lib/view/page/settings/note_display_page.dart @@ -331,6 +331,20 @@ class NoteDisplayPage extends HookConsumerWidget { .read(generalSettingsNotifierProvider.notifier) .setAlwaysExpandMediaInSubNote(value), ), + SwitchListTile( + title: Text(t.aria.mergeReactionsByName), + value: settings.mergeReactionsByName, + onChanged: (value) => ref + .read(generalSettingsNotifierProvider.notifier) + .setMergeReactionsByName(value), + ), + SwitchListTile( + title: Text(t.aria.alwaysShowAllReactions), + value: settings.alwaysShowAllReactions, + onChanged: (value) => ref + .read(generalSettingsNotifierProvider.notifier) + .setAlwaysShowAllReactions(value), + ), ListTile( title: Text(t.misskey.emojiStyle), subtitle: Text( diff --git a/lib/view/widget/note_detailed_widget.dart b/lib/view/widget/note_detailed_widget.dart index 794c7ea9..10966722 100644 --- a/lib/view/widget/note_detailed_widget.dart +++ b/lib/view/widget/note_detailed_widget.dart @@ -167,6 +167,10 @@ class NoteDetailedWidget extends HookConsumerWidget { }, ), ); + final showAllReactions = ref.watch( + generalSettingsNotifierProvider + .select((settings) => settings.alwaysShowAllReactions), + ); final colors = ref.watch(misskeyColorsProvider(Theme.of(context).brightness)); final style = DefaultTextStyle.of(context).style; @@ -545,6 +549,7 @@ class NoteDetailedWidget extends HookConsumerWidget { ReactionsViewer( account: account, noteId: appearNote.id, + showAllReactions: showAllReactions, ), NoteFooter( account: account, diff --git a/lib/view/widget/note_widget.dart b/lib/view/widget/note_widget.dart index ce3ee814..b524212a 100644 --- a/lib/view/widget/note_widget.dart +++ b/lib/view/widget/note_widget.dart @@ -219,6 +219,10 @@ class NoteWidget extends HookConsumerWidget { isRenote && (isMyRenote || isMyNote || appearNote.myReaction != null), ); + final showAllReactions = ref.watch( + generalSettingsNotifierProvider + .select((settings) => settings.alwaysShowAllReactions), + ); final backgroundColor = this.backgroundColor ?? ref.watch( generalSettingsNotifierProvider.select( @@ -591,6 +595,7 @@ class NoteWidget extends HookConsumerWidget { ReactionsViewer( account: account, noteId: appearNote.id, + showAllReactions: showAllReactions, note: this.note, ), if (showFooter) diff --git a/lib/view/widget/reactions_viewer.dart b/lib/view/widget/reactions_viewer.dart index 047352b0..b46dbd04 100644 --- a/lib/view/widget/reactions_viewer.dart +++ b/lib/view/widget/reactions_viewer.dart @@ -8,6 +8,7 @@ import '../../i18n/strings.g.dart'; import '../../model/account.dart'; import '../../provider/general_settings_notifier_provider.dart'; import '../../provider/note_provider.dart'; +import '../../util/decode_custom_emoji.dart'; import 'reaction_button.dart'; import 'reaction_effect.dart'; @@ -25,6 +26,45 @@ class ReactionsViewer extends HookConsumerWidget { final bool showAllReactions; final Note? note; + Map _mergeReactions(WidgetRef ref, Map reactions) { + final groups = reactions.entries + .groupListsBy((reaction) => reaction.key.startsWith(':')); + final customEmojiReactions = groups[true] + ?.map((reaction) { + final (name, host) = decodeCustomEmoji(reaction.key); + return ( + emoji: reaction.key, + name: name, + host: host, + count: reaction.value, + ); + }) + .groupFoldBy( + (reaction) => reaction.name, + (acc, reaction) => (acc == null || + reaction.host == null || + (acc.host != null && acc.count < reaction.count)) + ? ( + emoji: reaction.emoji, + host: reaction.host, + count: reaction.count, + totalCount: (acc?.totalCount ?? 0) + reaction.count, + ) + : ( + emoji: acc.emoji, + host: acc.host, + count: acc.count, + totalCount: acc.totalCount + reaction.count, + ), + ) + .map((key, value) => MapEntry(value.emoji, value.totalCount)); + return Map.fromEntries( + [...?groups[false], ...?customEmojiReactions?.entries] + .sortedBy((e) => -e.value), + ); + } + void _showReactionEffect( BuildContext context, GlobalKey key, @@ -73,15 +113,31 @@ class ReactionsViewer extends HookConsumerWidget { generalSettingsNotifierProvider .select((settings) => settings.reduceAnimation), ); - final reactions = useState( - Map.fromEntries(note.reactions.entries.sortedBy((e) => -e.value)), + final shouldMergeReactions = ref.watch( + generalSettingsNotifierProvider + .select((settings) => settings.mergeReactionsByName), + ); + final initialReactions = useMemoized( + () { + if (shouldMergeReactions) { + return _mergeReactions(ref, note.reactions); + } else { + return Map.fromEntries( + note.reactions.entries.sortedBy((e) => -e.value), + ); + } + }, + [shouldMergeReactions], ); + final reactions = useState(initialReactions); final keys = useState( - note.reactions.map((key, value) => MapEntry(key, GlobalKey())), + initialReactions.map((key, value) => MapEntry(key, GlobalKey())), ); useEffect( () { - final newSource = Map.of(note.reactions); + final newSource = shouldMergeReactions + ? _mergeReactions(ref, note.reactions) + : Map.of(note.reactions); final newReactions = {}; final emojis = {...note.emojis, ...note.reactionEmojis}; for (final reaction in reactions.value.entries) { diff --git a/lib/view/widget/sub_note_content.dart b/lib/view/widget/sub_note_content.dart index db7baa37..6d80a807 100644 --- a/lib/view/widget/sub_note_content.dart +++ b/lib/view/widget/sub_note_content.dart @@ -72,6 +72,10 @@ class SubNoteContent extends HookConsumerWidget { generalSettingsNotifierProvider .select((settings) => settings.alwaysExpandMediaInSubNote), ); + final showAllReactions = ref.watch( + generalSettingsNotifierProvider + .select((settings) => settings.alwaysShowAllReactions), + ); final colors = ref.watch(misskeyColorsProvider(Theme.of(context).brightness)); final isCollapsed = useState(isLong); @@ -232,6 +236,7 @@ class SubNoteContent extends HookConsumerWidget { ReactionsViewer( account: account, noteId: noteId, + showAllReactions: showAllReactions, note: this.note, ), ], diff --git a/test/view/widget/reactions_viewer_test.dart b/test/view/widget/reactions_viewer_test.dart new file mode 100644 index 00000000..ba26bbd8 --- /dev/null +++ b/test/view/widget/reactions_viewer_test.dart @@ -0,0 +1,247 @@ +import 'package:aria/model/account.dart'; +import 'package:aria/provider/general_settings_notifier_provider.dart'; +import 'package:aria/provider/notes_notifier_provider.dart'; +import 'package:aria/view/widget/reaction_button.dart'; +import 'package:aria/view/widget/reactions_viewer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../test_util/create_overrides.dart'; +import '../../../test_util/dummy_note.dart'; + +Future setupWidget( + WidgetTester tester, { + required Account account, + required String noteId, + bool showAllReactions = false, +}) async { + await tester.pumpWidget( + ProviderScope( + overrides: createOverrides(account), + child: MaterialApp( + home: ReactionsViewer( + account: account, + noteId: noteId, + showAllReactions: showAllReactions, + ), + ), + ), + ); + await tester.pumpAndSettle(); + final container = + ProviderScope.containerOf(tester.element(find.byType(ReactionsViewer))); + return container; +} + +void main() { + group('basic', () { + testWidgets('reactions should be ordered in descending order of the count', + (tester) async { + const account = Account(host: 'misskey.tld'); + final note = dummyNote.copyWith( + reactions: {':emoji1:': 1, ':emoji2:': 3, ':emoji3:': 2}, + ); + final container = await setupWidget( + tester, + account: account, + noteId: note.id, + ); + container.read(notesNotifierProvider(account).notifier).add(note); + await tester.pumpAndSettle(); + final reactions = + tester.widgetList(find.byType(ReactionButton)); + expect(reactions.length, 3); + expect(reactions.elementAt(0).emoji, ':emoji2:'); + expect(reactions.elementAt(1).emoji, ':emoji3:'); + expect(reactions.elementAt(2).emoji, ':emoji1:'); + }); + + testWidgets('should show a limited number of emojis', (tester) async { + const account = Account(host: 'misskey.tld'); + final note = dummyNote.copyWith( + reactions: Map.fromEntries( + List.generate(21, (i) => MapEntry(':emoji$i:', i + 1)), + ), + ); + final container = await setupWidget( + tester, + account: account, + noteId: note.id, + ); + container.read(notesNotifierProvider(account).notifier).add(note); + await tester.pumpAndSettle(); + expect(find.byType(ReactionButton), findsExactly(20)); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); + expect(find.byType(ReactionButton), findsExactly(21)); + expect(find.byType(TextButton), findsNothing); + }); + + testWidgets('should show all emojis if specified', (tester) async { + const account = Account(host: 'misskey.tld'); + final note = dummyNote.copyWith( + reactions: Map.fromEntries( + List.generate(21, (i) => MapEntry(':emoji$i:', i + 1)), + ), + ); + final container = await setupWidget( + tester, + account: account, + noteId: note.id, + showAllReactions: true, + ); + container.read(notesNotifierProvider(account).notifier).add(note); + await tester.pumpAndSettle(); + expect(find.byType(ReactionButton), findsExactly(21)); + expect(find.byType(TextButton), findsNothing); + }); + }); + + group('update', () { + testWidgets('should update reactions count in-place', (tester) async { + const account = Account(host: 'misskey.tld'); + final note = dummyNote.copyWith( + reactions: {':emoji1:': 1, ':emoji2:': 3, ':emoji3:': 2}, + ); + final container = await setupWidget( + tester, + account: account, + noteId: note.id, + ); + container.read(notesNotifierProvider(account).notifier).add(note); + await tester.pumpAndSettle(); + container.read(notesNotifierProvider(account).notifier).add( + note.copyWith( + reactions: {':emoji1:': 5, ':emoji2:': 3, ':emoji3:': 4}, + ), + ); + await tester.pumpAndSettle(); + final reactions = + tester.widgetList(find.byType(ReactionButton)); + expect(reactions.length, 3); + expect(reactions.elementAt(0).emoji, ':emoji2:'); + expect(reactions.elementAt(0).count, 3); + expect(reactions.elementAt(1).emoji, ':emoji3:'); + expect(reactions.elementAt(1).count, 4); + expect(reactions.elementAt(2).emoji, ':emoji1:'); + expect(reactions.elementAt(2).count, 5); + await tester.pumpAndSettle(); + }); + + testWidgets('should append new reactions at last', (tester) async { + const account = Account(host: 'misskey.tld'); + final note = dummyNote.copyWith( + reactions: {':emoji1:': 1, ':emoji2:': 3, ':emoji3:': 2}, + ); + final container = await setupWidget( + tester, + account: account, + noteId: note.id, + ); + container.read(notesNotifierProvider(account).notifier).add(note); + await tester.pumpAndSettle(); + container.read(notesNotifierProvider(account).notifier).add( + note.copyWith( + reactions: { + ':emoji0:': 1, + ':emoji1:': 1, + ':emoji2:': 3, + ':emoji3:': 2, + }, + ), + ); + await tester.pumpAndSettle(); + final reactions = + tester.widgetList(find.byType(ReactionButton)); + expect(reactions.length, 4); + expect(reactions.elementAt(0).emoji, ':emoji2:'); + expect(reactions.elementAt(1).emoji, ':emoji3:'); + expect(reactions.elementAt(2).emoji, ':emoji1:'); + expect(reactions.elementAt(3).emoji, ':emoji0:'); + await tester.pumpAndSettle(); + }); + + testWidgets('should preserve ordering of reactions when removed', + (tester) async { + const account = Account(host: 'misskey.tld'); + final note = dummyNote.copyWith( + reactions: {':emoji1:': 1, ':emoji2:': 3, ':emoji3:': 2}, + ); + final container = await setupWidget( + tester, + account: account, + noteId: note.id, + ); + container.read(notesNotifierProvider(account).notifier).add(note); + await tester.pumpAndSettle(); + container.read(notesNotifierProvider(account).notifier).add( + note.copyWith( + reactions: {':emoji1:': 4, ':emoji2:': 3}, + ), + ); + await tester.pumpAndSettle(); + final reactions = + tester.widgetList(find.byType(ReactionButton)); + expect(reactions.length, 2); + expect(reactions.elementAt(0).emoji, ':emoji2:'); + expect(reactions.elementAt(1).emoji, ':emoji1:'); + await tester.pumpAndSettle(); + }); + }); + + group('merge', () { + testWidgets('should show a local emoji if a local emoji is included', + (tester) async { + const account = Account(host: 'misskey.tld'); + final note = dummyNote.copyWith( + reactions: { + ':emoji@.:': 1, + ':emoji@misskey.tld:': 3, + ':emoji@misskey2.tld:': 2, + }, + ); + final container = await setupWidget( + tester, + account: account, + noteId: note.id, + ); + container.read(notesNotifierProvider(account).notifier).add(note); + await container + .read(generalSettingsNotifierProvider.notifier) + .setMergeReactionsByName(true); + await tester.pumpAndSettle(); + final reaction = + tester.widget(find.byType(ReactionButton)); + expect(reaction.emoji, ':emoji@.:'); + expect(reaction.count, 6); + }); + + testWidgets( + 'should show a most reacted emoji if a local emoji is not included', + (tester) async { + const account = Account(host: 'misskey.tld'); + final note = dummyNote.copyWith( + reactions: { + ':emoji@misskey.tld:': 1, + ':emoji@misskey2.tld:': 3, + ':emoji@misskey3.tld:': 2, + }, + ); + final container = await setupWidget( + tester, + account: account, + noteId: note.id, + ); + container.read(notesNotifierProvider(account).notifier).add(note); + await container + .read(generalSettingsNotifierProvider.notifier) + .setMergeReactionsByName(true); + await tester.pumpAndSettle(); + final reaction = + tester.widget(find.byType(ReactionButton)); + expect(reaction.emoji, ':emoji@misskey2.tld:'); + expect(reaction.count, 6); + }); + }); +}