1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3210 Space-level chats | Enhancement | Save recently used chat reactions and display it in the dedicated section of the reaction picker (#1967)

This commit is contained in:
Evgenii Kozlov 2025-01-04 07:12:08 +01:00 committed by GitHub
parent f38f3fd8d6
commit 5edb38a440
Signed by: github
GPG key ID: B5690EEEBB952194
12 changed files with 183 additions and 16 deletions

View file

@ -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
}

View file

@ -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) {

View file

@ -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<String>)
fun observeRecentlyUsedChatReactions(account: Account): Flow<List<String>>
}

View file

@ -131,4 +131,12 @@ class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSet
) {
cache.setDateFormat(account, format)
}
override suspend fun setRecentlyUsedChatReactions(account: Account, emojis: Set<String>) {
cache.setRecentlyUsedChatReactions(account, emojis)
}
override fun observeRecentlyUsedChatReactions(account: Account,): Flow<List<String>> {
return cache.observeRecentlyUsedChatReactions(account)
}
}

View file

@ -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<Unit, List<String>>(dispatchers.io) {
override fun build(): Flow<List<String>> = flow {
val acc = auth.getCurrentAccount()
emitAll(repo.observeRecentlyUsedChatReactions(acc))
}
override fun build(params: Unit): Flow<List<String>> = flow {
val acc = auth.getCurrentAccount()
emitAll(repo.observeRecentlyUsedChatReactions(acc))
}
}

View file

@ -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<Set<String>, Unit>(dispatchers.io) {
override suspend fun doWork(params: Set<String>) {
val acc = auth.getCurrentAccount()
repo.setRecentlyUsedChatReactions(
account = acc,
emojis = params
)
}
}

View file

@ -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<String>)
fun observeRecentlyUsedChatReactions(account: Account): Flow<List<String>>
}

View file

@ -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<Boolean>(replay = 0)
@ -33,9 +38,37 @@ class ChatReactionViewModel @Inject constructor(
/**
* Default emoji list, including categories.
*/
val default = MutableStateFlow<List<ReactionPickerView>>(emptyList())
private val default = MutableStateFlow<List<ReactionPickerView>>(emptyList())
private val recentlyUsed = MutableStateFlow<List<String>>(emptyList())
val views = combine(default, recentlyUsed) { default, recentlyUsed ->
buildList<ReactionPickerView> {
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 <T : ViewModel> create(modelClass: Class<T>): 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
}
}

View file

@ -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
)
}
}
}
}
}

View file

@ -1861,5 +1861,6 @@ Please provide specific details of your needs here.</string>
<string name="emoji_category_objects">Objects</string>
<string name="emoji_category_symbols">Symbols</string>
<string name="emoji_category_flags">Flags</string>
<string name="emoji_recently_used_section">Recently used</string>
</resources>

View file

@ -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<String>
) {
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<List<String>> {
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

View file

@ -18,6 +18,7 @@ message VaultPreference {
bool showIntroduceVault = 2;
bool isRelativeDates = 3;
optional string dateFormat = 4;
repeated string recentlyUsedChatReactions = 5;
}
message SpacePreference {