Merge branch 'develop' into feature/fga/pinned_message_banner_ui
This commit is contained in:
commit
8852735b70
128 changed files with 2576 additions and 962 deletions
|
|
@ -58,7 +58,11 @@ import kotlin.math.roundToInt
|
|||
* @param modifier The modifier for the layout.
|
||||
* @param sheetContentKey The key for the sheet content. If the key changes, the sheet will be remeasured.
|
||||
*/
|
||||
@Suppress("ContentTrailingLambda")
|
||||
@Suppress(
|
||||
"ContentTrailingLambda",
|
||||
// False positive
|
||||
"MultipleEmitters",
|
||||
)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ExpandableBottomSheetScaffold(
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ import android.os.Build
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
|
|
@ -75,12 +77,13 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
|||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.room.canCall
|
||||
import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState
|
||||
import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState
|
||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
|
@ -135,10 +138,9 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present()
|
||||
|
||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
||||
val userHasPermissionToRedactOwn by room.canRedactOwnAsState(updateKey = syncUpdateFlow.value)
|
||||
val userHasPermissionToRedactOther by room.canRedactOtherAsState(updateKey = syncUpdateFlow.value)
|
||||
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = syncUpdateFlow.value)
|
||||
|
||||
val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
|
||||
|
||||
val roomName: AsyncData<String> by remember {
|
||||
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
||||
}
|
||||
|
|
@ -215,11 +217,8 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
roomName = roomName,
|
||||
roomAvatar = roomAvatar,
|
||||
heroes = heroes,
|
||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
||||
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||
composerState = composerState,
|
||||
userEventPermissions = userEventPermissions,
|
||||
voiceMessageComposerState = voiceMessageComposerState,
|
||||
timelineState = timelineState,
|
||||
typingNotificationState = typingNotificationState,
|
||||
|
|
@ -240,6 +239,19 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun userEventPermissions(updateKey: Long): State<UserEventPermissions> {
|
||||
return produceState(UserEventPermissions.DEFAULT, key1 = updateKey) {
|
||||
value = UserEventPermissions(
|
||||
canSendMessage = room.canSendMessage(type = MessageEventType.ROOM_MESSAGE).getOrElse { true },
|
||||
canSendReaction = room.canSendMessage(type = MessageEventType.REACTION).getOrElse { true },
|
||||
canRedactOwn = room.canRedactOwn().getOrElse { false },
|
||||
canRedactOther = room.canRedactOther().getOrElse { false },
|
||||
canPinUnpin = room.canPinUnpin().getOrElse { false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MatrixRoomInfo.avatarData(): AvatarData {
|
||||
return AvatarData(
|
||||
id = id.value,
|
||||
|
|
@ -273,6 +285,30 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
TimelineItemAction.Forward -> handleForwardAction(targetEvent)
|
||||
TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
|
||||
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent, timelineState)
|
||||
TimelineItemAction.Pin -> handlePinAction(targetEvent)
|
||||
TimelineItemAction.Unpin -> handleUnpinAction(targetEvent)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handlePinAction(targetEvent: TimelineItem.Event) {
|
||||
if (targetEvent.eventId == null) return
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
pinEvent(targetEvent.eventId)
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to pin event ${targetEvent.eventId}")
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
|
||||
if (targetEvent.eventId == null) return
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
unpinEvent(targetEvent.eventId)
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to unpin event ${targetEvent.eventId}")
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,10 +38,7 @@ data class MessagesState(
|
|||
val roomName: AsyncData<String>,
|
||||
val roomAvatar: AsyncData<AvatarData>,
|
||||
val heroes: ImmutableList<AvatarData>,
|
||||
val userHasPermissionToSendMessage: Boolean,
|
||||
val userHasPermissionToRedactOwn: Boolean,
|
||||
val userHasPermissionToRedactOther: Boolean,
|
||||
val userHasPermissionToSendReaction: Boolean,
|
||||
val userEventPermissions: UserEventPermissions,
|
||||
val composerState: MessageComposerState,
|
||||
val voiceMessageComposerState: VoiceMessageComposerState,
|
||||
val timelineState: TimelineState,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
|||
aMessagesState(),
|
||||
aMessagesState(hasNetworkConnection = false),
|
||||
aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
|
||||
aMessagesState(userHasPermissionToSendMessage = false),
|
||||
aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)),
|
||||
aMessagesState(showReinvitePrompt = true),
|
||||
aMessagesState(
|
||||
roomName = AsyncData.Uninitialized,
|
||||
|
|
@ -101,10 +101,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
|||
fun aMessagesState(
|
||||
roomName: AsyncData<String> = AsyncData.Success("Room name"),
|
||||
roomAvatar: AsyncData<AvatarData> = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
|
||||
userHasPermissionToSendMessage: Boolean = true,
|
||||
userHasPermissionToRedactOwn: Boolean = false,
|
||||
userHasPermissionToRedactOther: Boolean = false,
|
||||
userHasPermissionToSendReaction: Boolean = true,
|
||||
userEventPermissions: UserEventPermissions = aUserEventPermissions(),
|
||||
composerState: MessageComposerState = aMessageComposerState(
|
||||
textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)),
|
||||
isFullScreen = false,
|
||||
|
|
@ -131,10 +128,7 @@ fun aMessagesState(
|
|||
roomName = roomName,
|
||||
roomAvatar = roomAvatar,
|
||||
heroes = persistentListOf(),
|
||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
||||
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||
userEventPermissions = userEventPermissions,
|
||||
composerState = composerState,
|
||||
voiceMessageComposerState = voiceMessageComposerState,
|
||||
typingNotificationState = aTypingNotificationState(),
|
||||
|
|
@ -155,6 +149,20 @@ fun aMessagesState(
|
|||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
fun aUserEventPermissions(
|
||||
canRedactOwn: Boolean = false,
|
||||
canRedactOther: Boolean = false,
|
||||
canSendMessage: Boolean = true,
|
||||
canSendReaction: Boolean = true,
|
||||
canPinUnpin: Boolean = false,
|
||||
) = UserEventPermissions(
|
||||
canRedactOwn = canRedactOwn,
|
||||
canRedactOther = canRedactOther,
|
||||
canSendMessage = canSendMessage,
|
||||
canSendReaction = canSendReaction,
|
||||
canPinUnpin = canPinUnpin,
|
||||
)
|
||||
|
||||
fun aReactionSummaryState(
|
||||
target: ReactionSummaryState.Summary? = null,
|
||||
eventSink: (ReactionSummaryEvents) -> Unit = {}
|
||||
|
|
|
|||
|
|
@ -160,10 +160,7 @@ fun MessagesView(
|
|||
state.actionListState.eventSink(
|
||||
ActionListEvents.ComputeForMessage(
|
||||
event = event,
|
||||
canRedactOwn = state.userHasPermissionToRedactOwn,
|
||||
canRedactOther = state.userHasPermissionToRedactOther,
|
||||
canSendMessage = state.userHasPermissionToSendMessage,
|
||||
canSendReaction = state.userHasPermissionToSendReaction,
|
||||
userEventPermissions = state.userEventPermissions,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -426,7 +423,7 @@ private fun MessagesViewComposerBottomSheetContents(
|
|||
subcomposing: Boolean,
|
||||
state: MessagesState,
|
||||
) {
|
||||
if (state.userHasPermissionToSendMessage) {
|
||||
if (state.userEventPermissions.canSendMessage) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
MentionSuggestionsPickerView(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl
|
||||
|
||||
/**
|
||||
* Represents the permissions a user has in a room.
|
||||
* It's dependent of the user's power level in the room.
|
||||
*/
|
||||
data class UserEventPermissions(
|
||||
val canRedactOwn: Boolean,
|
||||
val canRedactOther: Boolean,
|
||||
val canSendMessage: Boolean,
|
||||
val canSendReaction: Boolean,
|
||||
val canPinUnpin: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
val DEFAULT = UserEventPermissions(
|
||||
canRedactOwn = false,
|
||||
canRedactOther = false,
|
||||
canSendMessage = true,
|
||||
canSendReaction = true,
|
||||
canPinUnpin = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,15 +16,13 @@
|
|||
|
||||
package io.element.android.features.messages.impl.actionlist
|
||||
|
||||
import io.element.android.features.messages.impl.UserEventPermissions
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
|
||||
sealed interface ActionListEvents {
|
||||
data object Clear : ActionListEvents
|
||||
data class ComputeForMessage(
|
||||
val event: TimelineItem.Event,
|
||||
val canRedactOwn: Boolean,
|
||||
val canRedactOther: Boolean,
|
||||
val canSendMessage: Boolean,
|
||||
val canSendReaction: Boolean,
|
||||
val userEventPermissions: UserEventPermissions,
|
||||
) : ActionListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,25 +23,36 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.messages.impl.UserEventPermissions
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
|
||||
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
|
||||
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ActionListPresenter @Inject constructor(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val featureFlagsService: FeatureFlagService,
|
||||
private val room: MatrixRoom,
|
||||
) : Presenter<ActionListState> {
|
||||
@Composable
|
||||
override fun present(): ActionListState {
|
||||
|
|
@ -52,17 +63,20 @@ class ActionListPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
|
||||
val isPinnedEventsEnabled by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false)
|
||||
val pinnedEventIds by remember {
|
||||
room.roomInfoFlow.map { it.pinnedEventIds }
|
||||
}.collectAsState(initial = persistentListOf())
|
||||
|
||||
fun handleEvents(event: ActionListEvents) {
|
||||
when (event) {
|
||||
ActionListEvents.Clear -> target.value = ActionListState.Target.None
|
||||
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
||||
timelineItem = event.event,
|
||||
userCanRedactOwn = event.canRedactOwn,
|
||||
userCanRedactOther = event.canRedactOther,
|
||||
userCanSendMessage = event.canSendMessage,
|
||||
userCanSendReaction = event.canSendReaction,
|
||||
usersEventPermissions = event.userEventPermissions,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isPinnedEventsEnabled = isPinnedEventsEnabled,
|
||||
pinnedEventIds = pinnedEventIds,
|
||||
target = target,
|
||||
)
|
||||
}
|
||||
|
|
@ -76,136 +90,22 @@ class ActionListPresenter @Inject constructor(
|
|||
|
||||
private fun CoroutineScope.computeForMessage(
|
||||
timelineItem: TimelineItem.Event,
|
||||
userCanRedactOwn: Boolean,
|
||||
userCanRedactOther: Boolean,
|
||||
userCanSendMessage: Boolean,
|
||||
userCanSendReaction: Boolean,
|
||||
usersEventPermissions: UserEventPermissions,
|
||||
isDeveloperModeEnabled: Boolean,
|
||||
isPinnedEventsEnabled: Boolean,
|
||||
pinnedEventIds: ImmutableList<EventId>,
|
||||
target: MutableState<ActionListState.Target>
|
||||
) = launch {
|
||||
target.value = ActionListState.Target.Loading(timelineItem)
|
||||
val canRedact = timelineItem.isMine && userCanRedactOwn || !timelineItem.isMine && userCanRedactOther
|
||||
val actions =
|
||||
when (timelineItem.content) {
|
||||
is TimelineItemCallNotifyContent -> {
|
||||
if (isDeveloperModeEnabled) {
|
||||
listOf(TimelineItemAction.ViewSource)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
is TimelineItemRedactedContent -> {
|
||||
if (isDeveloperModeEnabled) {
|
||||
listOf(TimelineItemAction.ViewSource)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
is TimelineItemStateContent -> {
|
||||
buildList {
|
||||
add(TimelineItemAction.Copy)
|
||||
if (timelineItem.isRemote) {
|
||||
add(TimelineItemAction.CopyLink)
|
||||
}
|
||||
if (isDeveloperModeEnabled) {
|
||||
add(TimelineItemAction.ViewSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TimelineItemPollContent -> {
|
||||
val canEndPoll = timelineItem.isRemote &&
|
||||
!timelineItem.content.isEnded &&
|
||||
(timelineItem.isMine || canRedact)
|
||||
buildList {
|
||||
if (timelineItem.isRemote) {
|
||||
// Can only reply or forward messages already uploaded to the server
|
||||
add(TimelineItemAction.Reply)
|
||||
}
|
||||
if (timelineItem.isRemote && timelineItem.isEditable) {
|
||||
add(TimelineItemAction.Edit)
|
||||
}
|
||||
if (canEndPoll) {
|
||||
add(TimelineItemAction.EndPoll)
|
||||
}
|
||||
if (timelineItem.content.canBeCopied()) {
|
||||
add(TimelineItemAction.Copy)
|
||||
}
|
||||
if (timelineItem.isRemote) {
|
||||
add(TimelineItemAction.CopyLink)
|
||||
}
|
||||
if (isDeveloperModeEnabled) {
|
||||
add(TimelineItemAction.ViewSource)
|
||||
}
|
||||
if (!timelineItem.isMine) {
|
||||
add(TimelineItemAction.ReportContent)
|
||||
}
|
||||
if (canRedact) {
|
||||
add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TimelineItemVoiceContent -> {
|
||||
buildList {
|
||||
if (timelineItem.isRemote) {
|
||||
add(TimelineItemAction.Reply)
|
||||
add(TimelineItemAction.Forward)
|
||||
add(TimelineItemAction.CopyLink)
|
||||
}
|
||||
if (isDeveloperModeEnabled) {
|
||||
add(TimelineItemAction.ViewSource)
|
||||
}
|
||||
if (!timelineItem.isMine) {
|
||||
add(TimelineItemAction.ReportContent)
|
||||
}
|
||||
if (canRedact) {
|
||||
add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TimelineItemLegacyCallInviteContent -> {
|
||||
buildList {
|
||||
if (isDeveloperModeEnabled) {
|
||||
add(TimelineItemAction.ViewSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> buildList<TimelineItemAction> {
|
||||
if (timelineItem.isRemote) {
|
||||
// Can only reply or forward messages already uploaded to the server
|
||||
if (userCanSendMessage) {
|
||||
if (timelineItem.isThreaded) {
|
||||
add(TimelineItemAction.ReplyInThread)
|
||||
} else {
|
||||
add(TimelineItemAction.Reply)
|
||||
}
|
||||
}
|
||||
// Stickers can't be forwarded (yet) so we don't show the option
|
||||
// See https://github.com/element-hq/element-x-android/issues/2161
|
||||
if (!timelineItem.isSticker) {
|
||||
add(TimelineItemAction.Forward)
|
||||
}
|
||||
}
|
||||
if (timelineItem.isEditable) {
|
||||
add(TimelineItemAction.Edit)
|
||||
}
|
||||
if (timelineItem.content.canBeCopied()) {
|
||||
add(TimelineItemAction.Copy)
|
||||
}
|
||||
if (timelineItem.isRemote) {
|
||||
add(TimelineItemAction.CopyLink)
|
||||
}
|
||||
if (isDeveloperModeEnabled) {
|
||||
add(TimelineItemAction.ViewSource)
|
||||
}
|
||||
if (!timelineItem.isMine) {
|
||||
add(TimelineItemAction.ReportContent)
|
||||
}
|
||||
if (canRedact) {
|
||||
add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
}
|
||||
val displayEmojiReactions = userCanSendReaction &&
|
||||
|
||||
val actions = buildActions(
|
||||
timelineItem = timelineItem,
|
||||
usersEventPermissions = usersEventPermissions,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isPinnedEventsEnabled = isPinnedEventsEnabled,
|
||||
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
|
||||
)
|
||||
val displayEmojiReactions = usersEventPermissions.canSendReaction &&
|
||||
timelineItem.isRemote &&
|
||||
timelineItem.content.canReact()
|
||||
if (actions.isNotEmpty() || displayEmojiReactions) {
|
||||
|
|
@ -219,3 +119,71 @@ class ActionListPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildActions(
|
||||
timelineItem: TimelineItem.Event,
|
||||
usersEventPermissions: UserEventPermissions,
|
||||
isDeveloperModeEnabled: Boolean,
|
||||
isPinnedEventsEnabled: Boolean,
|
||||
isEventPinned: Boolean,
|
||||
): List<TimelineItemAction> {
|
||||
val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther
|
||||
return buildList {
|
||||
if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) {
|
||||
if (timelineItem.isThreaded) {
|
||||
add(TimelineItemAction.ReplyInThread)
|
||||
} else {
|
||||
add(TimelineItemAction.Reply)
|
||||
}
|
||||
}
|
||||
if (timelineItem.isRemote && timelineItem.content.canBeForwarded()) {
|
||||
add(TimelineItemAction.Forward)
|
||||
}
|
||||
if (timelineItem.isEditable) {
|
||||
add(TimelineItemAction.Edit)
|
||||
}
|
||||
if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) {
|
||||
add(TimelineItemAction.EndPoll)
|
||||
}
|
||||
val canPinUnpin = isPinnedEventsEnabled && usersEventPermissions.canPinUnpin && timelineItem.isRemote
|
||||
if (canPinUnpin) {
|
||||
if (isEventPinned) {
|
||||
add(TimelineItemAction.Unpin)
|
||||
} else {
|
||||
add(TimelineItemAction.Pin)
|
||||
}
|
||||
}
|
||||
if (timelineItem.content.canBeCopied()) {
|
||||
add(TimelineItemAction.Copy)
|
||||
}
|
||||
if (timelineItem.isRemote) {
|
||||
add(TimelineItemAction.CopyLink)
|
||||
}
|
||||
if (isDeveloperModeEnabled) {
|
||||
add(TimelineItemAction.ViewSource)
|
||||
}
|
||||
if (!timelineItem.isMine) {
|
||||
add(TimelineItemAction.ReportContent)
|
||||
}
|
||||
if (canRedact) {
|
||||
add(TimelineItemAction.Redact)
|
||||
}
|
||||
}.postFilter(timelineItem.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Post filter the actions based on the content of the event.
|
||||
*/
|
||||
private fun List<TimelineItemAction>.postFilter(content: TimelineItemEventContent): List<TimelineItemAction> {
|
||||
return filter { action ->
|
||||
when (content) {
|
||||
is TimelineItemCallNotifyContent,
|
||||
is TimelineItemLegacyCallInviteContent,
|
||||
is TimelineItemStateContent,
|
||||
is TimelineItemRedactedContent -> {
|
||||
action == TimelineItemAction.ViewSource
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ private fun SheetContent(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MultipleEmitters") // False positive
|
||||
@Composable
|
||||
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
|
||||
val content: @Composable () -> Unit
|
||||
|
|
|
|||
|
|
@ -39,4 +39,8 @@ sealed class TimelineItemAction(
|
|||
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options)
|
||||
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true)
|
||||
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end)
|
||||
data object Pin : TimelineItemAction(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin)
|
||||
|
||||
// TODO use the Unpin compound icon when available.
|
||||
data object Unpin : TimelineItemAction(CommonStrings.action_unpin, CompoundDrawables.ic_compound_pin)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ internal fun aTimelineItemEvent(
|
|||
transactionId: TransactionId? = null,
|
||||
isMine: Boolean = false,
|
||||
isEditable: Boolean = false,
|
||||
canBeRepliedTo: Boolean = false,
|
||||
senderDisplayName: String = "Sender",
|
||||
displayNameAmbiguous: Boolean = false,
|
||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||
|
|
@ -150,6 +151,7 @@ internal fun aTimelineItemEvent(
|
|||
sentTime = "12:34",
|
||||
isMine = isMine,
|
||||
isEditable = isEditable,
|
||||
canBeRepliedTo = canBeRepliedTo,
|
||||
senderProfile = aProfileTimelineDetailsReady(
|
||||
displayName = senderDisplayName,
|
||||
displayNameAmbiguous = displayNameAmbiguous,
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
|
|
@ -148,7 +147,7 @@ fun TimelineItemEventRow(
|
|||
} else {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
}
|
||||
val canReply = timelineRoomInfo.userHasPermissionToSendMessage && event.content.canBeRepliedTo()
|
||||
val canReply = timelineRoomInfo.userHasPermissionToSendMessage && event.canBeRepliedTo
|
||||
if (canReply) {
|
||||
val state: SwipeableActionsState = rememberSwipeableActionsState()
|
||||
val offset = state.offset.floatValue
|
||||
|
|
@ -377,6 +376,7 @@ private fun MessageSenderInformation(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MultipleEmitters") // False positive
|
||||
@Composable
|
||||
private fun MessageEventBubbleContent(
|
||||
event: TimelineItem.Event,
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ fun TimelineItemReactionsLayout(
|
|||
}
|
||||
val rows = rowsIn.toMutableList()
|
||||
val secondLastRow = rows[rows.size - 2].toMutableList()
|
||||
val expandButtonPlaceable = secondLastRow.removeLast()
|
||||
val expandButtonPlaceable = secondLastRow.removeAt(secondLastRow.lastIndex)
|
||||
lastRow.add(0, expandButtonPlaceable)
|
||||
rows[rows.size - 2] = secondLastRow
|
||||
rows[rows.size - 1] = lastRow
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
content = contentFactory.create(currentTimelineItem.event),
|
||||
isMine = currentTimelineItem.event.isOwn,
|
||||
isEditable = currentTimelineItem.event.isEditable,
|
||||
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
|
||||
sentTime = sentTime,
|
||||
groupPosition = groupPosition,
|
||||
reactionsState = currentTimelineItem.computeReactionsState(),
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ sealed interface TimelineItem {
|
|||
val sentTime: String = "",
|
||||
val isMine: Boolean = false,
|
||||
val isEditable: Boolean,
|
||||
val canBeRepliedTo: Boolean,
|
||||
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||
val reactionsState: TimelineItemReactions,
|
||||
val readReceiptState: TimelineItemReadReceipts,
|
||||
|
|
|
|||
|
|
@ -24,27 +24,27 @@ sealed interface TimelineItemEventContent {
|
|||
}
|
||||
|
||||
/**
|
||||
* Only text based content and states can be copied.
|
||||
* Only text based content can be copied.
|
||||
*/
|
||||
fun TimelineItemEventContent.canBeCopied(): Boolean =
|
||||
when (this) {
|
||||
is TimelineItemTextBasedContent,
|
||||
is TimelineItemStateContent,
|
||||
is TimelineItemRedactedContent -> true
|
||||
else -> false
|
||||
}
|
||||
this is TimelineItemTextBasedContent
|
||||
|
||||
/**
|
||||
* Determine if the event content can be replied to.
|
||||
* Note: it should match the logic in [io.element.android.features.messages.impl.actionlist.ActionListPresenter].
|
||||
* Returns true if the event content can be forwarded.
|
||||
*/
|
||||
fun TimelineItemEventContent.canBeRepliedTo(): Boolean =
|
||||
fun TimelineItemEventContent.canBeForwarded(): Boolean =
|
||||
when (this) {
|
||||
is TimelineItemRedactedContent,
|
||||
is TimelineItemLegacyCallInviteContent,
|
||||
is TimelineItemCallNotifyContent,
|
||||
is TimelineItemStateContent -> false
|
||||
else -> true
|
||||
is TimelineItemTextBasedContent,
|
||||
is TimelineItemImageContent,
|
||||
is TimelineItemFileContent,
|
||||
is TimelineItemAudioContent,
|
||||
is TimelineItemVideoContent,
|
||||
is TimelineItemLocationContent,
|
||||
is TimelineItemVoiceContent -> true
|
||||
// Stickers can't be forwarded (yet) so we don't show the option
|
||||
// See https://github.com/element-hq/element-x-android/issues/2161
|
||||
is TimelineItemStickerContent -> false
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Suppress("MultipleEmitters") // False positive
|
||||
@Composable
|
||||
fun TypingNotificationView(
|
||||
state: TypingNotificationState,
|
||||
|
|
|
|||
|
|
@ -39,12 +39,18 @@
|
|||
<string name="screen_room_timeline_read_marker_title">"Nowe"</string>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d zmiana pokoju"</item>
|
||||
<item quantity="few">"%1$d zmian pokoju"</item>
|
||||
<item quantity="many">"%1$d zmiany pokoju"</item>
|
||||
<item quantity="few">"%1$d zmiany pokoju"</item>
|
||||
<item quantity="many">"%1$d zmian pokoju"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="one">"%1$s, %2$s i %3$d inny"</item>
|
||||
<item quantity="few">"%1$s, %2$s i %3$d innych"</item>
|
||||
<item quantity="many">"%1$s, %2$s i %3$d innych"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="one">"%1$s piszę"</item>
|
||||
<item quantity="few">"%1$s piszą"</item>
|
||||
<item quantity="many">"%1$s piszą"</item>
|
||||
<item quantity="many">"%1$s pisze"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_typing_two_members">"%1$s i %2$s"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
<string name="screen_room_encrypted_history_banner">"O histórico de mensagens não está disponível no momento."</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Gostaria de convidá-los de volta?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"Você está sozinho neste chat"</string>
|
||||
<string name="screen_room_mentions_at_room_subtitle">"Notificar a sala inteira"</string>
|
||||
<string name="screen_room_mentions_at_room_title">"Todos"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Enviar novamente"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Sua mensagem não foi enviada"</string>
|
||||
|
|
@ -36,7 +37,16 @@
|
|||
<string name="screen_room_timeline_reactions_show_more">"Mostrar mais"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"Novo"</string>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d mudança de sala"</item>
|
||||
<item quantity="other">"%1$d mudanças de salas"</item>
|
||||
<item quantity="one">"%1$d alteração na sala"</item>
|
||||
<item quantity="other">"%1$d alterações na sala"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="one">"%1$s, %2$s e %3$d outro"</item>
|
||||
<item quantity="other">"%1$s, %2$s e %3$d outros"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="one">"%1$s está digitando"</item>
|
||||
<item quantity="other">"%1$s estão digitando"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_typing_two_members">"%1$s e %2$s"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="emoji_picker_category_activity">"Faoliyatlar"</string>
|
||||
<string name="emoji_picker_category_flags">"Bayroqlar"</string>
|
||||
<string name="emoji_picker_category_foods">"Oziq-ovqat va ichimliklar"</string>
|
||||
<string name="emoji_picker_category_nature">"Hayvonlar va tabiat"</string>
|
||||
<string name="emoji_picker_category_objects">"Ob\'ektlar"</string>
|
||||
<string name="emoji_picker_category_people">"Smayllar va odamlar"</string>
|
||||
<string name="emoji_picker_category_places">"Sayohat va Joylar"</string>
|
||||
<string name="emoji_picker_category_symbols">"Belgilar"</string>
|
||||
<string name="screen_report_content_block_user">"Foydalanuvchini bloklash"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Ushbu foydalanuvchidan barcha joriy va kelajakdagi xabarlarni yashirishni xohlayotganingizni tekshiring"</string>
|
||||
<string name="screen_report_content_explanation">"Bu xabar uy serveringiz administratoriga xabar qilinadi. Ular hech qanday shifrlangan xabarlarni o\'qiy olmaydi."</string>
|
||||
<string name="screen_report_content_hint">"Ushbu kontent haqida xabar berish sababi"</string>
|
||||
<string name="screen_room_attachment_source_camera">"Kamera"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Rasmga olmoq"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"Video yozib olish"</string>
|
||||
<string name="screen_room_attachment_source_files">"Biriktirma"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Fotosurat va video kutubxonasi"</string>
|
||||
<string name="screen_room_attachment_source_location">"Joylashuv"</string>
|
||||
<string name="screen_room_attachment_source_poll">"So\'ro\'vnoma"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Matnni formatlash"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Xabarlar tarixi hozirda mavjud emas."</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Ularni yana taklif qilmoqchimisiz?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"Siz bu chatda yolg\'izsiz"</string>
|
||||
<string name="screen_room_mentions_at_room_title">"Har kim"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Yana yuboring"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Xabaringiz yuborilmadi"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Emoji qo\'shmoq"</string>
|
||||
<string name="screen_room_timeline_beginning_of_room">"Bu %1$sni boshlanishi"</string>
|
||||
<string name="screen_room_timeline_beginning_of_room_no_name">"Bu suhbatning boshlanishi."</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Kamroq ko\'rsatish"</string>
|
||||
<string name="screen_room_timeline_message_copied">"Xabar nusxalandi"</string>
|
||||
<string name="screen_room_timeline_no_permission_to_post">"Sizda bu xonaga post yozishga ruxsat yo‘q"</string>
|
||||
<string name="screen_room_timeline_reactions_show_less">"Kamroq ko\'rsatish"</string>
|
||||
<string name="screen_room_timeline_reactions_show_more">"Ko\'proq ko\'rsatish"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"Yangi"</string>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="one">"%1$dxonani almashtirish"</item>
|
||||
<item quantity="other">"%1$dxona o\'zgarishi"</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue