mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-07 21:37:02 +09:00
DROID-3626 Space chat | Auto Invite Link (#2504)
This commit is contained in:
parent
b9edbcb316
commit
960da97835
10 changed files with 402 additions and 16 deletions
|
@ -15,6 +15,10 @@ import com.anytypeio.anytype.domain.debugging.Logger
|
|||
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.GenerateSpaceInviteLink
|
||||
import com.anytypeio.anytype.domain.multiplayer.GetSpaceInviteLink
|
||||
import com.anytypeio.anytype.domain.multiplayer.MakeSpaceShareable
|
||||
import com.anytypeio.anytype.domain.multiplayer.RevokeSpaceInviteLink
|
||||
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
|
@ -105,4 +109,8 @@ interface ChatComponentDependencies : ComponentDependencies {
|
|||
fun notificationPermissionManager(): NotificationPermissionManager
|
||||
fun storelessSubscriptionContainer(): StorelessSubscriptionContainer
|
||||
fun notificationBuilder(): NotificationBuilder
|
||||
fun generateSpaceInviteLink(): GenerateSpaceInviteLink
|
||||
fun makeSpaceShareable(): MakeSpaceShareable
|
||||
fun getSpaceInviteLink(): GetSpaceInviteLink
|
||||
fun revokeSpaceInviteLink(): RevokeSpaceInviteLink
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package com.anytypeio.anytype.ui.chats
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -8,9 +10,11 @@ import android.view.ViewGroup
|
|||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
|
@ -20,11 +24,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.res.colorResource
|
||||
|
@ -42,6 +49,8 @@ import com.anytypeio.anytype.core_utils.ext.toast
|
|||
import com.anytypeio.anytype.core_utils.intents.SystemAction.OpenUrl
|
||||
import com.anytypeio.anytype.core_utils.intents.proceedWithAction
|
||||
import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment
|
||||
import com.anytypeio.anytype.core_ui.features.multiplayer.GenerateInviteLinkCard
|
||||
import com.anytypeio.anytype.core_ui.features.multiplayer.ShareInviteLinkCard
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
import com.anytypeio.anytype.ext.daggerViewModel
|
||||
import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel
|
||||
|
@ -59,6 +68,9 @@ import com.anytypeio.anytype.ui.profile.ParticipantFragment
|
|||
import com.anytypeio.anytype.ui.search.GlobalSearchScreen
|
||||
import com.anytypeio.anytype.ui.sets.ObjectSetFragment
|
||||
import com.anytypeio.anytype.ui.settings.typography
|
||||
import com.anytypeio.anytype.presentation.multiplayer.ShareSpaceViewModel.ShareLinkViewState
|
||||
import com.anytypeio.anytype.ui.multiplayer.DeleteSpaceInviteLinkWarning
|
||||
import com.anytypeio.anytype.core_ui.views.BaseAlertDialog
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -88,8 +100,13 @@ class ChatFragment : BaseComposeFragment() {
|
|||
val notificationsSheetState =
|
||||
rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
var showGlobalSearchBottomSheet by remember { mutableStateOf(false) }
|
||||
val inviteModalState = vm.inviteModalState.collectAsStateWithLifecycle().value
|
||||
val showNotificationPermissionDialog =
|
||||
vm.showNotificationPermissionDialog.collectAsStateWithLifecycle().value
|
||||
val canCreateInviteLink = vm.canCreateInviteLink.collectAsStateWithLifecycle().value
|
||||
val isGeneratingInviteLink = vm.isGeneratingInviteLink.collectAsStateWithLifecycle().value
|
||||
|
||||
ErrorScreen()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
@ -181,6 +198,65 @@ class ChatFragment : BaseComposeFragment() {
|
|||
} else {
|
||||
componentManager().globalSearchComponent.release()
|
||||
}
|
||||
|
||||
when (inviteModalState) {
|
||||
is ChatViewModel.InviteModalState.ShowShareCard -> {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
vm.onInviteModalDismissed()
|
||||
},
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = Color.Transparent,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
dragHandle = null
|
||||
) {
|
||||
ShareInviteLinkCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp)
|
||||
.background(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = colorResource(id = R.color.widget_background)
|
||||
),
|
||||
link = inviteModalState.link,
|
||||
isCurrentUserOwner = canCreateInviteLink,
|
||||
onShareInviteClicked = { vm.onShareInviteLinkFromCardClicked() },
|
||||
onDeleteLinkClicked = { vm.onDeleteLinkClicked() },
|
||||
onShowQrCodeClicked = { vm.onShareQrCodeClicked() }
|
||||
)
|
||||
}
|
||||
}
|
||||
is ChatViewModel.InviteModalState.ShowGenerateCard -> {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
vm.onInviteModalDismissed()
|
||||
},
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = Color.Transparent,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
dragHandle = null
|
||||
) {
|
||||
GenerateInviteLinkCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp)
|
||||
.background(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = colorResource(id = R.color.widget_background)
|
||||
),
|
||||
onGenerateInviteLinkClicked = {
|
||||
vm.onGenerateInviteLinkClicked()
|
||||
},
|
||||
isLoading = isGeneratingInviteLink
|
||||
)
|
||||
}
|
||||
}
|
||||
ChatViewModel.InviteModalState.Hidden -> {
|
||||
// No modal shown
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
vm.navigation.collect { nav ->
|
||||
|
@ -303,6 +379,42 @@ class ChatFragment : BaseComposeFragment() {
|
|||
Timber.e(it, "Error while opening bookmark from chat")
|
||||
}
|
||||
}
|
||||
is ChatViewModel.ViewModelCommand.ShareInviteLink -> {
|
||||
runCatching {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, command.link)
|
||||
type = "text/plain"
|
||||
}
|
||||
startActivity(Intent.createChooser(intent, null))
|
||||
}.onFailure {
|
||||
Timber.e(it, "Error while sharing invite link")
|
||||
}
|
||||
}
|
||||
is ChatViewModel.ViewModelCommand.ShareQrCode -> {
|
||||
runCatching {
|
||||
Timber.d("ShareQrCode command received with link: ${command.link}")
|
||||
toast("QR Code sharing - to be implemented")
|
||||
}.onFailure {
|
||||
Timber.e(it, "Error while opening QR code")
|
||||
}
|
||||
}
|
||||
is ChatViewModel.ViewModelCommand.ShowDeleteLinkWarning -> {
|
||||
runCatching {
|
||||
val dialog = DeleteSpaceInviteLinkWarning()
|
||||
dialog.onAccepted = {
|
||||
vm.onDeleteLinkAccepted().also {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
dialog.onCancelled = {
|
||||
// Do nothing.
|
||||
}
|
||||
dialog.show(childFragmentManager, null)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Error while showing delete link warning")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -346,6 +458,25 @@ class ChatFragment : BaseComposeFragment() {
|
|||
// Do not apply.
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ErrorScreen() {
|
||||
val errorStateScreen = vm.errorState.collectAsStateWithLifecycle()
|
||||
when (val state = errorStateScreen.value) {
|
||||
ChatViewModel.UiErrorState.Hidden -> {
|
||||
// Do nothing
|
||||
}
|
||||
is ChatViewModel.UiErrorState.Show -> {
|
||||
BaseAlertDialog(
|
||||
dialogText = state.msg,
|
||||
buttonText = stringResource(id = R.string.membership_error_button_text_dismiss),
|
||||
onButtonClick = vm::hideError,
|
||||
onDismissRequest = vm::hideError
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CTX_KEY = "arg.discussion.ctx"
|
||||
private const val SPACE_KEY = "arg.discussion.space"
|
||||
|
|
|
@ -172,7 +172,9 @@
|
|||
android:label="Chat" >
|
||||
<action
|
||||
android:id="@+id/actionOpenWidgetsFromChat"
|
||||
app:destination="@id/homeScreen" />
|
||||
app:destination="@id/homeScreen"
|
||||
app:popUpTo="@id/vaultScreen"
|
||||
app:popUpToInclusive="false" />
|
||||
</fragment>
|
||||
|
||||
<dialog
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
|
|||
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
|
||||
import com.anytypeio.anytype.core_ui.views.BodyRegular
|
||||
import com.anytypeio.anytype.core_ui.views.ButtonPrimary
|
||||
import com.anytypeio.anytype.core_ui.views.ButtonPrimaryLoading
|
||||
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
|
||||
import com.anytypeio.anytype.core_ui.views.ButtonSize
|
||||
import com.anytypeio.anytype.core_ui.views.Title1
|
||||
|
@ -71,7 +72,8 @@ fun ShareInviteLinkCardOwnerPreview() {
|
|||
fun GenerateInviteLinkCardPreview() {
|
||||
GenerateInviteLinkCard(
|
||||
modifier = Modifier,
|
||||
onGenerateInviteLinkClicked = {}
|
||||
onGenerateInviteLinkClicked = {},
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -203,7 +205,8 @@ fun ShareInviteLinkCard(
|
|||
@Composable
|
||||
fun GenerateInviteLinkCard(
|
||||
modifier: Modifier = Modifier,
|
||||
onGenerateInviteLinkClicked: () -> Unit
|
||||
onGenerateInviteLinkClicked: () -> Unit,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = modifier,
|
||||
|
@ -240,13 +243,14 @@ fun GenerateInviteLinkCard(
|
|||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
ButtonPrimary(
|
||||
ButtonPrimaryLoading(
|
||||
text = stringResource(R.string.multiplayer_generate_invite_link),
|
||||
onClick = onGenerateInviteLinkClicked,
|
||||
size = ButtonSize.Large,
|
||||
modifier = Modifier
|
||||
modifierButton = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.height(48.dp),
|
||||
loading = isLoading
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<color name="shape_transparent_secondary">#29FFFFFF</color>
|
||||
|
||||
<color name="transparent_active">#66FFFFFF</color>
|
||||
<color name="transparent_inactive">#26FFFFFF</color>
|
||||
<color name="transparent_active_full_alpha">#FFFFFF</color>
|
||||
|
||||
<color name="glyph_active">#7B7B7B</color>
|
||||
|
|
|
@ -186,6 +186,7 @@
|
|||
<color name="overlay_black_light_5">#0D000000</color>
|
||||
<color name="transparent_black">#00000000</color>
|
||||
<color name="transparent_active">#66000000</color>
|
||||
<color name="transparent_inactive">#26000000</color>
|
||||
<color name="transparent_active_full_alpha">#000000</color>
|
||||
<color name="background_light_gray">#F3F2EC</color>
|
||||
<color name="background_soft_beige">#DFDDD0</color>
|
||||
|
|
|
@ -483,7 +483,10 @@ class ChatContainerTest {
|
|||
)
|
||||
|
||||
assertEquals(
|
||||
expected = ChatContainer.Intent.ScrollToMessage(id = "90"),
|
||||
expected = ChatContainer.Intent.ScrollToMessage(
|
||||
id = "90",
|
||||
startOfUnreadMessageSection = true
|
||||
),
|
||||
actual = initial.intent,
|
||||
)
|
||||
|
||||
|
|
|
@ -12,6 +12,10 @@ import com.anytypeio.anytype.core_models.Relations
|
|||
import com.anytypeio.anytype.core_models.Url
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE
|
||||
import com.anytypeio.anytype.core_models.multiplayer.InviteType
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceUxType
|
||||
import com.anytypeio.anytype.core_models.primitives.Space
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_ui.text.splitByMarks
|
||||
|
@ -19,6 +23,8 @@ import com.anytypeio.anytype.core_utils.common.DefaultFileInfo
|
|||
import com.anytypeio.anytype.core_utils.tools.DEFAULT_URL_REGEX
|
||||
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.fold
|
||||
import com.anytypeio.anytype.domain.base.getOrThrow
|
||||
import com.anytypeio.anytype.domain.base.onFailure
|
||||
import com.anytypeio.anytype.domain.base.onSuccess
|
||||
import com.anytypeio.anytype.domain.chats.AddChatMessage
|
||||
|
@ -31,6 +37,10 @@ import com.anytypeio.anytype.domain.misc.GetLinkPreview
|
|||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer.Store
|
||||
import com.anytypeio.anytype.domain.multiplayer.GenerateSpaceInviteLink
|
||||
import com.anytypeio.anytype.domain.multiplayer.GetSpaceInviteLink
|
||||
import com.anytypeio.anytype.domain.multiplayer.MakeSpaceShareable
|
||||
import com.anytypeio.anytype.domain.multiplayer.RevokeSpaceInviteLink
|
||||
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
|
@ -66,6 +76,7 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import com.anytypeio.anytype.presentation.multiplayer.ShareSpaceViewModel.ShareLinkViewState
|
||||
|
||||
class ChatViewModel @Inject constructor(
|
||||
private val vmParams: Params.Default,
|
||||
|
@ -87,7 +98,11 @@ class ChatViewModel @Inject constructor(
|
|||
private val createObjectFromUrl: CreateObjectFromUrl,
|
||||
private val notificationPermissionManager: NotificationPermissionManager,
|
||||
private val spacePermissionProvider: UserPermissionProvider,
|
||||
private val notificationBuilder: NotificationBuilder
|
||||
private val notificationBuilder: NotificationBuilder,
|
||||
private val generateSpaceInviteLink: GenerateSpaceInviteLink,
|
||||
private val makeSpaceShareable: MakeSpaceShareable,
|
||||
private val getSpaceInviteLink: GetSpaceInviteLink,
|
||||
private val revokeSpaceInviteLink: RevokeSpaceInviteLink
|
||||
) : BaseViewModel(), ExitToVaultDelegate by exitToVaultDelegate {
|
||||
|
||||
private val visibleRangeUpdates = MutableSharedFlow<Pair<Id, Id>>(
|
||||
|
@ -106,6 +121,10 @@ class ChatViewModel @Inject constructor(
|
|||
val mentionPanelState = MutableStateFlow<MentionPanelState>(MentionPanelState.Hidden)
|
||||
val showNotificationPermissionDialog = MutableStateFlow(false)
|
||||
val canCreateInviteLink = MutableStateFlow(false)
|
||||
val inviteModalState = MutableStateFlow<InviteModalState>(InviteModalState.Hidden)
|
||||
val isGeneratingInviteLink = MutableStateFlow(false)
|
||||
val spaceAccessType = MutableStateFlow<SpaceAccessType?>(null)
|
||||
val errorState = MutableStateFlow<UiErrorState>(UiErrorState.Hidden)
|
||||
|
||||
private val dateFormatter = SimpleDateFormat("d MMMM YYYY")
|
||||
private val messageRateLimiter = MessageRateLimiter()
|
||||
|
@ -150,6 +169,28 @@ class ChatViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// Check if we should show invite modal for newly created Chat spaces
|
||||
viewModelScope.launch {
|
||||
combine(
|
||||
spaceViews.observe(vmParams.space),
|
||||
canCreateInviteLink,
|
||||
uiState
|
||||
) { spaceView, canCreateInvite, chatState ->
|
||||
// Show invite modal if:
|
||||
// 1. It's a Chat space (spaceUxType == SpaceUxType.CHAT)
|
||||
// 2. User can create invite links (is owner)
|
||||
// 3. Chat is empty (no messages) - indicates it's newly created
|
||||
spaceView.spaceUxType == SpaceUxType.CHAT &&
|
||||
canCreateInvite &&
|
||||
chatState.messages.isEmpty()
|
||||
}.collect { shouldShow ->
|
||||
Timber.d("DROID-3626 Should show invite modal: $shouldShow")
|
||||
if (shouldShow) {
|
||||
inviteModalState.value = InviteModalState.ShowGenerateCard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
visibleRangeUpdates
|
||||
.distinctUntilChanged()
|
||||
|
@ -172,6 +213,8 @@ class ChatViewModel @Inject constructor(
|
|||
chat = vmParams.ctx
|
||||
)
|
||||
}
|
||||
|
||||
proceedWithSpaceSubscription()
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
|
@ -1059,6 +1102,159 @@ class ChatViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onInviteModalDismissed() {
|
||||
inviteModalState.value = InviteModalState.Hidden
|
||||
}
|
||||
|
||||
fun onGenerateInviteLinkClicked() {
|
||||
viewModelScope.launch {
|
||||
isGeneratingInviteLink.value = true
|
||||
proceedWithGeneratingInviteLink()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithGeneratingInviteLink(
|
||||
inviteType: InviteType = InviteType.MEMBER,
|
||||
permissions: SpaceMemberPermissions = SpaceMemberPermissions.READER
|
||||
) {
|
||||
if (spaceAccessType.value == SpaceAccessType.PRIVATE) {
|
||||
makeSpaceShareable.async(
|
||||
params = vmParams.space
|
||||
).fold(
|
||||
onSuccess = {
|
||||
Timber.d("Successfully made space shareable")
|
||||
generateInviteLink(
|
||||
inviteType = inviteType,
|
||||
permissions = permissions
|
||||
)
|
||||
},
|
||||
onFailure = { error ->
|
||||
Timber.e(error, "Error while making space shareable")
|
||||
isGeneratingInviteLink.value = false
|
||||
inviteModalState.value = InviteModalState.Hidden
|
||||
errorState.value = UiErrorState.Show(
|
||||
"Failed to make space shareable. Please try again."
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
generateInviteLink(
|
||||
inviteType = inviteType,
|
||||
permissions = permissions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun generateInviteLink(
|
||||
inviteType: InviteType,
|
||||
permissions: SpaceMemberPermissions
|
||||
) {
|
||||
generateSpaceInviteLink.async(
|
||||
params = GenerateSpaceInviteLink.Params(
|
||||
space = vmParams.space,
|
||||
inviteType = inviteType,
|
||||
permissions = permissions
|
||||
)
|
||||
).fold(
|
||||
onSuccess = { inviteLink ->
|
||||
Timber.d("Successfully generated invite link: ${inviteLink.scheme}")
|
||||
isGeneratingInviteLink.value = false
|
||||
inviteModalState.value = InviteModalState.ShowShareCard(inviteLink.scheme)
|
||||
},
|
||||
onFailure = { error ->
|
||||
Timber.e(error, "Error while generating invite link")
|
||||
isGeneratingInviteLink.value = false
|
||||
inviteModalState.value = InviteModalState.Hidden
|
||||
errorState.value = UiErrorState.Show(
|
||||
"Failed to generate invite link. Please try again."
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun onShareInviteLinkClicked() {
|
||||
viewModelScope.launch {
|
||||
// Check if we already have a link, if so show share card, otherwise show generate card
|
||||
when (val currentState = inviteModalState.value) {
|
||||
is InviteModalState.ShowShareCard -> {
|
||||
// Already showing share card - do nothing or could toggle visibility
|
||||
return@launch
|
||||
}
|
||||
else -> {
|
||||
// Check if we have an existing link
|
||||
val existingLink = getSpaceInviteLink.async(vmParams.space)
|
||||
if (existingLink.isSuccess) {
|
||||
inviteModalState.value = InviteModalState.ShowShareCard(existingLink.getOrThrow().scheme)
|
||||
} else {
|
||||
inviteModalState.value = InviteModalState.ShowGenerateCard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onShareInviteLinkFromCardClicked() {
|
||||
viewModelScope.launch {
|
||||
when (val state = inviteModalState.value) {
|
||||
is InviteModalState.ShowShareCard -> {
|
||||
commands.emit(ViewModelCommand.ShareInviteLink(state.link))
|
||||
}
|
||||
else -> {
|
||||
Timber.w("Ignoring share invite click while in state: $state")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onShareQrCodeClicked() {
|
||||
viewModelScope.launch {
|
||||
when (val state = inviteModalState.value) {
|
||||
is InviteModalState.ShowShareCard -> {
|
||||
commands.emit(ViewModelCommand.ShareQrCode(state.link))
|
||||
}
|
||||
else -> {
|
||||
Timber.w("Ignoring QR-code click while in state: $state")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteLinkClicked() {
|
||||
Timber.d("onDeleteLinkClicked")
|
||||
viewModelScope.launch {
|
||||
if (canCreateInviteLink.value) {
|
||||
commands.emit(ViewModelCommand.ShowDeleteLinkWarning)
|
||||
} else {
|
||||
Timber.w("Something wrong with permissions.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteLinkAccepted() {
|
||||
Timber.d("onDeleteLinkAccepted")
|
||||
viewModelScope.launch {
|
||||
if (canCreateInviteLink.value) {
|
||||
revokeSpaceInviteLink.async(
|
||||
params = vmParams.space
|
||||
).fold(
|
||||
onSuccess = {
|
||||
Timber.d("Revoked space invite link")
|
||||
inviteModalState.value = InviteModalState.Hidden
|
||||
},
|
||||
onFailure = { e ->
|
||||
Timber.e(e, "Error while revoking space invite link")
|
||||
inviteModalState.value = InviteModalState.Hidden
|
||||
errorState.value = UiErrorState.Show(
|
||||
"Failed to delete invite link. Please try again."
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Timber.w("Something wrong with permissions.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onMentionClicked(member: Id) {
|
||||
viewModelScope.launch {
|
||||
commands.emit(
|
||||
|
@ -1234,6 +1430,25 @@ class ChatViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun hideError() {
|
||||
errorState.value = UiErrorState.Hidden
|
||||
}
|
||||
|
||||
private fun proceedWithSpaceSubscription() {
|
||||
viewModelScope.launch {
|
||||
spaceViews.observe().collect { spaces ->
|
||||
val space = spaces.firstOrNull { it.targetSpaceId == vmParams.space.id }
|
||||
spaceAccessType.value = space?.spaceAccessType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class InviteModalState {
|
||||
data object Hidden : InviteModalState()
|
||||
data object ShowGenerateCard : InviteModalState()
|
||||
data class ShowShareCard(val link: String) : InviteModalState()
|
||||
}
|
||||
|
||||
data class ChatBoxMediaUri(
|
||||
val uri: String,
|
||||
val isVideo: Boolean = false
|
||||
|
@ -1247,6 +1462,9 @@ class ChatViewModel @Inject constructor(
|
|||
data class SelectChatReaction(val msg: Id) : ViewModelCommand()
|
||||
data class ViewChatReaction(val msg: Id, val emoji: String) : ViewModelCommand()
|
||||
data class ViewMemberCard(val member: Id, val space: SpaceId) : ViewModelCommand()
|
||||
data class ShareInviteLink(val link: String) : ViewModelCommand()
|
||||
data class ShareQrCode(val link: String) : ViewModelCommand()
|
||||
data object ShowDeleteLinkWarning : ViewModelCommand()
|
||||
}
|
||||
|
||||
sealed class UXCommand {
|
||||
|
@ -1314,6 +1532,11 @@ class ChatViewModel @Inject constructor(
|
|||
) : HeaderView()
|
||||
}
|
||||
|
||||
sealed class UiErrorState {
|
||||
data object Hidden : UiErrorState()
|
||||
data class Show(val msg: String) : UiErrorState()
|
||||
}
|
||||
|
||||
sealed class Params {
|
||||
abstract val space: Space
|
||||
data class Default(
|
||||
|
|
|
@ -13,6 +13,10 @@ import com.anytypeio.anytype.domain.media.UploadFile
|
|||
import com.anytypeio.anytype.domain.misc.GetLinkPreview
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.GenerateSpaceInviteLink
|
||||
import com.anytypeio.anytype.domain.multiplayer.GetSpaceInviteLink
|
||||
import com.anytypeio.anytype.domain.multiplayer.MakeSpaceShareable
|
||||
import com.anytypeio.anytype.domain.multiplayer.RevokeSpaceInviteLink
|
||||
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
|
||||
import com.anytypeio.anytype.domain.notifications.NotificationBuilder
|
||||
|
@ -43,7 +47,11 @@ class ChatViewModelFactory @Inject constructor(
|
|||
private val createObjectFromUrl: CreateObjectFromUrl,
|
||||
private val notificationPermissionManager: NotificationPermissionManager,
|
||||
private val spacePermissionProvider: UserPermissionProvider,
|
||||
private val notificationBuilder: NotificationBuilder
|
||||
private val notificationBuilder: NotificationBuilder,
|
||||
private val generateSpaceInviteLink: GenerateSpaceInviteLink,
|
||||
private val makeSpaceShareable: MakeSpaceShareable,
|
||||
private val getSpaceInviteLink: GetSpaceInviteLink,
|
||||
private val revokeSpaceInviteLink: RevokeSpaceInviteLink
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = ChatViewModel(
|
||||
|
@ -66,6 +74,10 @@ class ChatViewModelFactory @Inject constructor(
|
|||
createObjectFromUrl = createObjectFromUrl,
|
||||
notificationPermissionManager = notificationPermissionManager,
|
||||
spacePermissionProvider = spacePermissionProvider,
|
||||
notificationBuilder = notificationBuilder
|
||||
notificationBuilder = notificationBuilder,
|
||||
generateSpaceInviteLink = generateSpaceInviteLink,
|
||||
makeSpaceShareable = makeSpaceShareable,
|
||||
getSpaceInviteLink = getSpaceInviteLink,
|
||||
revokeSpaceInviteLink = revokeSpaceInviteLink
|
||||
) as T
|
||||
}
|
|
@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
@ -247,7 +248,7 @@ fun ChatScreenWrapper(
|
|||
onVisibleRangeChanged = vm::onVisibleRangeChanged,
|
||||
onUrlInserted = vm::onUrlPasted,
|
||||
onGoToMentionClicked = vm::onGoToMentionClicked,
|
||||
onShareInviteClicked = { /* TODO: implement share invite */ },
|
||||
onShareInviteClicked = vm::onShareInviteLinkClicked,
|
||||
canCreateInviteLink = vm.canCreateInviteLink.collectAsStateWithLifecycle().value,
|
||||
isReadOnly = vm.chatBoxMode
|
||||
.collectAsStateWithLifecycle()
|
||||
|
@ -969,11 +970,11 @@ fun Messages(
|
|||
.padding(horizontal = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
AlertIcon(
|
||||
icon = AlertConfig.Icon(
|
||||
gradient = GRADIENT_TYPE_BLUE,
|
||||
icon = R.drawable.ic_alert_message
|
||||
)
|
||||
Image(
|
||||
modifier = Modifier.size(56.dp),
|
||||
painter = painterResource(id = R.drawable.ic_vault_create_space),
|
||||
contentDescription = "Empty state icon",
|
||||
colorFilter = ColorFilter.tint(colorResource(id = R.color.transparent_inactive))
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.chat_empty_state_title),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue