diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 2773379ccc..b868dd6b81 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.ui) implementation(projects.libraries.network) + implementation(projects.libraries.pushstore.api) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.features.rageshake.api) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index 5470a788e9..0998318abf 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -34,11 +34,13 @@ import io.element.android.features.preferences.impl.about.AboutNode import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode +import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode import io.element.android.features.preferences.impl.root.PreferencesRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.UserId import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -70,6 +72,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data object NotificationSettings : NavTarget + + @Parcelize + data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -112,7 +117,16 @@ class PreferencesFlowNode @AssistedInject constructor( createNode(buildContext) } NavTarget.NotificationSettings -> { - createNode(buildContext) + val notificationSettingsCallback = object : NotificationSettingsNode.Callback { + override fun editDefaultNotificationMode(isOneToOne: Boolean) { + backstack.push(NavTarget.EditDefaultNotificationSetting(isOneToOne)) + } + } + createNode(buildContext, listOf(notificationSettingsCallback)) + } + is NavTarget.EditDefaultNotificationSetting -> { + val input = EditDefaultNotificationSettingNode.Inputs(navTarget.isOneToOne) + createNode(buildContext, plugins = listOf(input)) } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt new file mode 100644 index 0000000000..f1ee07f6b1 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import io.element.android.libraries.matrix.api.room.RoomNotificationMode + +sealed interface NotificationSettingsEvents { + + data object RefreshSystemNotificationsEnabled : NotificationSettingsEvents + data class SetNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents + data class SetAtRoomNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents + data class SetCallNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents + data class SetDefaultGroupNotificationMode(val mode: RoomNotificationMode) : NotificationSettingsEvents + data class SetDefaultOneToOneNotificationMode(val mode: RoomNotificationMode) : NotificationSettingsEvents + data object FixConfigurationMismatch : NotificationSettingsEvents + data object ClearConfigurationMismatchError : NotificationSettingsEvents +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt index d0db81ea7a..d7a9532219 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt @@ -21,10 +21,12 @@ import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.UserId @ContributesNode(SessionScope::class) class NotificationSettingsNode @AssistedInject constructor( @@ -32,13 +34,25 @@ class NotificationSettingsNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: NotificationSettingsPresenter, ) : Node(buildContext, plugins = plugins) { + + interface Callback : Plugin { + fun editDefaultNotificationMode(isOneToOne: Boolean) + } + + private val callbacks = plugins() + + private fun openEditDefault(isOneToOne: Boolean) { + callbacks.forEach { it.editDefaultNotificationMode(isOneToOne) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() NotificationSettingsView( state = state, + onOpenEditDefault = { openEditDefault(it) }, onBackPressed = ::navigateUp, - modifier = modifier + modifier = modifier, ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt index 255284f003..84d140b470 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt @@ -17,18 +17,170 @@ package io.element.android.features.preferences.impl.notifications import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.pushstore.api.UserPushStore +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds -class NotificationSettingsPresenter @Inject constructor() : Presenter { - +class NotificationSettingsPresenter @Inject constructor( + private val notificationSettingsService: NotificationSettingsService, + private val userPushStoreFactory: UserPushStoreFactory, + private val matrixClient: MatrixClient, + private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider +) : Presenter { @Composable override fun present(): NotificationSettingsState { + val userPushStore = userPushStoreFactory.create(matrixClient.sessionId) + val systemNotificationsEnabled: MutableState = remember { + mutableStateOf(systemNotificationsEnabledProvider.notificationsEnabled()) + } + + val localCoroutineScope = rememberCoroutineScope() + val appNotificationsEnabled = userPushStore + .getNotificationEnabledForDevice() + .collectAsState(initial = false) + + val matrixSettings: MutableState = remember { + mutableStateOf(NotificationSettingsState.MatrixNotificationSettings.Uninitialized) + } + + LaunchedEffect(Unit) { + fetchSettings(matrixSettings) + observeNotificationSettings(matrixSettings) + } + + fun handleEvents(event: NotificationSettingsEvents) { + when (event) { + is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> localCoroutineScope.setAtRoomNotificationsEnabled(event.enabled) + is NotificationSettingsEvents.SetCallNotificationsEnabled -> localCoroutineScope.setCallNotificationsEnabled(event.enabled) + is NotificationSettingsEvents.SetDefaultGroupNotificationMode -> localCoroutineScope.setDefaultGroupNotificationMode(event.mode) + is NotificationSettingsEvents.SetDefaultOneToOneNotificationMode -> localCoroutineScope.setDefaultOneToOneNotificationMode(event.mode) + is NotificationSettingsEvents.SetNotificationsEnabled -> localCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled) + NotificationSettingsEvents.ClearConfigurationMismatchError -> matrixSettings.value = NotificationSettingsState.MatrixNotificationSettings.InvalidNotificationSettingsState(fixFailed = false) + NotificationSettingsEvents.FixConfigurationMismatch -> localCoroutineScope.fixConfigurationMismatch(matrixSettings) + NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled() + } + } + return NotificationSettingsState( - isEnabled = true, - hasSystemPermission = true, - notifyMeOnRoom = true, - acceptCalls = true + matrixNotificationSettings = matrixSettings.value, + appNotificationSettings = NotificationSettingsState.AppNotificationSettings( + systemNotificationsEnabled = systemNotificationsEnabled.value, + appNotificationsEnabled = appNotificationsEnabled.value + ), + eventSink = ::handleEvents ) } + + @OptIn(FlowPreview::class) + private fun CoroutineScope.observeNotificationSettings(target: MutableState) { + notificationSettingsService.notificationSettingsChangeFlow + .debounce(0.5.seconds) + .onEach { + fetchSettings(target) + } + .launchIn(this) + } + + private fun CoroutineScope.fetchSettings(target: MutableState) = launch { + val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow() + val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow() + + val oneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = true).getOrThrow() + val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow() + + if(groupDefaultMode != encryptedGroupDefaultMode || oneToOneDefaultMode != encryptedOneToOneDefaultMode) { + target.value = NotificationSettingsState.MatrixNotificationSettings.InvalidNotificationSettingsState(fixFailed = false) + return@launch + } + + val callNotificationsEnabled = notificationSettingsService.isCallEnabled().getOrThrow() + val atRoomNotificationsEnabled = notificationSettingsService.isRoomMentionEnabled().getOrThrow() + + target.value = NotificationSettingsState.MatrixNotificationSettings.ValidNotificationSettingsState( + atRoomNotificationsEnabled = atRoomNotificationsEnabled, + callNotificationsEnabled = callNotificationsEnabled, + defaultGroupNotificationMode = encryptedGroupDefaultMode, + defaultOneToOneNotificationMode = encryptedOneToOneDefaultMode, + ) + } + + private fun CoroutineScope.fixConfigurationMismatch(target: MutableState) = launch { + runCatching { + val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow() + val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow() + + if (groupDefaultMode != encryptedGroupDefaultMode) { + notificationSettingsService.setDefaultRoomNotificationMode( + isEncrypted = encryptedGroupDefaultMode != RoomNotificationMode.ALL_MESSAGES, + mode = RoomNotificationMode.ALL_MESSAGES, + isOneToOne = false, + ) + } + + val oneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = true).getOrThrow() + val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow() + + if (oneToOneDefaultMode != encryptedOneToOneDefaultMode) { + notificationSettingsService.setDefaultRoomNotificationMode( + isEncrypted = encryptedOneToOneDefaultMode != RoomNotificationMode.ALL_MESSAGES, + mode = RoomNotificationMode.ALL_MESSAGES, + isOneToOne = true, + ) + } + }.fold( + onSuccess = {}, + onFailure = { + target.value = NotificationSettingsState.MatrixNotificationSettings.InvalidNotificationSettingsState(fixFailed = true) + } + ) + } + + private fun CoroutineScope.setAtRoomNotificationsEnabled(enabled: Boolean) = launch { + notificationSettingsService.setRoomMentionEnabled(enabled) + } + + private fun CoroutineScope.setCallNotificationsEnabled(enabled: Boolean) = launch { + notificationSettingsService.setCallEnabled(enabled) + } + + private fun CoroutineScope.setDefaultGroupNotificationMode(mode: RoomNotificationMode) = launch { + notificationSettingsService.setDefaultRoomNotificationMode(false, mode, false) + notificationSettingsService.setDefaultRoomNotificationMode(true, mode, false) + } + + private fun CoroutineScope.setDefaultOneToOneNotificationMode(mode: RoomNotificationMode) = launch { + notificationSettingsService.setDefaultRoomNotificationMode(false, mode, true) + notificationSettingsService.setDefaultRoomNotificationMode(true, mode, true) + } + + private fun CoroutineScope.setNotificationsEnabled(userPushStore: UserPushStore, enabled: Boolean) = launch { + userPushStore.setNotificationEnabledForDevice(enabled) + } + + private fun CoroutineScope.getDefaultRoomNotificationMode(isOneToOne: Boolean, defaultRoomNotificationMode: MutableState) = launch { + val encryptedMode = notificationSettingsService.getDefaultRoomNotificationMode(true, isOneToOne).getOrThrow() + val unencryptedMode = notificationSettingsService.getDefaultRoomNotificationMode(false, isOneToOne).getOrThrow() + if (encryptedMode == unencryptedMode) { + defaultRoomNotificationMode.value + } else { + defaultRoomNotificationMode.value = null + } + } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt index ca65fcf5d0..ac3502b89b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt @@ -16,10 +16,35 @@ package io.element.android.features.preferences.impl.notifications +import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.room.RoomNotificationMode + +@Immutable data class NotificationSettingsState( - val hasSystemPermission: Boolean, - val isEnabled: Boolean, - val notifyMeOnRoom: Boolean, - val acceptCalls: Boolean -// val eventSink: (AnalyticsOptInEvents) -> Unit, -) + val matrixNotificationSettings: MatrixNotificationSettings, + val appNotificationSettings: AppNotificationSettings, + val eventSink: (NotificationSettingsEvents) -> Unit, +) { + sealed interface MatrixNotificationSettings { + data object Uninitialized : MatrixNotificationSettings + data class ValidNotificationSettingsState( + val atRoomNotificationsEnabled: Boolean, + val callNotificationsEnabled: Boolean, + val defaultGroupNotificationMode: RoomNotificationMode?, + val defaultOneToOneNotificationMode: RoomNotificationMode?, + ) : MatrixNotificationSettings + + data class InvalidNotificationSettingsState( + val fixFailed: Boolean + ) : MatrixNotificationSettings + } + + data class AppNotificationSettings( + val systemNotificationsEnabled: Boolean, + val appNotificationsEnabled: Boolean, + ) +} + + + + diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt index c6c72f3acc..a4b9014b0c 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.preferences.impl.notifications import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.room.RoomNotificationMode open class NotificationSettingsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -26,8 +27,15 @@ open class NotificationSettingsStateProvider : PreviewParameterProvider Unit, onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { + OnLifecycleEvent { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + else -> Unit + } + } PreferenceView( modifier = modifier, onBackPressed = onBackPressed, title = stringResource(id = CommonStrings.screen_notification_settings_title) ) { - if (state.isEnabled && !state.hasSystemPermission) { + when (state.matrixNotificationSettings) { + is NotificationSettingsState.MatrixNotificationSettings.InvalidNotificationSettingsState -> InvalidNotificationSettingsView( + showError = state.matrixNotificationSettings.fixFailed, + onContinueClicked = { state.eventSink(NotificationSettingsEvents.FixConfigurationMismatch) }, + onDismissError = { state.eventSink(NotificationSettingsEvents.ClearConfigurationMismatchError) }, + modifier = modifier, + ) + NotificationSettingsState.MatrixNotificationSettings.Uninitialized -> return@PreferenceView + is NotificationSettingsState.MatrixNotificationSettings.ValidNotificationSettingsState -> NotificationSettingsContentView( + matrixSettings = state.matrixNotificationSettings, + systemSettings = state.appNotificationSettings, + onNotificationsEnabledChanged = { state.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(it))}, + onGroupChatsClicked = { onOpenEditDefault(false) }, + onDirectChatsClicked = { onOpenEditDefault(true) }, + onMentionNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(it)) }, + onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) }, + modifier = modifier, + ) + } + } +} + +@Composable +private fun NotificationSettingsContentView( + matrixSettings: NotificationSettingsState.MatrixNotificationSettings.ValidNotificationSettingsState, + systemSettings: NotificationSettingsState.AppNotificationSettings, + onNotificationsEnabledChanged: (Boolean) -> Unit, + onGroupChatsClicked: () -> Unit, + onDirectChatsClicked: () -> Unit, + onMentionNotificationsChanged: (Boolean) -> Unit, + onCallsNotificationsChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + if (systemSettings.appNotificationsEnabled && !systemSettings.systemNotificationsEnabled) { PreferenceText( icon = Icons.Filled.NotificationsOff, title = stringResource(id = CommonStrings.screen_notification_settings_system_notifications_turned_off), subtitle = stringResource(id = CommonStrings.screen_notification_settings_system_notifications_action_required, stringResource(id = CommonStrings.screen_notification_settings_system_notifications_action_required_content_link)), - onClick = {} + onClick = { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val uri: Uri = Uri.fromParts("package", context.packageName, null) + intent.data = uri + context.startActivity(intent) + } ) } PreferenceSwitch( - modifier = Modifier, + modifier = modifier, title = stringResource(id = CommonStrings.screen_notification_settings_enable_notifications), - isChecked = state.isEnabled, + isChecked = systemSettings.appNotificationsEnabled, switchAlignment = Alignment.Top, + onCheckedChange = onNotificationsEnabledChanged ) - if (state.isEnabled) { + if (systemSettings.appNotificationsEnabled) { PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_notification_section_title)) { PreferenceText( title = stringResource(id = CommonStrings.screen_notification_settings_group_chats), - subtitle = "All messages" + subtitle = getTitleForRoomNotificationMode(mode = matrixSettings.defaultGroupNotificationMode), + onClick = onGroupChatsClicked ) PreferenceText( title = stringResource(id = CommonStrings.screen_notification_settings_direct_chats), - subtitle = "Mentions and Keywords" + subtitle = getTitleForRoomNotificationMode(mode = matrixSettings.defaultOneToOneNotificationMode), + onClick = onDirectChatsClicked ) } @@ -78,8 +151,9 @@ fun NotificationSettingsView( PreferenceSwitch( modifier = Modifier, title = stringResource(id = CommonStrings.screen_notification_settings_room_mention_label), - isChecked = state.notifyMeOnRoom, + isChecked = matrixSettings.atRoomNotificationsEnabled, switchAlignment = Alignment.Top, + onCheckedChange = onMentionNotificationsChanged ) } @@ -87,22 +161,83 @@ fun NotificationSettingsView( PreferenceSwitch( modifier = Modifier, title = stringResource(id = CommonStrings.screen_notification_settings_calls_label), - isChecked = state.acceptCalls, + isChecked = matrixSettings.callNotificationsEnabled, switchAlignment = Alignment.Top, + onCheckedChange = onCallsNotificationsChanged ) } } +} + + +@Composable +private fun getTitleForRoomNotificationMode(mode: RoomNotificationMode?) = +when(mode) { + RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages) + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords) + RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute) + null -> "" +} + +@Composable +private fun InvalidNotificationSettingsView( + showError: Boolean, + onContinueClicked: () -> Unit, + onDismissError: () -> Unit, + modifier: Modifier = Modifier +) { + Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Surface( + Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.small, + color = MaterialTheme.colorScheme.surfaceVariant + ) { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + Row { + Text( + stringResource(R.string.screen_notification_settings_configuration_mismatch), + modifier = Modifier.weight(1f), + style = ElementTheme.typography.fontBodyLgMedium, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Start, + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + stringResource(R.string.screen_notification_settings_configuration_mismatch_description), + style = ElementTheme.typography.fontBodyMdRegular, + ) + Spacer(modifier = Modifier.height(12.dp)) + Button( + text = stringResource(CommonStrings.action_continue), + size = ButtonSize.Medium, + modifier = Modifier.fillMaxWidth(), + onClick = onContinueClicked, + ) + } + } + } + if(showError) { + ErrorDialog( + title = stringResource(id = CommonStrings.dialog_title_error), + content = stringResource(id = CommonStrings.screen_notification_settings_failed_fixing_configuration), + onDismiss = onDismissError + ) } } @Preview @Composable -internal fun AboutViewLightPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = +internal fun NotificationSettingsViewLightPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -internal fun AboutViewDarkPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = +internal fun NotificationSettingsViewDarkPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = ElementPreviewDark { ContentToPreview(state) } @Composable @@ -110,5 +245,26 @@ private fun ContentToPreview(state: NotificationSettingsState) { NotificationSettingsView( state = state, onBackPressed = {}, + onOpenEditDefault = {}, + ) +} + + +@Preview +@Composable +internal fun InvalidNotificationSettingsViewightPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = + ElementPreviewLight { InvalidNotificationSettingsContentToPreview(state) } + +@Preview +@Composable +internal fun InvalidNotificationSettingsViewDarkPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = + ElementPreviewDark { InvalidNotificationSettingsContentToPreview(state) } + +@Composable +private fun InvalidNotificationSettingsContentToPreview(state: NotificationSettingsState) { + InvalidNotificationSettingsView( + showError = false, + onContinueClicked = {}, + onDismissError = {}, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/SystemNotificationsEnabledProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/SystemNotificationsEnabledProvider.kt new file mode 100644 index 0000000000..f33b1cb773 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/SystemNotificationsEnabledProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import javax.inject.Inject + +interface SystemNotificationsEnabledProvider { + fun notificationsEnabled(): Boolean +} +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class, boundType = SystemNotificationsEnabledProvider::class) +class DefaultSystemNotificationsEnabledProvider @Inject constructor( + @ApplicationContext private val context: Context, +): SystemNotificationsEnabledProvider { + override fun notificationsEnabled(): Boolean { + return NotificationManagerCompat.from(context).areNotificationsEnabled() + } +} + diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/DefaultNotificationSettingOption.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/DefaultNotificationSettingOption.kt new file mode 100644 index 0000000000..b06df90d30 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/DefaultNotificationSettingOption.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.RadioButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.R + +@Composable +fun DefaultNotificationSettingOption( + mode: RoomNotificationMode, + modifier: Modifier = Modifier, + isSelected: Boolean = false, + onOptionSelected: (RoomNotificationMode) -> Unit = {}, +) { + val subtitle = when(mode) { + RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages) + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords) + else -> "" + } + Row( + modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onOptionSelected(mode) }, + role = Role.RadioButton, + ) + .padding(8.dp), + ) { + Column( + Modifier + .weight(1f) + .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + ) { + Text( + text = subtitle, + style = ElementTheme.typography.fontBodyLgRegular, + ) + } + + RadioButton( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + selected = isSelected, + onClick = null // null recommended for accessibility with screenreaders + ) + } +} +@DayNightPreviews +@Composable +internal fun DefaultNotificationSettingOptionLightPreview() = ElementPreview { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + DefaultNotificationSettingOption( + mode = RoomNotificationMode.ALL_MESSAGES, + isSelected = true, + ) + DefaultNotificationSettingOption( + mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, + isSelected = false, + ) + } +} + diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt new file mode 100644 index 0000000000..6c4fd646f4 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class EditDefaultNotificationSettingNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: EditDefaultNotificationSettingPresenter.Factory +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val isOneToOne: Boolean + ) : NodeInputs + + private val inputs = inputs() + private val presenter = presenterFactory.create(inputs.isOneToOne) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + EditDefaultNotificationSettingView( + state = state, + onBackPressed = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt new file mode 100644 index 0000000000..b146155486 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +class EditDefaultNotificationSettingPresenter @AssistedInject constructor( + private val notificationSettingsService: NotificationSettingsService, + @Assisted private val isOneToOne: Boolean, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(oneToOne: Boolean): EditDefaultNotificationSettingPresenter + } + @Composable + override fun present(): EditDefaultNotificationSettingState { + + val mode: MutableState = remember { + mutableStateOf(null) + } + val localCoroutineScope = rememberCoroutineScope() + LaunchedEffect(Unit) { + fetchSettings(mode) + observeNotificationSettings(mode) + } + + fun handleEvents(event: EditDefaultNotificationSettingStateEvents) { + when (event) { + is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> localCoroutineScope.setDefaultNotificationMode(event.mode) + } + } + + return EditDefaultNotificationSettingState( + isOneToOne = isOneToOne, + mode = mode.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.fetchSettings(mode: MutableState) = launch { + mode.value = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = isOneToOne).getOrThrow() + } + + @OptIn(FlowPreview::class) + private fun CoroutineScope.observeNotificationSettings(mode: MutableState) { + notificationSettingsService.notificationSettingsChangeFlow + .debounce(0.5.seconds) + .onEach { + fetchSettings(mode) + } + .launchIn(this) + } + + private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode) = launch { + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne) + } + +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt new file mode 100644 index 0000000000..62c708d988 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit + +import io.element.android.libraries.matrix.api.room.RoomNotificationMode + +data class EditDefaultNotificationSettingState( + val isOneToOne: Boolean, + val mode: RoomNotificationMode?, + val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt new file mode 100644 index 0000000000..75c9b6c1a4 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit + +import io.element.android.libraries.matrix.api.room.RoomNotificationMode + +sealed interface EditDefaultNotificationSettingStateEvents { + data class SetNotificationMode(val mode: RoomNotificationMode): EditDefaultNotificationSettingStateEvents +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt new file mode 100644 index 0000000000..5c00cd49cb --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.ui.strings.R +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun EditDefaultNotificationSettingView( + state: EditDefaultNotificationSettingState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + + val title = if(state.isOneToOne) { + CommonStrings.screen_notification_settings_direct_chats + } else { + CommonStrings.screen_notification_settings_group_chats + } + PreferenceView( + modifier = modifier, + onBackPressed = onBackPressed, + title = stringResource(id = title) + ) { + + val validModes = listOf(RoomNotificationMode.ALL_MESSAGES, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + + val categoryTitle = if(state.isOneToOne) { + R.string.screen_notification_settings_edit_screen_direct_section_header + } else { + R.string.screen_notification_settings_edit_screen_group_section_header + } + PreferenceCategory(title = stringResource(id = categoryTitle)) { + + if (state.mode != null) { + Column(modifier = modifier.selectableGroup()) { + validModes.forEach { item -> + DefaultNotificationSettingOption( + mode = item, + isSelected = state.mode == item, + onOptionSelected = { state.eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(it)) } + ) + } + } + } + } + } +} + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index b12d4710a0..34a0b2b2e1 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -106,7 +106,7 @@ class RoomDetailsPresenter @Inject constructor( } RoomDetailsEvent.UnmuteNotification -> { scope.launch(dispatchers.io) { - client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.activeMemberCount) + client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.isOneToOne) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt index d5c88e53d4..f1993d27b1 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt @@ -75,8 +75,6 @@ class RoomNotificationSettingsPresenter @Inject constructor( } } - Timber.d("NotifState: $roomNotificationSettingsState") - return RoomNotificationSettingsState( roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), defaultRoomNotificationMode = defaultRoomNotificationMode.value, @@ -97,7 +95,7 @@ class RoomNotificationSettingsPresenter @Inject constructor( private fun CoroutineScope.getDefaultRoomNotificationMode(defaultRoomNotificationMode: MutableState) = launch { defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( room.isEncrypted, - room.activeMemberCount + room.isOneToOne ).getOrThrow() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt index 0f82604aba..3e62b0c97f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt @@ -21,16 +21,22 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettin import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationSettings import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.withContext interface NotificationSettingsService { /** * State of the current room notification settings flow ([MatrixRoomNotificationSettingsState.Unknown] if not started). */ val notificationSettingsChangeFlow : SharedFlow - suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result - suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: Long): Result + suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result + suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result + suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result suspend fun muteRoom(roomId: RoomId): Result - suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result + suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result + suspend fun isRoomMentionEnabled(): Result + suspend fun setRoomMentionEnabled(enabled: Boolean): Result + suspend fun isCallEnabled(): Result + suspend fun setCallEnabled(enabled: Boolean): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 60c8b1ddd5..1f90913ef4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -49,6 +49,12 @@ interface MatrixRoom : Closeable { val activeMemberCount: Long val joinedMemberCount: Long + /** + * A one-to-one is a room with exactly 2 members. + * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules). + */ + val isOneToOne: Boolean get() = joinedMemberCount == 2L + /** * The current loaded members as a StateFlow. * Initial value is [MatrixRoomMembersState.Unknown]. @@ -178,6 +184,7 @@ interface MatrixRoom : Closeable { suspend fun endPoll(pollStartId: EventId, text: String): Result override fun close() = destroy() + } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt index 1b1d51214f..7dea15a809 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt @@ -47,16 +47,22 @@ class RustNotificationSettingsService( notificationSettings.setDelegate(notificationSettingsDelegate) } - override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result = + override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result = runCatching { - notificationSettings.getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne(membersCount)).let(RoomNotificationSettingsMapper::map) + notificationSettings.getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::map) } - override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: Long): Result = + override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result = runCatching { - notificationSettings.getDefaultRoomNotificationMode(isEncrypted, isOneToOne(membersCount)).let(RoomNotificationSettingsMapper::mapMode) + notificationSettings.getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode) } + override suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) + } + } + override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result = withContext(dispatchers.io) { runCatching { notificationSettings.setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode)) @@ -71,16 +77,33 @@ class RustNotificationSettingsService( override suspend fun muteRoom(roomId: RoomId): Result = setRoomNotificationMode(roomId, RoomNotificationMode.MUTE) - override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, membersCount: Long) = withContext(dispatchers.io) { + override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean) = withContext(dispatchers.io) { runCatching { - notificationSettings.unmuteRoom(roomId.value, isEncrypted, isOneToOne(membersCount)) + notificationSettings.unmuteRoom(roomId.value, isEncrypted, isOneToOne) } } - /** - * A one-to-one is a room with exactly 2 members. - * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules). - * @param membersCount The active members count in a room - */ - private fun isOneToOne(membersCount: Long) = membersCount == 2L + override suspend fun isRoomMentionEnabled(): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.isRoomMentionEnabled() + } + } + + override suspend fun setRoomMentionEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.setRoomMentionEnabled(enabled) + } + } + + override suspend fun isCallEnabled(): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.isCallEnabled() + } + } + + override suspend fun setCallEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.setCallEnabled(enabled) + } + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 461e43931e..53352ae640 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -210,7 +210,7 @@ class RustMatrixRoom( val currentRoomNotificationSettings = currentState.roomNotificationSettings() _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) runCatching { - roomNotificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, activeMemberCount).getOrThrow() + roomNotificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() }.map { _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(it) }.onFailure { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt index 5e06d08de5..ccf5cd4155 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt @@ -36,14 +36,18 @@ class FakeNotificationSettingsService : NotificationSettingsService { override val notificationSettingsChangeFlow: SharedFlow get() = _roomNotificationSettingsStateFlow - override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result { + override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result { return getRoomNotificationSettingsResult } - override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: Long): Result { + override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result { return getDefaultRoomNotificationMode } + override suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result { + TODO("Not yet implemented") + } + override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result { return setRoomNotificationMode } @@ -56,7 +60,24 @@ class FakeNotificationSettingsService : NotificationSettingsService { return muteRoomResult } - override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result { + override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result { return unmuteRoomResult } + + override suspend fun isRoomMentionEnabled(): Result { + return Result.success(false) + } + + override suspend fun setRoomMentionEnabled(enabled: Boolean): Result { + return Result.success(Unit) + } + + override suspend fun isCallEnabled(): Result { + return Result.success(false) + } + + override suspend fun setCallEnabled(enabled: Boolean): Result { + return Result.success(Unit) + } + } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index c3d68e52ac..7c5d24c31a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -123,7 +124,7 @@ class DefaultPushHandler @Inject constructor( } val userPushStore = userPushStoreFactory.create(userId) - if (!userPushStore.areNotificationEnabledForDevice()) { + if (!userPushStore.getNotificationEnabledForDevice().first()) { // TODO We need to check if this is an incoming call Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") return diff --git a/libraries/pushstore/api/build.gradle.kts b/libraries/pushstore/api/build.gradle.kts index fdfd794c2e..00d7776770 100644 --- a/libraries/pushstore/api/build.gradle.kts +++ b/libraries/pushstore/api/build.gradle.kts @@ -22,5 +22,6 @@ android { } dependencies { + implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) } diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt index 28577ba3f8..a10413fdf5 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt @@ -15,6 +15,8 @@ */ package io.element.android.libraries.pushstore.api +import kotlinx.coroutines.flow.Flow + /** * Store data related to push about a user. @@ -25,7 +27,7 @@ interface UserPushStore { suspend fun getCurrentRegisteredPushKey(): String? suspend fun setCurrentRegisteredPushKey(value: String) - suspend fun areNotificationEnabledForDevice(): Boolean + fun getNotificationEnabledForDevice(): Flow suspend fun setNotificationEnabledForDevice(enabled: Boolean) /** diff --git a/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt b/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt index c87c772ddf..67b07bfd1d 100644 --- a/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt +++ b/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt @@ -20,6 +20,7 @@ import androidx.test.platform.app.InstrumentationRegistry import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver +import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Test import kotlin.concurrent.thread @@ -49,8 +50,8 @@ class DefaultUserPushStoreFactoryTest { thread1.join() thread2.join() runBlocking { - userPushStore1!!.areNotificationEnabledForDevice() - userPushStore2!!.areNotificationEnabledForDevice() + userPushStore1!!.getNotificationEnabledForDevice().first() + userPushStore2!!.getNotificationEnabledForDevice().first() } } } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 56867a6584..718ddb51fa 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -26,7 +26,9 @@ import androidx.datastore.preferences.preferencesDataStore import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.UserPushStore +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map /** * Store data related to push about a user. @@ -60,8 +62,8 @@ class UserPushStoreDataStore( } } - override suspend fun areNotificationEnabledForDevice(): Boolean { - return context.dataStore.data.first()[notificationEnabled].orTrue() + override fun getNotificationEnabledForDevice(): Flow { + return context.dataStore.data.map{ it[notificationEnabled].orTrue() } } override suspend fun setNotificationEnabledForDevice(enabled: Boolean) {