diff --git a/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt b/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt index 01626c1c89..891be2ca8b 100644 --- a/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt +++ b/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt @@ -99,6 +99,7 @@ class AnytypeNotificationService @Inject constructor( ) } is NotificationPayload.ParticipantRequestApproved -> { + Timber.d("Processing participant request approved notification : ${notification}") val placeholder = context.resources.getString(R.string.untitled) val title = context.resources.getString( R.string.multiplayer_notification_member_request_approved @@ -106,16 +107,26 @@ class AnytypeNotificationService @Inject constructor( val actionTitle = context.resources.getString( R.string.multiplayer_notification_go_to_space ) - val body = if (payload.permissions.isOwnerOrEditor()) { - context.resources.getString( - R.string.multiplayer_notification_member_request_approved_with_edit_rights, - payload.spaceName.ifEmpty { placeholder } - ) - } else { - context.resources.getString( - R.string.multiplayer_notification_member_request_approved_with_read_only_rights, - payload.spaceName.ifEmpty { placeholder } - ) + val permissions = payload.permissions + val body = when { + permissions == null -> { + context.resources.getString( + R.string.multiplayer_notification_member_request_approved_unknown_rights, + payload.spaceName.ifEmpty { placeholder } + ) + } + permissions.isOwnerOrEditor() -> { + context.resources.getString( + R.string.multiplayer_notification_member_request_approved_with_edit_rights, + payload.spaceName.ifEmpty { placeholder } + ) + } + else -> { + context.resources.getString( + R.string.multiplayer_notification_member_request_approved_with_read_only_rights, + payload.spaceName.ifEmpty { placeholder } + ) + } } val intent = Intent(context, MainActivity::class.java).apply { putExtra(Relations.SPACE_ID, payload.spaceId.id) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt index e57bb41181..391cb62af6 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt @@ -72,7 +72,9 @@ class RequestJoinSpaceFragment : BaseBottomSheetComposeFragment() { return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - val bottomSheetState = rememberModalBottomSheetState() + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) val scope = rememberCoroutineScope() val launcher = rememberLauncherForActivityResult( diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Notification.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Notification.kt index 3590bf343c..1e68eff56d 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Notification.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Notification.kt @@ -10,7 +10,7 @@ data class Notification( val isLocal: Boolean, val payload: NotificationPayload, val space: SpaceId, - val aclHeadId: String + val aclHeadId: String? = null ) { sealed class Event { @@ -52,7 +52,7 @@ sealed class NotificationPayload { data class ParticipantRequestApproved( val spaceId: SpaceId, val spaceName: String, - val permissions: SpaceMemberPermissions + val permissions: SpaceMemberPermissions? = null ) : NotificationPayload() diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 635f825f4e..9203301776 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -1430,6 +1430,7 @@ %1$s joined %2$s space with edit access rights Request approved Your request to join the %1$s space has been approved with read-only access rights. The space will be available on your device soon. + Your request to join the %1$s space has been approved. The space will be available on your device soon. Your request to join the %1$s space has been approved with edit access rights. The space will be available on your device soon. The space \"%1$s\" is no longer accessible. You have been removed from the space \"%1$s\", or the space was deleted by the owner. diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt index 0d1844ae5b..1fb2552d50 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt @@ -7,6 +7,9 @@ import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.analytics.base.EventsDictionary.screenInviteRequest import com.anytypeio.anytype.analytics.base.EventsDictionary.screenRequestSent import com.anytypeio.anytype.analytics.base.sendEvent +import com.anytypeio.anytype.core_models.Notification +import com.anytypeio.anytype.core_models.NotificationPayload +import com.anytypeio.anytype.core_models.NotificationStatus import com.anytypeio.anytype.core_models.multiplayer.MultiplayerError import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteError import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteView @@ -27,6 +30,7 @@ import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.common.TypedViewState import javax.inject.Inject +import kotlin.random.Random import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -130,54 +134,86 @@ class RequestJoinSpaceViewModel( } fun onRequestToJoinClicked() { - when(val curr = state.value) { - is TypedViewState.Success -> { - joinSpaceRequestJob?.cancel() - joinSpaceRequestJob = viewModelScope.launch { - val fileKey = spaceInviteResolver.parseFileKey(params.link) - val contentId = spaceInviteResolver.parseContentId(params.link) - if (contentId != null && fileKey != null) { - isRequestInProgress.value = true - sendJoinSpaceRequest.async( - SendJoinSpaceRequest.Params( - space = curr.data.space, - network = configStorage.getOrNull()?.network, - inviteFileKey = fileKey, - inviteContentId = contentId - ) - ).fold( - onFailure = { e -> - Timber.e(e, "Error while sending space join request") - if (e is MultiplayerError.Generic) { - commands.emit(Command.ShowGenericMultiplayerError(e)) - } else { - sendToast(e.msg()) - } - }, - onSuccess = { - analytics.sendEvent(eventName = screenRequestSent) - if (notificator.areNotificationsEnabled) { - if (!curr.data.withoutApprove) { - commands.emit(Command.Toast.RequestSent) - } - commands.emit(Command.Dismiss) - } else { - if (!curr.data.withoutApprove) { - commands.emit(Command.Toast.RequestSent) - } - showEnableNotificationDialog.value = true - } - } - ) - isRequestInProgress.value = false - } - } - } else -> { - // Do nothing. + val currentState = state.value + if (currentState !is TypedViewState.Success) return + + joinSpaceRequestJob?.cancel() + joinSpaceRequestJob = viewModelScope.launch { + val fileKey = spaceInviteResolver.parseFileKey(params.link) + val contentId = spaceInviteResolver.parseContentId(params.link) + + if (fileKey == null || contentId == null) { + Timber.w("Could not parse invite link in onRequestToJoinClicked: ${params.link}") + return@launch } + + isRequestInProgress.value = true + + val params = SendJoinSpaceRequest.Params( + space = currentState.data.space, + network = configStorage.getOrNull()?.network, + inviteFileKey = fileKey, + inviteContentId = contentId + ) + + sendJoinSpaceRequest.async(params).fold( + onFailure = { handleJoinRequestFailure(it) }, + onSuccess = { handleJoinRequestSuccess(currentState.data) } + ) + + isRequestInProgress.value = false } } + private suspend fun handleJoinRequestFailure(error: Throwable) { + Timber.e(error, "Error while sending space join request") + when (error) { + is MultiplayerError.Generic -> commands.emit(Command.ShowGenericMultiplayerError(error)) + else -> sendToast(error.msg()) + } + } + + private suspend fun handleJoinRequestSuccess(data: SpaceInviteView) { + analytics.sendEvent(eventName = screenRequestSent) + + val shouldNotify = data.withoutApprove + val notificationsEnabled = notificator.areNotificationsEnabled + + if (shouldNotify) { + sendApprovalNotification(data) + } + + if (notificationsEnabled) { + if (!shouldNotify) { + commands.emit(Command.Toast.RequestSent) + } + commands.emit(Command.Dismiss) + } else { + if (!shouldNotify) { + commands.emit(Command.Toast.RequestSent) + } + showEnableNotificationDialog.value = true + } + } + + private fun createApprovalNotification(data: SpaceInviteView): Notification { + return Notification( + id = Random.nextInt().toString(), + createTime = System.currentTimeMillis(), + status = NotificationStatus.CREATED, + isLocal = true, + payload = NotificationPayload.ParticipantRequestApproved( + spaceId = data.space, + spaceName = data.spaceName + ), + space = data.space + ) + } + + private fun sendApprovalNotification(data: SpaceInviteView) { + notificator.notify(createApprovalNotification(data)) + } + fun onCancelJoinSpaceRequestClicked() { joinSpaceRequestJob?.cancel() isRequestInProgress.value = false diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/NotificationsViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/NotificationsViewModel.kt index 3304dbdbc1..8c0d46718c 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/NotificationsViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/notifications/NotificationsViewModel.kt @@ -80,7 +80,8 @@ class NotificationsViewModel( notification = notification.id, space = payload.spaceId, spaceName = payload.spaceName, - isReadOnly = !payload.permissions.isOwnerOrEditor() + isReadOnly = payload.permissions == null + || payload.permissions?.isOwnerOrEditor() != true ) } is NotificationPayload.ParticipantRemove -> { diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModelTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModelTest.kt index 8318c4ca98..c4c9a3b6f9 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModelTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/onboarding/signup/OnboardingMnemonicViewModelTest.kt @@ -6,6 +6,7 @@ import com.anytypeio.anytype.core_models.NetworkMode import com.anytypeio.anytype.core_models.NetworkModeConfig import com.anytypeio.anytype.domain.auth.interactor.GetMnemonic import com.anytypeio.anytype.domain.config.ConfigStorage +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.device.NetworkConnectionStatus import com.anytypeio.anytype.domain.network.NetworkModeProvider import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule @@ -40,8 +41,11 @@ class OnboardingMnemonicViewModelTest { @Mock private lateinit var networkModeProvider: NetworkModeProvider + lateinit var pendingIntentStore: PendingIntentStore + @Before fun setup() { + pendingIntentStore = PendingIntentStore() MockitoAnnotations.openMocks(this) } @@ -139,7 +143,8 @@ class OnboardingMnemonicViewModelTest { analytics = analytics, configStorage = configStorage, networkModeProvider = networkModeProvider, - networkConnectionStatus = networkConnectionStatus + networkConnectionStatus = networkConnectionStatus, + pendingIntentStore = pendingIntentStore ) } } \ No newline at end of file