1
0
Fork 0
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:
Konstantin Ivanov 2025-06-06 10:29:36 +02:00 committed by GitHub
parent b9edbcb316
commit 960da97835
Signed by: github
GPG key ID: B5690EEEBB952194
10 changed files with 402 additions and 16 deletions

View file

@ -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
}

View file

@ -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"

View file

@ -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

View file

@ -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))

View file

@ -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>

View file

@ -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>

View file

@ -483,7 +483,10 @@ class ChatContainerTest {
)
assertEquals(
expected = ChatContainer.Intent.ScrollToMessage(id = "90"),
expected = ChatContainer.Intent.ScrollToMessage(
id = "90",
startOfUnreadMessageSection = true
),
actual = initial.intent,
)

View file

@ -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(

View file

@ -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
}

View file

@ -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),