From 5edb38a4402b5cdf132bb1ce22ee7aa2a95059c6 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Sat, 4 Jan 2025 07:12:08 +0100 Subject: [PATCH] DROID-3210 Space-level chats | Enhancement | Save recently used chat reactions and display it in the dedicated section of the reaction picker (#1967) --- .../di/feature/discussions/ChatReactionDI.kt | 4 ++ .../anytype/ui/chats/ChatReactionFragment.kt | 4 +- .../data/auth/repo/UserSettingsCache.kt | 3 + .../auth/repo/UserSettingsDataRepository.kt | 8 +++ .../chats/ObserveRecentlyUsedChatReactions.kt | 27 ++++++++ .../chats/SetRecentlyUsedChatReactions.kt | 22 ++++++ .../domain/config/UserSettingsRepository.kt | 3 + .../presentation/ChatReactionViewModel.kt | 68 ++++++++++++++++--- .../ui/ChatReactionPicker.kt | 22 ++++-- localization/src/main/res/values/strings.xml | 1 + .../repo/DefaultUserSettingsCache.kt | 36 +++++++++- persistence/src/main/proto/preferences.proto | 1 + 12 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/chats/ObserveRecentlyUsedChatReactions.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/chats/SetRecentlyUsedChatReactions.kt diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/ChatReactionDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/ChatReactionDI.kt index 0d6c06d8e8..3f0d50476d 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/ChatReactionDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/ChatReactionDI.kt @@ -4,8 +4,10 @@ import androidx.lifecycle.ViewModelProvider import com.anytypeio.anytype.core_utils.di.scope.PerScreen import com.anytypeio.anytype.data.auth.repo.block.BlockRemote import com.anytypeio.anytype.di.common.ComponentDependencies +import com.anytypeio.anytype.domain.auth.repo.AuthRepository import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.UserSettingsRepository import com.anytypeio.anytype.emojifier.data.Emoji import com.anytypeio.anytype.emojifier.data.EmojiProvider import com.anytypeio.anytype.emojifier.suggest.EmojiSuggester @@ -59,4 +61,6 @@ interface ChatReactionPickerDependencies : ComponentDependencies { fun dispatchers(): AppCoroutineDispatchers fun suggester(): EmojiSuggester fun repo(): BlockRepository + fun auth(): AuthRepository + fun prefs(): UserSettingsRepository } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatReactionFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatReactionFragment.kt index 1bbe49e329..372c76b3e5 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatReactionFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatReactionFragment.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.fragment.findNavController import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_utils.ext.arg @@ -22,7 +21,6 @@ import com.anytypeio.anytype.feature_discussions.ui.ChatReactionPicker import com.anytypeio.anytype.ui.settings.typography import javax.inject.Inject import kotlin.getValue -import okhttp3.internal.notify class ChatReactionFragment : BaseBottomSheetComposeFragment() { @@ -45,7 +43,7 @@ class ChatReactionFragment : BaseBottomSheetComposeFragment() { typography = typography ) { ChatReactionPicker( - views = vm.default.collectAsStateWithLifecycle().value, + views = vm.views.collectAsStateWithLifecycle(emptyList()).value, onEmojiClicked = vm::onEmojiClicked ) LaunchedEffect(Unit) { diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/UserSettingsCache.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/UserSettingsCache.kt index 52b63f6a45..0023c2c440 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/UserSettingsCache.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/UserSettingsCache.kt @@ -49,4 +49,7 @@ interface UserSettingsCache { suspend fun setRelativeDates(account: Account, enabled: Boolean) suspend fun setDateFormat(account: Account, format: String) + + suspend fun setRecentlyUsedChatReactions(account: Account, emojis: Set) + fun observeRecentlyUsedChatReactions(account: Account): Flow> } \ No newline at end of file diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/UserSettingsDataRepository.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/UserSettingsDataRepository.kt index 68d85ac188..1865632853 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/UserSettingsDataRepository.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/UserSettingsDataRepository.kt @@ -131,4 +131,12 @@ class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSet ) { cache.setDateFormat(account, format) } + + override suspend fun setRecentlyUsedChatReactions(account: Account, emojis: Set) { + cache.setRecentlyUsedChatReactions(account, emojis) + } + + override fun observeRecentlyUsedChatReactions(account: Account,): Flow> { + return cache.observeRecentlyUsedChatReactions(account) + } } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/chats/ObserveRecentlyUsedChatReactions.kt b/domain/src/main/java/com/anytypeio/anytype/domain/chats/ObserveRecentlyUsedChatReactions.kt new file mode 100644 index 0000000000..de3d99607d --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/chats/ObserveRecentlyUsedChatReactions.kt @@ -0,0 +1,27 @@ +package com.anytypeio.anytype.domain.chats + +import com.anytypeio.anytype.domain.auth.repo.AuthRepository +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.FlowInteractor +import com.anytypeio.anytype.domain.config.UserSettingsRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.emitAll + +class ObserveRecentlyUsedChatReactions @Inject constructor( + private val auth: AuthRepository, + private val repo: UserSettingsRepository, + dispatchers: AppCoroutineDispatchers +) : FlowInteractor>(dispatchers.io) { + + override fun build(): Flow> = flow { + val acc = auth.getCurrentAccount() + emitAll(repo.observeRecentlyUsedChatReactions(acc)) + } + + override fun build(params: Unit): Flow> = flow { + val acc = auth.getCurrentAccount() + emitAll(repo.observeRecentlyUsedChatReactions(acc)) + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/chats/SetRecentlyUsedChatReactions.kt b/domain/src/main/java/com/anytypeio/anytype/domain/chats/SetRecentlyUsedChatReactions.kt new file mode 100644 index 0000000000..87ed108a06 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/chats/SetRecentlyUsedChatReactions.kt @@ -0,0 +1,22 @@ +package com.anytypeio.anytype.domain.chats + +import com.anytypeio.anytype.domain.auth.repo.AuthRepository +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor +import com.anytypeio.anytype.domain.config.UserSettingsRepository +import javax.inject.Inject + +class SetRecentlyUsedChatReactions @Inject constructor( + private val auth: AuthRepository, + private val repo: UserSettingsRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor, Unit>(dispatchers.io) { + + override suspend fun doWork(params: Set) { + val acc = auth.getCurrentAccount() + repo.setRecentlyUsedChatReactions( + account = acc, + emojis = params + ) + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/config/UserSettingsRepository.kt b/domain/src/main/java/com/anytypeio/anytype/domain/config/UserSettingsRepository.kt index bbf6708220..3421db1ba9 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/config/UserSettingsRepository.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/config/UserSettingsRepository.kt @@ -53,4 +53,7 @@ interface UserSettingsRepository { suspend fun setRelativeDates(account: Account, enabled: Boolean) suspend fun setDateFormat(account: Account, format: String) + + suspend fun setRecentlyUsedChatReactions(account: Account, emojis: Set) + fun observeRecentlyUsedChatReactions(account: Account): Flow> } \ No newline at end of file diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/ChatReactionViewModel.kt b/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/ChatReactionViewModel.kt index 2cd2af7cac..480bc2b1af 100644 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/ChatReactionViewModel.kt +++ b/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/ChatReactionViewModel.kt @@ -5,27 +5,32 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.core_models.Command import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_ui.features.editor.holders.text.Toggle import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.onFailure +import com.anytypeio.anytype.domain.chats.ObserveRecentlyUsedChatReactions +import com.anytypeio.anytype.domain.chats.SetRecentlyUsedChatReactions import com.anytypeio.anytype.domain.chats.ToggleChatMessageReaction import com.anytypeio.anytype.emojifier.Emojifier import com.anytypeio.anytype.emojifier.data.Emoji import com.anytypeio.anytype.emojifier.data.EmojiProvider import com.anytypeio.anytype.emojifier.suggest.EmojiSuggester import com.anytypeio.anytype.presentation.common.BaseViewModel -import com.anytypeio.anytype.presentation.editor.picker.EmojiPickerView import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber class ChatReactionViewModel @Inject constructor( private val vmParams: Params, private val provider: EmojiProvider, private val suggester: EmojiSuggester, private val dispatchers: AppCoroutineDispatchers, - private val toggleChatMessageReaction: ToggleChatMessageReaction + private val toggleChatMessageReaction: ToggleChatMessageReaction, + private val setRecentlyUsedChatReactions: SetRecentlyUsedChatReactions, + private val observeRecentlyUsedChatReactions: ObserveRecentlyUsedChatReactions ) : BaseViewModel() { val isDismissed = MutableSharedFlow(replay = 0) @@ -33,9 +38,37 @@ class ChatReactionViewModel @Inject constructor( /** * Default emoji list, including categories. */ - val default = MutableStateFlow>(emptyList()) + private val default = MutableStateFlow>(emptyList()) + + private val recentlyUsed = MutableStateFlow>(emptyList()) + + val views = combine(default, recentlyUsed) { default, recentlyUsed -> + buildList { + if (recentlyUsed.isNotEmpty()) { + add(ReactionPickerView.RecentUsedSection) + addAll( + recentlyUsed.map { unicode -> + ReactionPickerView.Emoji( + unicode = unicode, + page = -1, + index = -1, + emojified = Emojifier.safeUri(unicode) + ) + } + ) + } + addAll(default) + } + } init { + viewModelScope.launch { + observeRecentlyUsedChatReactions + .flow() + .collect { + recentlyUsed.value = it + } + } viewModelScope.launch { val loaded = loadEmojiWithCategories() default.value = loaded @@ -72,13 +105,23 @@ class ChatReactionViewModel @Inject constructor( fun onEmojiClicked(emoji: String) { viewModelScope.launch { - toggleChatMessageReaction.execute( + setRecentlyUsedChatReactions.async( + params = (listOf(emoji) + recentlyUsed.value) + .toSet() + .take(MAX_RECENTLY_USED_COUNT) + .toSet() + ).onFailure { + Timber.e(it, "Error while saving recently used reactions") + } + toggleChatMessageReaction.async( params = Command.ChatCommand.ToggleMessageReaction( msg = vmParams.msg, chat = vmParams.chat, emoji = emoji ) - ) + ).onFailure { + Timber.e(it, "Error while toggling chat message reaction") + } isDismissed.emit(true) } } @@ -88,7 +131,9 @@ class ChatReactionViewModel @Inject constructor( private val emojiProvider: EmojiProvider, private val emojiSuggester: EmojiSuggester, private val dispatchers: AppCoroutineDispatchers, - private val toggleChatMessageReaction: ToggleChatMessageReaction + private val toggleChatMessageReaction: ToggleChatMessageReaction, + private val setRecentlyUsedChatReactions: SetRecentlyUsedChatReactions, + private val observeRecentlyUsedChatReactions: ObserveRecentlyUsedChatReactions ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = ChatReactionViewModel( @@ -96,7 +141,9 @@ class ChatReactionViewModel @Inject constructor( provider = emojiProvider, suggester = emojiSuggester, dispatchers = dispatchers, - toggleChatMessageReaction = toggleChatMessageReaction + toggleChatMessageReaction = toggleChatMessageReaction, + setRecentlyUsedChatReactions = setRecentlyUsedChatReactions, + observeRecentlyUsedChatReactions = observeRecentlyUsedChatReactions ) as T } @@ -106,6 +153,7 @@ class ChatReactionViewModel @Inject constructor( ) sealed class ReactionPickerView { + data object RecentUsedSection: ReactionPickerView() data class Category(val index: Int) : ReactionPickerView() data class Emoji( val unicode: String, @@ -114,4 +162,8 @@ class ChatReactionViewModel @Inject constructor( val emojified: String = "" ) : ReactionPickerView() } + + companion object { + const val MAX_RECENTLY_USED_COUNT = 20 + } } \ No newline at end of file diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/ChatReactionPicker.kt b/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/ChatReactionPicker.kt index 830b4dbfdf..822fd9bce7 100644 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/ChatReactionPicker.kt +++ b/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/ChatReactionPicker.kt @@ -17,6 +17,8 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -42,7 +44,7 @@ fun ChatReactionPicker( LazyVerticalGrid( columns = GridCells.Fixed(6), modifier = Modifier - .systemBarsPadding() + .nestedScroll(rememberNestedScrollInteropConnection()) .fillMaxSize() .padding( start = 16.dp, @@ -58,8 +60,7 @@ fun ChatReactionPicker( is ReactionPickerView.Emoji -> { GridItemSpan(1) } - - is ReactionPickerView.Category -> { + is ReactionPickerView.Category, is ReactionPickerView.RecentUsedSection -> { GridItemSpan(maxLineSpan) } } @@ -84,7 +85,6 @@ fun ChatReactionPicker( ) } } - is ReactionPickerView.Category -> { Box( modifier = Modifier @@ -110,6 +110,20 @@ fun ChatReactionPicker( ) } } + is ReactionPickerView.RecentUsedSection -> { + Box( + modifier = Modifier + .height(48.dp) + .fillMaxWidth() + ) { + Text( + text = stringResource(R.string.emoji_recently_used_section), + color = colorResource(R.color.text_secondary), + modifier = Modifier.align(Alignment.Center), + style = Caption1Medium + ) + } + } } } } diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 33141643a4..e380ca9bdd 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -1861,5 +1861,6 @@ Please provide specific details of your needs here. Objects Symbols Flags + Recently used \ No newline at end of file diff --git a/persistence/src/main/java/com/anytypeio/anytype/persistence/repo/DefaultUserSettingsCache.kt b/persistence/src/main/java/com/anytypeio/anytype/persistence/repo/DefaultUserSettingsCache.kt index c2ff6612a8..1c46e3a8bd 100644 --- a/persistence/src/main/java/com/anytypeio/anytype/persistence/repo/DefaultUserSettingsCache.kt +++ b/persistence/src/main/java/com/anytypeio/anytype/persistence/repo/DefaultUserSettingsCache.kt @@ -50,7 +50,8 @@ class DefaultUserSettingsCache( return VaultPreference( showIntroduceVault = DEFAULT_SHOW_INTRODUCE_VAULT, isRelativeDates = DEFAULT_RELATIVE_DATES, - dateFormat = appDefaultDateFormatProvider.provide() + dateFormat = appDefaultDateFormatProvider.provide(), + orderOfSpaces = emptyList() ) } //endregion @@ -486,6 +487,39 @@ class DefaultUserSettingsCache( } } + override suspend fun setRecentlyUsedChatReactions( + account: Account, + emojis: Set + ) { + context.vaultPrefsStore.updateData { existingPreferences -> + val curr = existingPreferences.preferences.getOrDefault( + key = account.id, + defaultValue = initialVaultSettings() + ) + existingPreferences.copy( + preferences = existingPreferences.preferences + mapOf( + account.id to curr.copy( + recentlyUsedChatReactions = emojis.toList() + ) + ) + ) + } + } + + override fun observeRecentlyUsedChatReactions(account: Account): Flow> { + return context + .vaultPrefsStore + .data + .map { existing -> + val settings = existing.preferences[account.id] + if (settings != null) { + settings.recentlyUsedChatReactions + } else { + emptyList() + } + } + } + override suspend fun setDateFormat( account: Account, format: String diff --git a/persistence/src/main/proto/preferences.proto b/persistence/src/main/proto/preferences.proto index ec28c517a8..7e5b3e8e6f 100644 --- a/persistence/src/main/proto/preferences.proto +++ b/persistence/src/main/proto/preferences.proto @@ -18,6 +18,7 @@ message VaultPreference { bool showIntroduceVault = 2; bool isRelativeDates = 3; optional string dateFormat = 4; + repeated string recentlyUsedChatReactions = 5; } message SpacePreference {