diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt index 2a40ecbaa7..7941af8ae7 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt @@ -105,6 +105,7 @@ class SpaceSettingsFragment : Fragment(), ObjectTypeSelectionListener { NewSpaceSettingsScreen( uiState = vm.uiState.collectAsStateWithLifecycle().value, uiWallpaperState = vm.spaceWallpapers.collectAsStateWithLifecycle().value, + chatsWithCustomNotifications = vm.chatsWithCustomNotifications.collectAsStateWithLifecycle().value, locale = locale, uiEvent = vm::onUiEvent ) @@ -208,6 +209,16 @@ class SpaceSettingsFragment : Fragment(), ObjectTypeSelectionListener { } } + override fun onStart() { + super.onStart() + vm.onStart() + } + + override fun onStop() { + super.onStop() + vm.onStop() + } + private suspend fun observeCommands( showNotificationPermissionDialog: MutableState, showWallpaperPicker: MutableState diff --git a/core-ui/src/main/res/drawable/ic_notification_status_clear_24.xml b/core-ui/src/main/res/drawable/ic_notification_status_clear_24.xml new file mode 100644 index 0000000000..78392007a7 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_notification_status_clear_24.xml @@ -0,0 +1,18 @@ + + + + diff --git a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/NewSettings.kt b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/NewSettings.kt index b46a75f99a..3449f692c0 100644 --- a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/NewSettings.kt +++ b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/NewSettings.kt @@ -51,6 +51,7 @@ import com.anytypeio.anytype.core_ui.views.Caption1Regular import com.anytypeio.anytype.core_ui.views.HeadlineHeading import com.anytypeio.anytype.core_ui.views.PreviewTitle1Medium import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK +import com.anytypeio.anytype.presentation.spaces.ChatNotificationItem import com.anytypeio.anytype.presentation.spaces.UiEvent import com.anytypeio.anytype.presentation.spaces.UiEvent.OnChangeSpaceType.* import com.anytypeio.anytype.presentation.spaces.UiEvent.OnDefaultObjectTypeClicked @@ -68,6 +69,7 @@ import timber.log.Timber fun NewSpaceSettingsScreen( uiState: UiSpaceSettingsState, uiWallpaperState: List, + chatsWithCustomNotifications: List, locale: Locale?, uiEvent: (UiEvent) -> Unit ) { @@ -586,10 +588,8 @@ fun NewSpaceSettingsScreen( NotificationsPreferenceSheet( targetSpaceId = uiState.targetSpaceId, currentState = uiState.notificationState, - uiEvent = { - showNotificationsSettings = false - uiEvent(it) - }, + chatsWithCustomNotifications = chatsWithCustomNotifications, + uiEvent = uiEvent, onDismiss = { showNotificationsSettings = false } diff --git a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/NotificationsPreferenceSheet.kt b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/NotificationsPreferenceSheet.kt index 2ccccae057..f3f22d8af2 100644 --- a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/NotificationsPreferenceSheet.kt +++ b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/NotificationsPreferenceSheet.kt @@ -3,16 +3,24 @@ package com.anytypeio.anytype.ui_settings.space.new_settings import android.os.Build import androidx.compose.foundation.Image import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -22,15 +30,23 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.chats.NotificationState import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.foundation.Divider import com.anytypeio.anytype.core_ui.foundation.Dragger +import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.views.BodyRegular +import com.anytypeio.anytype.core_ui.views.Caption1Medium +import com.anytypeio.anytype.core_ui.views.Relations3 import com.anytypeio.anytype.core_ui.views.Title1 +import com.anytypeio.anytype.core_ui.views.Title2 +import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK +import com.anytypeio.anytype.presentation.objects.ObjectIcon +import com.anytypeio.anytype.presentation.spaces.ChatNotificationItem import com.anytypeio.anytype.presentation.spaces.UiEvent @OptIn(ExperimentalMaterial3Api::class) @@ -38,6 +54,7 @@ import com.anytypeio.anytype.presentation.spaces.UiEvent fun NotificationsPreferenceSheet( targetSpaceId: String?, currentState: NotificationState, + chatsWithCustomNotifications: List, uiEvent: (UiEvent) -> Unit, onDismiss: () -> Unit ) { @@ -57,45 +74,109 @@ fun NotificationsPreferenceSheet( dragHandle = {}, shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), ) { - Column( - modifier = Modifier.fillMaxWidth() + LazyColumn( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - Dragger( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(vertical = 6.dp) - ) - Text( - text = stringResource(R.string.notifications_title), - style = Title1, - color = colorResource(id = R.color.text_primary), - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(horizontal = 24.dp, vertical = 8.dp) - ) - NotificationOption( - title = stringResource(R.string.notifications_all), - checked = currentState == NotificationState.ALL, - onClick = { uiEvent(UiEvent.OnNotificationsSetting.All(targetSpaceId)) } - ) - Divider( - paddingStart = 16.dp, - paddingEnd = 16.dp, - ) - NotificationOption( - title = stringResource(R.string.notifications_mentions), - checked = currentState == NotificationState.MENTIONS, - onClick = { uiEvent(UiEvent.OnNotificationsSetting.Mentions(targetSpaceId)) } - ) - Divider( - paddingStart = 16.dp, - paddingEnd = 16.dp, - ) - NotificationOption( - title = stringResource(R.string.notifications_disable), - checked = currentState == NotificationState.DISABLE, - onClick = { uiEvent(UiEvent.OnNotificationsSetting.None(targetSpaceId)) } - ) + item { + Dragger( + modifier = Modifier + .padding(vertical = 6.dp) + ) + } + item { + Text( + text = stringResource(R.string.notifications_title), + style = Title1, + color = colorResource(id = R.color.text_primary), + modifier = Modifier + .padding(horizontal = 24.dp, vertical = 12.dp) + ) + } + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + ) { + Text( + text = stringResource(R.string.notify_me_about), + style = Caption1Medium, + color = colorResource(id = R.color.text_secondary), + modifier = Modifier + .align(Alignment.BottomStart) + .padding(start = 16.dp, bottom = 12.dp) + ) + } + } + item { + NotificationOption( + title = stringResource(R.string.notifications_all), + checked = currentState == NotificationState.ALL, + onClick = { uiEvent(UiEvent.OnNotificationsSetting.All(targetSpaceId)) } + ) + } + item { + Divider( + paddingStart = 16.dp, + paddingEnd = 16.dp, + ) + } + item { + NotificationOption( + title = stringResource(R.string.notifications_mentions), + checked = currentState == NotificationState.MENTIONS, + onClick = { uiEvent(UiEvent.OnNotificationsSetting.Mentions(targetSpaceId)) } + ) + } + item { + Divider( + paddingStart = 16.dp, + paddingEnd = 16.dp, + ) + } + item { + NotificationOption( + title = stringResource(R.string.notifications_disable), + checked = currentState == NotificationState.DISABLE, + onClick = { uiEvent(UiEvent.OnNotificationsSetting.None(targetSpaceId)) } + ) + } + + // Show chats with custom notification settings + if (chatsWithCustomNotifications.isNotEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + ) { + Text( + text = stringResource(R.string.chat_specific_notifications), + style = Caption1Medium, + color = colorResource(id = R.color.text_secondary), + modifier = Modifier + .align(Alignment.BottomStart) + .padding(start = 16.dp, bottom = 12.dp) + ) + } + } + items(chatsWithCustomNotifications) { chatItem -> + ChatNotificationItem( + chatItem = chatItem, + onResetClick = { + uiEvent(UiEvent.OnResetChatNotification(chatItem.id)) + } + ) + Divider( + paddingStart = 16.dp, + paddingEnd = 16.dp, + ) + } + item { + Spacer(modifier = Modifier.height(16.dp)) + } + } } } } @@ -110,7 +191,7 @@ fun NotificationOption( modifier = Modifier .fillMaxWidth() .clickable { onClick() } - .padding(horizontal = 24.dp, vertical = 16.dp), + .padding(horizontal = 16.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically ) { Text( @@ -128,12 +209,82 @@ fun NotificationOption( } } +@Composable +fun ChatNotificationItem( + chatItem: ChatNotificationItem, + onResetClick: () -> Unit +) { + Column( + modifier = Modifier + .height(72.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.Center + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + ListWidgetObjectIcon( + icon = chatItem.icon, + modifier = Modifier.padding(end = 12.dp), + iconSize = 48.dp, + ) + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = chatItem.name, + style = Title2, + color = colorResource(id = R.color.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = when (chatItem.customState) { + NotificationState.ALL -> stringResource(R.string.notifications_all_short) + NotificationState.MENTIONS -> stringResource(R.string.notifications_mentions_short) + NotificationState.DISABLE -> stringResource(R.string.notifications_disable_short) + }, + style = Relations3, + color = colorResource(id = R.color.text_secondary) + ) + } + Icon( + painter = painterResource(id = R.drawable.ic_notification_status_clear_24), + contentDescription = "Reset to space default", + tint = colorResource(id = R.color.control_secondary), + modifier = Modifier + .size(24.dp) + .noRippleClickable { + onResetClick() + } + ) + } + } +} + @DefaultPreviews @Composable fun NotificationsPreferenceSheetPreview() { NotificationsPreferenceSheet( targetSpaceId = "space_view_id", currentState = NotificationState.ALL, + chatsWithCustomNotifications = listOf( + ChatNotificationItem( + id = "chat1", + name = "Team Chat", + customState = NotificationState.MENTIONS, + icon = ObjectIcon.TypeIcon.Default.DEFAULT + ), + ChatNotificationItem( + id = "chat2", + name = "Project Discussion", + customState = NotificationState.DISABLE, + icon = ObjectIcon.TypeIcon.Default.DEFAULT + ) + ), uiEvent = {}, onDismiss = {} ) diff --git a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/Previews.kt b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/Previews.kt index 536bbc35a9..3d89b072e6 100644 --- a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/Previews.kt +++ b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/Previews.kt @@ -73,6 +73,7 @@ fun NewSpaceSettingsScreenPreview() { WallpaperView.SolidColor(isSelected = false, code = WallpaperColor.PURPLE.code), WallpaperView.SolidColor(isSelected = false, code = WallpaperColor.RED.code), ), + chatsWithCustomNotifications = emptyList(), locale = Locale.getDefault() ) } \ No newline at end of file diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 9faca14c32..8a338423ac 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -2239,9 +2239,9 @@ Please provide specific details of your needs here. Camera permission denied Notifications - All activity + All new messages Mentions only - Disable notifications + Nothing All Mentions Disable @@ -2347,5 +2347,7 @@ Please provide specific details of your needs here. Upload Image Receive all Mute all + Chat specific notifications + Notify me about \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt index 98ed611349..108b9a0ee7 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt @@ -14,8 +14,11 @@ import com.anytypeio.anytype.analytics.base.sendEvent import com.anytypeio.anytype.analytics.event.EventAnalytics import com.anytypeio.anytype.analytics.props.Props import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.DVFilter +import com.anytypeio.anytype.core_models.DVFilterCondition import com.anytypeio.anytype.core_models.Filepath import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relations @@ -31,7 +34,6 @@ import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.primitives.TypeId import com.anytypeio.anytype.core_models.primitives.TypeKey import com.anytypeio.anytype.domain.auth.interactor.GetAccount -import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.cover.GetCoverGradientCollection import com.anytypeio.anytype.domain.device.DeviceTokenStoringService @@ -39,6 +41,8 @@ import com.anytypeio.anytype.domain.invite.GetCurrentInviteAccessLevel import com.anytypeio.anytype.domain.invite.SpaceInviteLinkStore import com.anytypeio.anytype.domain.launch.GetDefaultObjectType import com.anytypeio.anytype.domain.launch.SetDefaultObjectType +import com.anytypeio.anytype.domain.library.StoreSearchParams +import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.media.UploadFile import com.anytypeio.anytype.domain.misc.AppActionManager import com.anytypeio.anytype.domain.misc.UrlBuilder @@ -47,10 +51,10 @@ import com.anytypeio.anytype.domain.multiplayer.CopyInviteLinkToClipboard import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.multiplayer.sharedSpaceCount +import com.anytypeio.anytype.domain.notifications.ResetSpaceChatNotification import com.anytypeio.anytype.domain.notifications.SetSpaceNotificationMode -import com.anytypeio.anytype.domain.`object`.FetchObject -import com.anytypeio.anytype.domain.`object`.SetObjectDetails import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.getTypeOfObject import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager import com.anytypeio.anytype.domain.spaces.DeleteSpace import com.anytypeio.anytype.domain.spaces.SetSpaceDetails @@ -63,8 +67,8 @@ import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.mapper.objectIcon import com.anytypeio.anytype.presentation.multiplayer.SpaceLimitsState import com.anytypeio.anytype.presentation.multiplayer.spaceLimitsState -import com.anytypeio.anytype.presentation.multiplayer.toSpaceMemberView import com.anytypeio.anytype.presentation.notifications.NotificationPermissionManager +import com.anytypeio.anytype.presentation.notifications.NotificationStateCalculator import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.spaces.SpaceSettingsViewModel.Command.ManageBin import com.anytypeio.anytype.presentation.spaces.SpaceSettingsViewModel.Command.ManageRemoteStorage @@ -90,6 +94,7 @@ import com.anytypeio.anytype.presentation.wallpaper.WallpaperView import com.anytypeio.anytype.presentation.wallpaper.computeWallpaperResult import com.anytypeio.anytype.presentation.wallpaper.getSpaceIconColor import javax.inject.Inject +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.catch @@ -104,7 +109,6 @@ class SpaceSettingsViewModel( private val analytics: Analytics, private val setSpaceDetails: SetSpaceDetails, private val spaceManager: SpaceManager, - private val gradientProvider: SpaceGradientProvider, private val urlBuilder: UrlBuilder, private val deleteSpace: DeleteSpace, private val spaceGradientProvider: SpaceGradientProvider, @@ -119,8 +123,6 @@ class SpaceSettingsViewModel( private val storeOfObjectTypes: StoreOfObjectTypes, private val appActionManager: AppActionManager, private val copyInviteLinkToClipboard: CopyInviteLinkToClipboard, - private val fetchObject: FetchObject, - private val setObjectDetails: SetObjectDetails, private val getAccount: GetAccount, private val notificationPermissionManager: NotificationPermissionManager, private val setSpaceNotificationMode: SetSpaceNotificationMode, @@ -129,8 +131,9 @@ class SpaceSettingsViewModel( private val spaceInviteLinkStore: SpaceInviteLinkStore, private val getGradients: GetCoverGradientCollection, private val setWallpaper: SetWallpaper, - private val stringResourceProvider: StringResourceProvider -): BaseViewModel() { + private val storelessSubscriptionContainer: StorelessSubscriptionContainer, + private val resetSpaceChatNotification: ResetSpaceChatNotification +) : BaseViewModel() { val commands = MutableSharedFlow() val isDismissed = MutableStateFlow(false) @@ -142,14 +145,22 @@ class SpaceSettingsViewModel( val _notificationState = MutableStateFlow(NotificationState.ALL) val uiQrCodeState = MutableStateFlow(UiSpaceQrCodeState.Hidden) - + private val spaceInfoTitleClickCount = MutableStateFlow(0) - val inviteLinkAccessLevel = MutableStateFlow(SpaceInviteLinkAccessLevel.LinkDisabled()) + val inviteLinkAccessLevel = + MutableStateFlow(SpaceInviteLinkAccessLevel.LinkDisabled()) val spaceWallpapers = MutableStateFlow>(listOf()) val spaceSettingsErrors = MutableStateFlow(SpaceSettingsErrors.Hidden) + /** + * StateFlow containing chats that have custom notification states different from the space default. + * These chats can be reset to use the space default notification setting. + */ + val chatsWithCustomNotifications = MutableStateFlow>(emptyList()) + private var chatNotificationsJob: Job? = null + init { Timber.d("SpaceSettingsViewModel, Init, vmParams: $vmParams") viewModelScope.launch { @@ -161,6 +172,23 @@ class SpaceSettingsViewModel( subscribeToInviteLinkState() } + fun onStart() { + chatNotificationsJob = subscribeToChatsWithCustomNotifications() + } + + fun onStop() { + chatNotificationsJob?.cancel() + chatNotificationsJob = null + viewModelScope.launch { + storelessSubscriptionContainer.unsubscribe( + listOf( + getSpaceChatsCustomNotificationsSubscriptionId() + ) + ) + chatsWithCustomNotifications.value = emptyList() + } + } + private fun proceedWithObservingSpaceView() { val restrictions = combine( @@ -253,18 +281,20 @@ class SpaceSettingsViewModel( val targetSpaceId = spaceView.targetSpaceId - val spaceCreator = if (spaceMembers is ActiveSpaceMemberSubscriptionContainer.Store.Data) { - spaceMembers.members.find { it.id == spaceView.getValue(Relations.CREATOR) } - } else { - null - } + val spaceCreator = + if (spaceMembers is ActiveSpaceMemberSubscriptionContainer.Store.Data) { + spaceMembers.members.find { it.id == spaceView.getValue(Relations.CREATOR) } + } else { + null + } val createdByNameOrId = spaceCreator?.globalName?.takeIf { it.isNotEmpty() } ?: spaceCreator?.identity // Calculate active members count (excluding JOINING) and limits state val (spaceMemberCount, spaceLimitsState) = if (spaceMembers is ActiveSpaceMemberSubscriptionContainer.Store.Data) { // Count only ACTIVE members for display - val activeMemberCount = spaceMembers.members.count { it.status == ParticipantStatus.ACTIVE } + val activeMemberCount = + spaceMembers.members.count { it.status == ParticipantStatus.ACTIVE } activeMemberCount to spaceView.spaceLimitsState( spaceMembers = spaceMembers.members, @@ -277,11 +307,12 @@ class SpaceSettingsViewModel( } // Count members with JOINING status (pending join requests) - val requests: Int = if (spaceMembers is ActiveSpaceMemberSubscriptionContainer.Store.Data) { - spaceMembers.members.count { it.status == ParticipantStatus.JOINING } - } else { - 0 - } + val requests: Int = + if (spaceMembers is ActiveSpaceMemberSubscriptionContainer.Store.Data) { + spaceMembers.members.count { it.status == ParticipantStatus.JOINING } + } else { + 0 + } val deviceToken = if (BuildConfig.DEBUG || clickCount >= 5) { try { @@ -298,8 +329,7 @@ class SpaceSettingsViewModel( createdBy = createdByNameOrId.orEmpty(), creationDateInMillis = spaceView .getValue(Relations.CREATED_DATE) - ?.let { timeInSeconds -> (timeInSeconds * 1000L).toLong() } - , + ?.let { timeInSeconds -> (timeInSeconds * 1000L).toLong() }, networkId = spaceManager.getConfig(vmParams.space)?.network.orEmpty(), isDebugVisible = BuildConfig.DEBUG || clickCount >= 5, deviceToken = deviceToken @@ -322,6 +352,7 @@ class SpaceSettingsViewModel( add(Spacer(height = 4)) add(MembersSmall(count = spaceMemberCount)) } + SpaceAccessType.DEFAULT, null -> { add(Spacer(height = 4)) add(EntrySpace) @@ -345,6 +376,7 @@ class SpaceSettingsViewModel( ) ) } + is SpaceInviteLinkAccessLevel.RequestAccess -> { add(Spacer(height = 24)) add(InviteLink(inviteLink.link)) @@ -357,6 +389,7 @@ class SpaceSettingsViewModel( ) ) } + is SpaceInviteLinkAccessLevel.ViewerAccess -> { add(Spacer(height = 24)) add(InviteLink(inviteLink.link)) @@ -369,6 +402,7 @@ class SpaceSettingsViewModel( ) ) } + is SpaceInviteLinkAccessLevel.LinkDisabled -> { add(UiSpaceSettingsItem.Section.Collaboration) add( @@ -404,7 +438,12 @@ class SpaceSettingsViewModel( add(UiSpaceSettingsItem.Section.Preferences) add(defaultObjectTypeSettingItem) add(Spacer(height = 8)) - add(UiSpaceSettingsItem.Wallpapers(wallpaper = wallpaperResult, spaceIconView = spaceIcon)) + add( + UiSpaceSettingsItem.Wallpapers( + wallpaper = wallpaperResult, + spaceIconView = spaceIcon + ) + ) if (permission?.isOwnerOrEditor() == true) { add(UiSpaceSettingsItem.Section.DataManagement) @@ -440,32 +479,39 @@ class SpaceSettingsViewModel( fun onUiEvent(uiEvent: UiEvent) { Timber.d("onUiEvent: $uiEvent") - when(uiEvent) { + when (uiEvent) { UiEvent.IconMenu.OnRemoveIconClicked -> { proceedWithRemovingSpaceIcon() } + UiEvent.IconMenu.OnChangeIconColorClicked -> { proceedWithUpdateSpaceIconColor() } + UiEvent.OnBackPressed -> { isDismissed.value = true } + UiEvent.OnDeleteSpaceClicked -> { viewModelScope.launch { commands.emit(ShowDeleteSpaceWarning) } } + UiEvent.OnLeaveSpaceClicked -> { viewModelScope.launch { commands.emit(ShowLeaveSpaceWarning) } } + UiEvent.OnRemoteStorageClick -> { viewModelScope.launch { commands.emit(ManageRemoteStorage) } } + UiEvent.OnBinClick -> { viewModelScope.launch { commands.emit(ManageBin(vmParams.space)) } } + UiEvent.OnInviteClicked -> { viewModelScope.launch { commands.emit( @@ -473,9 +519,11 @@ class SpaceSettingsViewModel( ) } } + UiEvent.OnPersonalizationClicked -> { sendToast("Coming soon") } + is UiEvent.OnQrCodeClicked -> { viewModelScope.launch { val (spaceName, spaceIcon) = when (val state = uiState.value) { @@ -486,6 +534,7 @@ class SpaceSettingsViewModel( .firstOrNull()?.icon name to icon } + else -> "" to null } uiQrCodeState.value = SpaceInvite( @@ -503,6 +552,7 @@ class SpaceSettingsViewModel( ) } } + is UiEvent.OnSaveDescriptionClicked -> { viewModelScope.launch { setSpaceDetails.async( @@ -515,6 +565,7 @@ class SpaceSettingsViewModel( ) } } + is UiEvent.OnSaveTitleClicked -> { viewModelScope.launch { setSpaceDetails.async( @@ -527,14 +578,17 @@ class SpaceSettingsViewModel( ) } } + is UiEvent.OnSpaceImagePicked -> { proceedWithSettingSpaceImage(uiEvent.uri) } + is UiEvent.OnSpaceMembersClicked -> { viewModelScope.launch { commands.emit(ManageSharedSpace(vmParams.space)) } } + is UiEvent.OnDefaultObjectTypeClicked -> { viewModelScope.launch { commands.emit( @@ -548,11 +602,13 @@ class SpaceSettingsViewModel( ) } } + UiEvent.OnObjectTypesClicked -> { viewModelScope.launch { commands.emit(OpenTypesScreen(vmParams.space)) } } + UiEvent.OnPropertiesClicked -> { viewModelScope.launch { commands.emit(OpenPropertiesScreen(vmParams.space)) @@ -569,11 +625,13 @@ class SpaceSettingsViewModel( } ) } + UiEvent.OnDebugClicked -> { viewModelScope.launch { commands.emit(OpenDebugScreen(vmParams.space.id)) } } + UiEvent.OnSpaceInfoTitleClicked -> { val currentCount = spaceInfoTitleClickCount.value spaceInfoTitleClickCount.value = currentCount + 1 @@ -604,6 +662,7 @@ class SpaceSettingsViewModel( ) } } + is UiEvent.OnShareLinkClicked -> { viewModelScope.launch { // Analytics Event #6: ClickShareSpaceShareLink @@ -614,27 +673,36 @@ class SpaceSettingsViewModel( ) } } + is UiEvent.OnUpdateWallpaperClicked -> { proceedWithWallpaperUpdate(uiEvent) } + UiEvent.OnAddMoreSpacesClicked -> { viewModelScope.launch { commands.emit(Command.NavigateToMembership) } } + UiEvent.OnChangeTypeClicked -> { // This event opens the bottom sheet, no action needed in ViewModel } + is UiEvent.OnChangeSpaceType -> { when (uiEvent) { is UiEvent.OnChangeSpaceType.ToChat -> { proceedWithChangingSpaceType(SpaceUxType.CHAT) } + is UiEvent.OnChangeSpaceType.ToSpace -> { proceedWithChangingSpaceType(SpaceUxType.DATA) } } } + + is UiEvent.OnResetChatNotification -> { + onResetChatNotification(uiEvent.chatId) + } } } @@ -670,12 +738,14 @@ class SpaceSettingsViewModel( code = newWallpaper.code ) } + is WallpaperView.SolidColor -> { SetWallpaper.Params.SolidColor( space = vmParams.space.id, code = newWallpaper.code ) } + is WallpaperView.SpaceColor -> { SetWallpaper.Params.Clear( space = vmParams.space.id @@ -825,7 +895,7 @@ class SpaceSettingsViewModel( Timber.e(it, "Error while setting default object type") }, onSuccess = { - when(val state = uiState.value) { + when (val state = uiState.value) { is UiSpaceSettingsState.SpaceSettings -> { uiState.value = state.copy( items = state.items.map { item -> @@ -841,6 +911,7 @@ class SpaceSettingsViewModel( } ) } + else -> { Timber.w("Unexpected ui state when updating object type: $state") } @@ -913,7 +984,7 @@ class SpaceSettingsViewModel( commands.emit(Command.RequestNotificationPermission) return@launch } - + // Call backend to set notification state setSpaceNotificationMode.async( SetSpaceNotificationMode.Params( @@ -985,6 +1056,91 @@ class SpaceSettingsViewModel( } } + private fun getSpaceChatsCustomNotificationsSubscriptionId(): String { + return "space-chats-custom-notifications-${vmParams.space.id}" + } + + /** + * Subscribes to chats with custom notification states and filters them to show only those + * with notification states different from the space default. + */ + private fun subscribeToChatsWithCustomNotifications(): Job { + return viewModelScope.launch { + // Observe all chat objects in the space + val searchParams = StoreSearchParams( + space = vmParams.space, + subscription = getSpaceChatsCustomNotificationsSubscriptionId(), + filters = listOf( + DVFilter( + relation = Relations.LAYOUT, + condition = DVFilterCondition.EQUAL, + value = ObjectType.Layout.CHAT_DERIVED.code.toDouble() + ) + ), + keys = listOf( + Relations.ID, + Relations.NAME, + Relations.ICON_EMOJI, + Relations.ICON_IMAGE, + Relations.TARGET_OBJECT_TYPE + ) + ) + + // Combine chats with space view to filter by custom notification states + combine( + storelessSubscriptionContainer.subscribe(searchParams = searchParams), + spaceViewContainer.observe(vmParams.space) + ) { chats, spaceView -> + + // Filter chats that have custom notification states (different from space default) + chats.mapNotNull { chat -> + val chatState = NotificationStateCalculator.calculateChatNotificationState( + chatSpace = spaceView, + chatId = chat.id + ) + if (chatState != spaceView.spacePushNotificationMode) { + val objectType = storeOfObjectTypes.getTypeOfObject(chat) + ChatNotificationItem( + id = chat.id, + name = chat.name.orEmpty(), + customState = chatState, + icon = chat.objectIcon(builder = urlBuilder, objType = objectType) + ) + } else null + } + }.catch { error -> + Timber.e(error, "Error observing chats with custom notifications") + emit(emptyList()) + }.collect { filteredChats -> + chatsWithCustomNotifications.value = filteredChats + Timber.d("Chats with custom notifications updated: ${filteredChats.size} chats") + } + } + } + + /** + * Resets a chat's notification state to the space default by removing it from all + * force notification lists. + */ + fun onResetChatNotification(chatId: Id) { + viewModelScope.launch { + resetSpaceChatNotification.async( + ResetSpaceChatNotification.Params( + space = vmParams.space, + chatId = chatId + ) + ).fold( + onSuccess = { + Timber.d("Successfully reset notification state for chat: $chatId") + }, + onFailure = { error -> + Timber.e(error, "Failed to reset notification state for chat: $chatId") + sendToast("Failed to reset notification settings") + } + ) + } + } + /** * Determines whether the "Change Type" option should be shown in space settings. * @@ -1008,9 +1164,9 @@ class SpaceSettingsViewModel( return false return permission?.isOwner() == true - && spaceView.spaceUxType != null - && spaceView.spaceAccessType == SpaceAccessType.SHARED - && !spaceView.chatId.isNullOrEmpty() + && spaceView.spaceUxType != null + && spaceView.spaceAccessType == SpaceAccessType.SHARED + && !spaceView.chatId.isNullOrEmpty() } data class ShareLimitsState( @@ -1024,7 +1180,9 @@ class SpaceSettingsViewModel( data class ManageSharedSpace(val space: SpaceId) : Command() data class ShareInviteLink(val link: String) : Command() data class ManageBin(val space: SpaceId) : Command() - data class SelectDefaultObjectType(val space: SpaceId, val excludedTypeIds: List) : Command() + data class SelectDefaultObjectType(val space: SpaceId, val excludedTypeIds: List) : + Command() + data object ExitToVault : Command() data object ShowDeleteSpaceWarning : Command() data object ShowLeaveSpaceWarning : Command() @@ -1058,7 +1216,6 @@ class SpaceSettingsViewModel( private val container: SpaceViewSubscriptionContainer, private val urlBuilder: UrlBuilder, private val setSpaceDetails: SetSpaceDetails, - private val gradientProvider: SpaceGradientProvider, private val spaceManager: SpaceManager, private val deleteSpace: DeleteSpace, private val spaceGradientProvider: SpaceGradientProvider, @@ -1072,8 +1229,6 @@ class SpaceSettingsViewModel( private val appActionManager: AppActionManager, private val storeOfObjectTypes: StoreOfObjectTypes, private val copyInviteLinkToClipboard: CopyInviteLinkToClipboard, - private val fetchObject: FetchObject, - private val setObjectDetails: SetObjectDetails, private val getAccount: GetAccount, private val notificationPermissionManager: NotificationPermissionManager, private val setSpaceNotificationMode: SetSpaceNotificationMode, @@ -1082,7 +1237,8 @@ class SpaceSettingsViewModel( private val spaceInviteLinkStore: SpaceInviteLinkStore, private val getGradients: GetCoverGradientCollection, private val setWallpaper: SetWallpaper, - private val stringResourceProvider: StringResourceProvider + private val storelessSubscriptionContainer: StorelessSubscriptionContainer, + private val resetSpaceChatNotification: ResetSpaceChatNotification ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( @@ -1092,7 +1248,6 @@ class SpaceSettingsViewModel( urlBuilder = urlBuilder, spaceManager = spaceManager, setSpaceDetails = setSpaceDetails, - gradientProvider = gradientProvider, analytics = analytics, deleteSpace = deleteSpace, spaceGradientProvider = spaceGradientProvider, @@ -1107,8 +1262,6 @@ class SpaceSettingsViewModel( appActionManager = appActionManager, storeOfObjectTypes = storeOfObjectTypes, copyInviteLinkToClipboard = copyInviteLinkToClipboard, - fetchObject = fetchObject, - setObjectDetails = setObjectDetails, getAccount = getAccount, notificationPermissionManager = notificationPermissionManager, setSpaceNotificationMode = setSpaceNotificationMode, @@ -1117,7 +1270,8 @@ class SpaceSettingsViewModel( spaceInviteLinkStore = spaceInviteLinkStore, getGradients = getGradients, setWallpaper = setWallpaper, - stringResourceProvider = stringResourceProvider + storelessSubscriptionContainer = storelessSubscriptionContainer, + resetSpaceChatNotification = resetSpaceChatNotification ) as T } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/UiEvent.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/UiEvent.kt index 451b284e2d..0040dc4d47 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/UiEvent.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/UiEvent.kt @@ -42,6 +42,8 @@ sealed class UiEvent { data class None(override val targetSpaceId: Id?) : OnNotificationsSetting() } + data class OnResetChatNotification(val chatId: Id) : UiEvent() + data object OnAddMoreSpacesClicked : UiEvent() data object OnChangeTypeClicked : UiEvent() diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/UiState.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/UiState.kt index 76695490bd..b6f11f0b59 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/UiState.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/UiState.kt @@ -42,6 +42,16 @@ data class SpaceTechInfo( val deviceToken: String? = null ) +/** + * Represents a chat with a custom notification state different from the space default. + */ +data class ChatNotificationItem( + val id: Id, + val name: String, + val icon: ObjectIcon, + val customState: NotificationState +) + sealed class UiSpaceSettingsItem { sealed class Section : UiSpaceSettingsItem() { data object Collaboration : Section()