Skip to content

Commit 62912f8

Browse files
author
Maxime NATUREL
committed
Introducing a NotificationsStatus to render the push notification toggle in session overview screen
1 parent 1acb42f commit 62912f8

12 files changed

+264
-51
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.settings.devices.v2.notification
18+
19+
import im.vector.app.core.di.ActiveSessionHolder
20+
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
21+
import javax.inject.Inject
22+
23+
class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor(
24+
private val activeSessionHolder: ActiveSessionHolder,
25+
) {
26+
27+
fun execute(deviceId: String): Boolean {
28+
return activeSessionHolder
29+
.getSafeActiveSession()
30+
?.accountDataService()
31+
?.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.settings.devices.v2.notification
18+
19+
import im.vector.app.core.di.ActiveSessionHolder
20+
import kotlinx.coroutines.flow.Flow
21+
import kotlinx.coroutines.flow.emptyFlow
22+
import kotlinx.coroutines.flow.flowOf
23+
import kotlinx.coroutines.flow.map
24+
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
25+
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
26+
import org.matrix.android.sdk.api.session.events.model.toModel
27+
import org.matrix.android.sdk.flow.flow
28+
import org.matrix.android.sdk.flow.unwrap
29+
import javax.inject.Inject
30+
31+
class GetNotificationsStatusUseCase @Inject constructor(
32+
private val activeSessionHolder: ActiveSessionHolder,
33+
private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
34+
private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase,
35+
) {
36+
37+
// TODO add unit tests
38+
fun execute(deviceId: String): Flow<NotificationsStatus> {
39+
val session = activeSessionHolder.getSafeActiveSession()
40+
return when {
41+
session == null -> emptyFlow()
42+
checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> {
43+
session.flow()
44+
.livePushers()
45+
.map { it.filter { pusher -> pusher.deviceId == deviceId } }
46+
.map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } }
47+
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
48+
}
49+
checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> {
50+
session.flow()
51+
.liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
52+
.unwrap()
53+
.map { it.content.toModel<LocalNotificationSettingsContent>()?.isSilenced?.not() }
54+
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
55+
}
56+
else -> flowOf(NotificationsStatus.NOT_SUPPORTED)
57+
}
58+
}
59+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.settings.devices.v2.notification
18+
19+
enum class NotificationsStatus {
20+
ENABLED,
21+
DISABLED,
22+
NOT_SUPPORTED,
23+
}
Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package im.vector.app.features.settings.devices.v2.overview
17+
package im.vector.app.features.settings.devices.v2.notification
1818

1919
import im.vector.app.core.di.ActiveSessionHolder
2020
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
@@ -24,17 +24,21 @@ import javax.inject.Inject
2424

2525
class TogglePushNotificationUseCase @Inject constructor(
2626
private val activeSessionHolder: ActiveSessionHolder,
27+
private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
28+
private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase,
2729
) {
2830

2931
suspend fun execute(deviceId: String, enabled: Boolean) {
3032
val session = activeSessionHolder.getSafeActiveSession() ?: return
31-
val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
32-
devicePusher?.let { pusher ->
33-
session.pushersService().togglePusher(pusher, enabled)
33+
34+
if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) {
35+
val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
36+
devicePusher?.let { pusher ->
37+
session.pushersService().togglePusher(pusher, enabled)
38+
}
3439
}
3540

36-
val accountData = session.accountDataService().getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
37-
if (accountData != null) {
41+
if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId)) {
3842
val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled)
3943
session.accountDataService().updateUserAccountData(
4044
UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId,

vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import android.view.View
2424
import android.view.ViewGroup
2525
import android.widget.Toast
2626
import androidx.appcompat.app.AppCompatActivity
27+
import androidx.core.view.isGone
2728
import androidx.core.view.isVisible
2829
import com.airbnb.mvrx.Success
2930
import com.airbnb.mvrx.fragmentViewModel
@@ -43,6 +44,7 @@ import im.vector.app.features.auth.ReAuthActivity
4344
import im.vector.app.features.crypto.recover.SetupMode
4445
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
4546
import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet
47+
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
4648
import im.vector.app.features.workers.signout.SignOutUiWorker
4749
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
4850
import org.matrix.android.sdk.api.extensions.orFalse
@@ -177,7 +179,7 @@ class SessionOverviewFragment :
177179
updateEntryDetails(state.deviceId)
178180
updateSessionInfo(state)
179181
updateLoading(state.isLoading)
180-
updatePushNotificationToggle(state.deviceId, state.notificationsEnabled)
182+
updatePushNotificationToggle(state.deviceId, state.notificationsStatus)
181183
}
182184

183185
private fun updateToolbar(viewState: SessionOverviewViewState) {
@@ -218,15 +220,19 @@ class SessionOverviewFragment :
218220
}
219221
}
220222

221-
private fun updatePushNotificationToggle(deviceId: String, enabled: Boolean) {
222-
views.sessionOverviewPushNotifications.apply {
223-
setOnCheckedChangeListener(null)
224-
setChecked(enabled)
225-
post {
226-
setOnCheckedChangeListener { _, isChecked ->
227-
viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked))
223+
private fun updatePushNotificationToggle(deviceId: String, notificationsStatus: NotificationsStatus) {
224+
views.sessionOverviewPushNotifications.isGone = notificationsStatus == NotificationsStatus.NOT_SUPPORTED
225+
when (notificationsStatus) {
226+
NotificationsStatus.ENABLED, NotificationsStatus.DISABLED -> {
227+
views.sessionOverviewPushNotifications.setOnCheckedChangeListener(null)
228+
views.sessionOverviewPushNotifications.setChecked(notificationsStatus == NotificationsStatus.ENABLED)
229+
views.sessionOverviewPushNotifications.post {
230+
views.sessionOverviewPushNotifications.setOnCheckedChangeListener { _, isChecked ->
231+
viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked))
232+
}
228233
}
229234
}
235+
else -> Unit
230236
}
231237
}
232238

vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,25 @@ import im.vector.app.core.resources.StringProvider
2929
import im.vector.app.features.auth.PendingAuthHandler
3030
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
3131
import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
32+
import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
33+
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
34+
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
3235
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
3336
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
3437
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase
3538
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
3639
import kotlinx.coroutines.flow.distinctUntilChanged
3740
import kotlinx.coroutines.flow.launchIn
3841
import kotlinx.coroutines.flow.map
39-
import kotlinx.coroutines.flow.merge
4042
import kotlinx.coroutines.flow.onEach
4143
import kotlinx.coroutines.launch
42-
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
4344
import org.matrix.android.sdk.api.auth.UIABaseAuth
4445
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
4546
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
4647
import org.matrix.android.sdk.api.extensions.orFalse
4748
import org.matrix.android.sdk.api.failure.Failure
48-
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS
4949
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
50-
import org.matrix.android.sdk.api.session.events.model.toModel
5150
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
52-
import org.matrix.android.sdk.flow.flow
53-
import org.matrix.android.sdk.flow.unwrap
5451
import timber.log.Timber
5552
import javax.net.ssl.HttpsURLConnection
5653
import kotlin.coroutines.Continuation
@@ -65,6 +62,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
6562
private val pendingAuthHandler: PendingAuthHandler,
6663
private val activeSessionHolder: ActiveSessionHolder,
6764
private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
65+
private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase,
6866
refreshDevicesUseCase: RefreshDevicesUseCase,
6967
) : VectorSessionsListViewModel<SessionOverviewViewState, SessionOverviewAction, SessionOverviewViewEvent>(
7068
initialState, activeSessionHolder, refreshDevicesUseCase
@@ -81,7 +79,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
8179
refreshPushers()
8280
observeSessionInfo(initialState.deviceId)
8381
observeCurrentSessionInfo()
84-
observePushers(initialState.deviceId)
82+
observeNotificationsStatus(initialState.deviceId)
8583
}
8684

