1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-07 21:37:02 +09:00

DROID-3115 Chats | Fix | Read-only state for chat box (#2465)

This commit is contained in:
Evgenii Kozlov 2025-05-26 21:15:52 +02:00 committed by konstantiniiv
parent e130fb5602
commit 2baf2e1204
6 changed files with 160 additions and 65 deletions

View file

@ -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<Pair<Id, Id>>(
@ -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
}
}

View file

@ -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 <T : ViewModel> create(modelClass: Class<T>): T = ChatViewModel(
@ -62,6 +64,7 @@ class ChatViewModelFactory @Inject constructor(
exitToVaultDelegate = exitToVaultDelegate,
getLinkPreview = getLinkPreview,
createObjectFromUrl = createObjectFromUrl,
notificationPermissionManager = notificationPermissionManager
notificationPermissionManager = notificationPermissionManager,
spacePermissionProvider = spacePermissionProvider
) as T
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M9,3.75C10.243,3.75 11.25,4.757 11.25,6V8H6.75V6C6.75,4.757 7.757,3.75 9,3.75ZM5.5,8L5.5,6C5.5,4.067 7.067,2.5 9,2.5C10.933,2.5 12.5,4.067 12.5,6V8C13.328,8 14,8.672 14,9.5V14C14,14.828 13.328,15.5 12.5,15.5H5.5C4.672,15.5 4,14.828 4,14V9.5C4,8.672 4.672,8 5.5,8Z"
android:fillColor="@color/glyph_button"
android:fillType="evenOdd"/>
</vector>