From 2baf2e1204c3d889376ec83346326702173d294f Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Mon, 26 May 2025 21:15:52 +0200 Subject: [PATCH] DROID-3115 Chats | Fix | Read-only state for chat box (#2465) --- .../presentation/ChatViewModel.kt | 25 +++- .../presentation/ChatViewModelFactory.kt | 7 +- .../anytype/feature_chats/ui/ChatBox.kt | 44 ++++++- .../anytype/feature_chats/ui/ChatBubble.kt | 31 ++--- .../anytype/feature_chats/ui/ChatScreen.kt | 108 ++++++++++-------- .../src/main/res/drawable/ic_chatbox_lock.xml | 10 ++ 6 files changed, 160 insertions(+), 65 deletions(-) create mode 100644 feature-chats/src/main/res/drawable/ic_chatbox_lock.xml diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt index d8fd8d7564..b8d605d819 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt @@ -32,6 +32,7 @@ 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.SpaceViewSubscriptionContainer +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.objects.CreateObjectFromUrl import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.getTypeOfObject @@ -83,7 +84,8 @@ class ChatViewModel @Inject constructor( private val exitToVaultDelegate: ExitToVaultDelegate, private val getLinkPreview: GetLinkPreview, private val createObjectFromUrl: CreateObjectFromUrl, - private val notificationPermissionManager: NotificationPermissionManager + private val notificationPermissionManager: NotificationPermissionManager, + private val spacePermissionProvider: UserPermissionProvider ) : BaseViewModel(), ExitToVaultDelegate by exitToVaultDelegate { private val visibleRangeUpdates = MutableSharedFlow>( @@ -110,6 +112,20 @@ class ChatViewModel @Inject constructor( // generateDummyChatHistory() + viewModelScope.launch { + spacePermissionProvider + .observe(vmParams.space) + .collect { permission -> + if (permission?.isOwnerOrEditor() == true) { + if (chatBoxMode.value is ChatBoxMode.ReadOnly) { + chatBoxMode.value = ChatBoxMode.Default() + } + } else { + chatBoxMode.value = ChatBoxMode.ReadOnly + } + } + } + viewModelScope.launch { spaceViews .observe( @@ -663,6 +679,9 @@ class ChatViewModel @Inject constructor( chatBoxAttachments.value = emptyList() chatBoxMode.value = ChatBoxMode.Default() } + is ChatBoxMode.ReadOnly -> { + // Do nothing. + } } } } @@ -1150,6 +1169,9 @@ class ChatViewModel @Inject constructor( abstract val isSendingMessageBlocked: Boolean + data object ReadOnly : ChatBoxMode() { + override val isSendingMessageBlocked: Boolean = true + } data class Default( override val isSendingMessageBlocked: Boolean = false ) : ChatBoxMode() @@ -1170,6 +1192,7 @@ class ChatViewModel @Inject constructor( is ChatBoxMode.Default -> copy(isSendingMessageBlocked = isBlocked) is ChatBoxMode.EditMessage -> copy(isSendingMessageBlocked = isBlocked) is ChatBoxMode.Reply -> copy(isSendingMessageBlocked = isBlocked) + is ChatBoxMode.ReadOnly -> this } } diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt index 72ad71dc2c..3e15a1c5d1 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt @@ -14,6 +14,7 @@ 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.SpaceViewSubscriptionContainer +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.`object`.OpenObject import com.anytypeio.anytype.domain.`object`.SetObjectDetails import com.anytypeio.anytype.domain.objects.CreateObjectFromUrl @@ -41,7 +42,8 @@ class ChatViewModelFactory @Inject constructor( private val exitToVaultDelegate: ExitToVaultDelegate, private val getLinkPreview: GetLinkPreview, private val createObjectFromUrl: CreateObjectFromUrl, - private val notificationPermissionManager: NotificationPermissionManager + private val notificationPermissionManager: NotificationPermissionManager, + private val spacePermissionProvider: UserPermissionProvider ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = ChatViewModel( @@ -62,6 +64,7 @@ class ChatViewModelFactory @Inject constructor( exitToVaultDelegate = exitToVaultDelegate, getLinkPreview = getLinkPreview, createObjectFromUrl = createObjectFromUrl, - notificationPermissionManager = notificationPermissionManager + notificationPermissionManager = notificationPermissionManager, + spacePermissionProvider = spacePermissionProvider ) as T } \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt index 4d12c82f83..4eaffb7b45 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.colorResource @@ -63,6 +64,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.Url +import com.anytypeio.anytype.core_models.primitives.Space import com.anytypeio.anytype.core_ui.common.DEFAULT_DISABLED_ALPHA import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.common.FULL_ALPHA @@ -149,10 +151,13 @@ fun ChatBox( ) when(mode) { is ChatBoxMode.Default -> { - + // Do nothing } is ChatBoxMode.EditMessage -> { - + // Do nothing + } + is ChatBoxMode.ReadOnly -> { + // Do nothing } is ChatBoxMode.Reply -> { Box( @@ -884,6 +889,41 @@ fun ChatBoxEditPanel( } } +@Composable +fun ReaderChatBox(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = colorResource(R.color.navigation_panel), + shape = RoundedCornerShape(16.dp) + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.ic_chatbox_lock), + contentDescription = "Lock icon" + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + modifier = Modifier.padding(start = 12.dp), + text = "Only editors can send messages. Contact the owner to request access.", + style = Caption1Regular, + color = colorResource(R.color.text_primary) + ) + } +} + +@DefaultPreviews +@Composable +fun ReaderChatBoxPreview() { + ReaderChatBox() +} + + sealed class ChatMarkupEvent { data object Bold : ChatMarkupEvent() data object Italic : ChatMarkupEvent() diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt index 60107d5de3..7ef7181489 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt @@ -94,7 +94,8 @@ fun Bubble( onScrollToReplyClicked: (ChatView.Message.Reply) -> Unit, onAddReactionClicked: () -> Unit, onViewChatReaction: (String) -> Unit, - onMentionClicked: (Id) -> Unit + onMentionClicked: (Id) -> Unit, + isReadOnly: Boolean = false ) { var showDropdownMenu by remember { mutableStateOf(false) } var showDeleteMessageWarning by remember { mutableStateOf(false) } @@ -318,19 +319,21 @@ fun Bubble( ) Divider(paddingStart = 0.dp, paddingEnd = 0.dp) } - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.chats_reply), - color = colorResource(id = R.color.text_primary), - modifier = Modifier.padding(end = 64.dp) - ) - }, - onClick = { - onReply() - showDropdownMenu = false - } - ) + if (!isReadOnly) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.chats_reply), + color = colorResource(id = R.color.text_primary), + modifier = Modifier.padding(end = 64.dp) + ) + }, + onClick = { + onReply() + showDropdownMenu = false + } + ) + } if (content.msg.isNotEmpty()) { Divider(paddingStart = 0.dp, paddingEnd = 0.dp) DropdownMenuItem( diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt index 4f988bfcd7..1e3f16a892 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt @@ -228,7 +228,10 @@ fun ChatScreenWrapper( onScrollToBottomClicked = vm::onScrollToBottomClicked, onVisibleRangeChanged = vm::onVisibleRangeChanged, onUrlInserted = vm::onUrlPasted, - onGoToMentionClicked = vm::onGoToMentionClicked + onGoToMentionClicked = vm::onGoToMentionClicked, + isReadOnly = vm.chatBoxMode + .collectAsStateWithLifecycle() + .value is ChatBoxMode.ReadOnly ) LaunchedEffect(Unit) { vm.uXCommands.collect { command -> @@ -342,7 +345,8 @@ fun ChatScreen( onScrollToBottomClicked: (Id?) -> Unit, onVisibleRangeChanged: (Id, Id) -> Unit, onUrlInserted: (Url) -> Unit, - onGoToMentionClicked: () -> Unit + onGoToMentionClicked: () -> Unit, + isReadOnly: Boolean = false ) { Timber.d("DROID-2966 Render called with state, number of messages: ${messages.size}") @@ -518,7 +522,8 @@ fun ChatScreen( onViewChatReaction = onViewChatReaction, onMemberIconClicked = onMemberIconClicked, onMentionClicked = onMentionClicked, - onScrollToReplyClicked = onScrollToReplyClicked + onScrollToReplyClicked = onScrollToReplyClicked, + isReadOnly = isReadOnly ) GoToMentionButton( @@ -694,52 +699,61 @@ fun ChatScreen( } } } - ChatBox( - mode = chatBoxMode, - modifier = Modifier - .imePadding() - .navigationBarsPadding(), - chatBoxFocusRequester = chatBoxFocusRequester, - onMessageSent = { text, markup -> - onMessageSent(text, markup) - }, - resetScroll = { - if (!isPerformingScrollIntent.value) { - scope.launch { - lazyListState.scrollToItem(0) - awaitFrame() - while (!isAtBottom) { - val offset = lazyListState.firstVisibleItemScrollOffset - val delta = (-offset).coerceAtLeast(-80) - lazyListState.animateScrollBy(delta.toFloat()) + + if (isReadOnly) { + ReaderChatBox( + modifier = Modifier + .padding(start = 20.dp, end = 20.dp, bottom = 12.dp) + .navigationBarsPadding() + ) + } else { + ChatBox( + mode = chatBoxMode, + modifier = Modifier + .imePadding() + .navigationBarsPadding(), + chatBoxFocusRequester = chatBoxFocusRequester, + onMessageSent = { text, markup -> + onMessageSent(text, markup) + }, + resetScroll = { + if (!isPerformingScrollIntent.value) { + scope.launch { + lazyListState.scrollToItem(0) awaitFrame() + while (!isAtBottom) { + val offset = lazyListState.firstVisibleItemScrollOffset + val delta = (-offset).coerceAtLeast(-80) + lazyListState.animateScrollBy(delta.toFloat()) + awaitFrame() + } } } - } - }, - attachments = attachments, - clearText = { - text = TextFieldValue() - }, - onAttachObjectClicked = onAttachObjectClicked, - onClearAttachmentClicked = onClearAttachmentClicked, - onClearReplyClicked = onClearReplyClicked, - onChatBoxMediaPicked = onChatBoxMediaPicked, - onChatBoxFilePicked = onChatBoxFilePicked, - onExitEditMessageMode = { - onExitEditMessageMode().also { + }, + attachments = attachments, + clearText = { text = TextFieldValue() - } - }, - onValueChange = { t, s -> - text = t - spans = s - onTextChanged(t) - }, - text = text, - spans = spans, - onUrlInserted = onUrlInserted - ) + }, + onAttachObjectClicked = onAttachObjectClicked, + onClearAttachmentClicked = onClearAttachmentClicked, + onClearReplyClicked = onClearReplyClicked, + onChatBoxMediaPicked = onChatBoxMediaPicked, + onChatBoxFilePicked = onChatBoxFilePicked, + onExitEditMessageMode = { + onExitEditMessageMode().also { + text = TextFieldValue() + } + }, + onValueChange = { t, s -> + text = t + spans = s + onTextChanged(t) + }, + text = text, + spans = spans, + onUrlInserted = onUrlInserted + ) + } } } @@ -760,6 +774,7 @@ fun Messages( onMemberIconClicked: (Id?) -> Unit, onMentionClicked: (Id) -> Unit, onScrollToReplyClicked: (Id) -> Unit, + isReadOnly: Boolean = false ) { // Timber.d("DROID-2966 Messages composition: ${messages.map { if (it is ChatView.Message) it.content.msg else it }}") val scope = rememberCoroutineScope() @@ -848,7 +863,8 @@ fun Messages( onViewChatReaction = { emoji -> onViewChatReaction(msg.id, emoji) }, - onMentionClicked = onMentionClicked + onMentionClicked = onMentionClicked, + isReadOnly = isReadOnly ) } if (idx == messages.lastIndex) { diff --git a/feature-chats/src/main/res/drawable/ic_chatbox_lock.xml b/feature-chats/src/main/res/drawable/ic_chatbox_lock.xml new file mode 100644 index 0000000000..f0b04268bc --- /dev/null +++ b/feature-chats/src/main/res/drawable/ic_chatbox_lock.xml @@ -0,0 +1,10 @@ + + +