8785
private fun refreshPushers() {
@@ -107,20 +105,9 @@ class SessionOverviewViewModel @AssistedInject constructor(
107105
}
108106
}
109107

110-
private fun observePushers(deviceId: String) {
111-
val session = activeSessionHolder.getSafeActiveSession() ?: return
112-
val pusherFlow = session.flow()
113-
.livePushers()
114-
.map { it.filter { pusher -> pusher.deviceId == deviceId } }
115-
.map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } }
116-
117-
val accountDataFlow = session.flow()
118-
.liveUserAccountData(TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
119-
.unwrap()
120-
.map { it.content.toModel<LocalNotificationSettingsContent>()?.isSilenced?.not() }
121-
122-
merge(pusherFlow, accountDataFlow)
123-
.onEach { it?.let { setState { copy(notificationsEnabled = it) } } }
108+
private fun observeNotificationsStatus(deviceId: String) {
109+
getNotificationsStatusUseCase.execute(deviceId)
110+
.onEach { setState { copy(notificationsStatus = it) } }
124111
.launchIn(viewModelScope)
125112
}
126113

@@ -233,7 +220,9 @@ class SessionOverviewViewModel @AssistedInject constructor(
233220
private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) {
234221
viewModelScope.launch {
235222
togglePushNotificationUseCase.execute(action.deviceId, action.enabled)
236-
setState { copy(notificationsEnabled = action.enabled) }
223+
// TODO should not be needed => test without
224+
val status = if (action.enabled) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED
225+
setState { copy(notificationsStatus = status) }
237226
}
238227
}
239228
}

vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import com.airbnb.mvrx.Async
2020
import com.airbnb.mvrx.MavericksState
2121
import com.airbnb.mvrx.Uninitialized
2222
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
23+
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
2324

2425
data class SessionOverviewViewState(
2526
val deviceId: String,
2627
val isCurrentSessionTrusted: Boolean = false,
2728
val deviceInfo: Async<DeviceFullInfo> = Uninitialized,
2829
val isLoading: Boolean = false,
29-
val notificationsEnabled: Boolean = false,
30+
val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED,
3031
) : MavericksState {
3132
constructor(args: SessionOverviewArgs) : this(
3233
deviceId = args.deviceId
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.settings.devices.v2.notification
18+
19+
import im.vector.app.test.fakes.FakeActiveSessionHolder
20+
import io.mockk.mockk
21+
import org.amshove.kluent.shouldBeEqualTo
22+
import org.junit.Test
23+
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
24+
25+
private const val A_DEVICE_ID = "device-id"
26+
27+
class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
28+
29+
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
30+
31+
private val checkIfCanTogglePushNotificationsViaAccountDataUseCase =
32+
CheckIfCanTogglePushNotificationsViaAccountDataUseCase(
33+
activeSessionHolder = fakeActiveSessionHolder.instance,
34+
)
35+
36+
@Test
37+
fun `given current session and an account data for the device id when execute then result is true`() {
38+
// Given
39+
fakeActiveSessionHolder
40+
.fakeSession
41+
.accountDataService()
42+
.givenGetUserAccountDataEventReturns(
43+
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
44+
content = mockk(),
45+
)
46+
47+
// When
48+
val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID)
49+
50+
// Then
51+
result shouldBeEqualTo true
52+
}
53+
54+
@Test
55+
fun `given current session and NO account data for the device id when execute then result is false`() {
56+
// Given
57+
fakeActiveSessionHolder
58+
.fakeSession
59+
.accountDataService()
60+
.givenGetUserAccountDataEventReturns(
61+
type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
62+
content = null,
63+
)
64+
65+
// When
66+
val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID)
67+
68+
// Then
69+
result shouldBeEqualTo false
70+
}
71+
}

0 commit comments

Comments
 (0)