Merge branch 'develop' into feature/fga/message_queuing

This commit is contained in:
ganfra 2024-06-11 17:08:47 +02:00
commit b927daffe7
620 changed files with 6821 additions and 1244 deletions

View file

@ -16,7 +16,6 @@
package io.element.android.features.messages.impl
import android.content.Context
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -31,8 +30,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.call.CallType
import io.element.android.features.call.ui.ElementCallActivity
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.SendLocationEntryPoint
import io.element.android.features.location.api.ShowLocationEntryPoint
@ -58,7 +57,6 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.architecture.overlay.Overlay
import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
@ -78,11 +76,11 @@ import kotlinx.parcelize.Parcelize
class MessagesFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
@ApplicationContext private val context: Context,
private val matrixClient: MatrixClient,
private val sendLocationEntryPoint: SendLocationEntryPoint,
private val showLocationEntryPoint: ShowLocationEntryPoint,
private val createPollEntryPoint: CreatePollEntryPoint,
private val elementCallEntryPoint: ElementCallEntryPoint,
private val analyticsService: AnalyticsService,
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
backstack = BackStack(
@ -188,12 +186,12 @@ class MessagesFlowNode @AssistedInject constructor(
}
override fun onJoinCallClick(roomId: RoomId) {
val inputs = CallType.RoomCall(
val callType = CallType.RoomCall(
sessionId = matrixClient.sessionId,
roomId = roomId,
)
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
ElementCallActivity.start(context, inputs)
elementCallEntryPoint.startCall(callType)
}
}
val inputs = MessagesNode.Inputs(

View file

@ -48,6 +48,7 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
@ -386,6 +387,7 @@ class MessagesPresenter @AssistedInject constructor(
is TimelineItemStateContent,
is TimelineItemEncryptedContent,
is TimelineItemLegacyCallInviteContent,
is TimelineItemCallNotifyContent,
is TimelineItemUnknownContent -> null
}
val composerMode = MessageComposerMode.Reply(

View file

@ -21,7 +21,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
@ -32,10 +31,8 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@ -70,6 +67,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
import io.element.android.features.messages.impl.timeline.TimelineView
import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
@ -107,7 +105,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import timber.log.Timber
import kotlin.random.Random
import androidx.compose.material3.Button as Material3Button
@Composable
fun MessagesView(
@ -224,6 +221,7 @@ fun MessagesView(
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
},
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
onJoinCallClick = onJoinCallClick,
)
},
snackbarHost = {
@ -314,6 +312,7 @@ private fun MessagesViewContent(
onMessageLongClick: (TimelineItem.Event) -> Unit,
onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit,
forceJumpToBottomVisibility: Boolean,
modifier: Modifier = Modifier,
onSwipeToReply: (TimelineItem.Event) -> Unit,
@ -385,6 +384,7 @@ private fun MessagesViewContent(
onReadReceiptClick = onReadReceiptClick,
modifier = Modifier.padding(paddingValues),
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
onJoinCallClick = onJoinCallClick,
)
},
sheetContent = { subcomposing: Boolean ->
@ -467,16 +467,11 @@ private fun MessagesViewTopBar(
}
},
actions = {
if (callState == RoomCallState.ONGOING) {
JoinCallMenuItem(onJoinCallClick = onJoinCallClick)
} else {
IconButton(onClick = onJoinCallClick, enabled = callState != RoomCallState.DISABLED) {
Icon(
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_call),
)
}
}
CallMenuItem(
isCallOngoing = callState == RoomCallState.ONGOING,
onClick = onJoinCallClick,
enabled = callState != RoomCallState.DISABLED
)
Spacer(Modifier.width(8.dp))
},
windowInsets = WindowInsets(0.dp)
@ -484,29 +479,20 @@ private fun MessagesViewTopBar(
}
@Composable
private fun JoinCallMenuItem(
onJoinCallClick: () -> Unit,
private fun CallMenuItem(
isCallOngoing: Boolean,
enabled: Boolean = true,
onClick: () -> Unit,
) {
Material3Button(
onClick = onJoinCallClick,
colors = ButtonDefaults.buttonColors(
contentColor = ElementTheme.colors.bgCanvasDefault,
containerColor = ElementTheme.colors.iconAccentTertiary
),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
modifier = Modifier.heightIn(min = 36.dp),
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = null
)
Spacer(Modifier.width(8.dp))
Text(
text = stringResource(CommonStrings.action_join),
style = ElementTheme.typography.fontBodyMdMedium
)
Spacer(Modifier.width(8.dp))
if (isCallOngoing) {
JoinCallMenuItem(onJoinCallClick = onClick)
} else {
IconButton(onClick = onClick, enabled = enabled) {
Icon(
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_call),
)
}
}
}

View file

@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.TimelineItemLegacyCallInviteContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
@ -86,6 +87,13 @@ class ActionListPresenter @Inject constructor(
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)

View file

@ -59,6 +59,7 @@ import io.element.android.features.messages.impl.sender.SenderName
import io.element.android.features.messages.impl.sender.SenderNameMode
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
@ -265,6 +266,9 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
is TimelineItemLegacyCallInviteContent -> {
content = { ContentForBody(textContent) }
}
is TimelineItemCallNotifyContent -> {
content = { ContentForBody(stringResource(CommonStrings.common_call_started)) }
}
}
Row(modifier = modifier) {
icon()

View file

@ -38,6 +38,7 @@ import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
@ -85,6 +86,7 @@ class TimelinePresenter @AssistedInject constructor(
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
val timelineItems by timelineItemsFactory.collectItemsAsState()
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
@ -196,6 +198,7 @@ class TimelinePresenter @AssistedInject constructor(
isDm = room.isDm,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
)
}
}

View file

@ -51,4 +51,5 @@ data class TimelineRoomInfo(
val name: String?,
val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToSendReaction: Boolean,
val isCallOngoing: Boolean,
)

View file

@ -232,4 +232,5 @@ internal fun aTimelineRoomInfo(
name = name,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = true,
isCallOngoing = false,
)

View file

@ -88,6 +88,7 @@ fun TimelineView(
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false
) {
@ -148,6 +149,7 @@ fun TimelineView(
onReadReceiptClick = onReadReceiptClick,
eventSink = state.eventSink,
onSwipeToReply = onSwipeToReply,
onJoinCallClick = onJoinCallClick,
)
}
}
@ -302,6 +304,7 @@ internal fun TimelineViewPreview(
onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},
onReadReceiptClick = {},
onJoinCallClick = {},
forceJumpToBottomVisibility = true,
)
}

View file

@ -0,0 +1,61 @@
/*
* 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
*
* http://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.timeline.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun JoinCallMenuItem(
onJoinCallClick: () -> Unit,
) {
Button(
onClick = onJoinCallClick,
colors = ButtonDefaults.buttonColors(
contentColor = ElementTheme.colors.bgCanvasDefault,
containerColor = ElementTheme.colors.iconAccentTertiary
),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
modifier = Modifier.heightIn(min = 36.dp),
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = null
)
Spacer(Modifier.width(8.dp))
Text(
text = stringResource(CommonStrings.action_join),
style = ElementTheme.typography.fontBodyMdMedium
)
Spacer(Modifier.width(8.dp))
}
}

View file

@ -0,0 +1,127 @@
/*
* 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
*
* http://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.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
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.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun TimelineItemCallNotifyView(
event: TimelineItem.Event,
isCallOngoing: Boolean,
onLongClick: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(8.dp))
.combinedClickable(enabled = true, onClick = {}, onLongClick = { onLongClick(event) })
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Avatar(avatarData = event.senderAvatar)
Column(modifier = Modifier.weight(1f)) {
Text(
text = event.safeSenderName,
style = ElementTheme.typography.fontBodyLgMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(20.sp.toDp()),
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = null,
tint = ElementTheme.colors.iconSecondary,
)
Text(
text = stringResource(CommonStrings.common_call_started),
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
if (isCallOngoing) {
JoinCallMenuItem(onJoinCallClick)
} else {
Text(
text = event.sentTime,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@PreviewsDayNight
@Composable
internal fun TimelineItemCallNotifyViewPreview() {
ElementPreview {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
TimelineItemCallNotifyView(
event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()),
isCallOngoing = true,
onLongClick = {},
onJoinCallClick = {},
)
TimelineItemCallNotifyView(
event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()),
isCallOngoing = false,
onLongClick = {},
onJoinCallClick = {},
)
}
}
}

View file

@ -136,6 +136,7 @@ private fun TimelineItemGroupedEventsRowContent(
onReadReceiptClick = onReadReceiptClick,
eventSink = eventSink,
onSwipeToReply = {},
onJoinCallClick = {},
)
}
}

View file

@ -30,6 +30,7 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
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.TimelineItemLegacyCallInviteContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.libraries.designsystem.text.toPx
@ -54,6 +55,7 @@ internal fun TimelineItemRow(
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
modifier: Modifier = Modifier
) {
@ -77,36 +79,48 @@ internal fun TimelineItemRow(
)
}
is TimelineItem.Event -> {
if (timelineItem.content is TimelineItemStateContent || timelineItem.content is TimelineItemLegacyCallInviteContent) {
TimelineItemStateEventRow(
event = timelineItem,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = { onClick(timelineItem) },
onReadReceiptsClick = onReadReceiptClick,
onLongClick = { onLongClick(timelineItem) },
eventSink = eventSink,
)
} else {
TimelineItemEventRow(
event = timelineItem,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = { onClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) },
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
inReplyToClick = inReplyToClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onReadReceiptClick = onReadReceiptClick,
onSwipeToReply = { onSwipeToReply(timelineItem) },
eventSink = eventSink,
)
when (timelineItem.content) {
is TimelineItemStateContent, is TimelineItemLegacyCallInviteContent -> {
TimelineItemStateEventRow(
event = timelineItem,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = { onClick(timelineItem) },
onReadReceiptsClick = onReadReceiptClick,
onLongClick = { onLongClick(timelineItem) },
eventSink = eventSink,
)
}
is TimelineItemCallNotifyContent -> {
TimelineItemCallNotifyView(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
event = timelineItem,
isCallOngoing = timelineRoomInfo.isCallOngoing,
onLongClick = onLongClick,
onJoinCallClick = onJoinCallClick,
)
}
else -> {
TimelineItemEventRow(
event = timelineItem,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = { onClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) },
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
inReplyToClick = inReplyToClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onReadReceiptClick = onReadReceiptClick,
onSwipeToReply = { onSwipeToReply(timelineItem) },
eventSink = eventSink,
)
}
}
}
is TimelineItem.GroupedEvents -> {

View file

@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.rememberPresenter
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
@ -118,5 +119,6 @@ fun TimelineItemEventContentView(
modifier = modifier
)
}
is TimelineItemCallNotifyContent -> error("This shouldn't be rendered as the content of a bubble")
}
}

View file

@ -16,9 +16,11 @@
package io.element.android.features.messages.impl.timeline.factories.event
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.TimelineItemUnknownContent
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
@ -67,6 +69,7 @@ class TimelineItemContentFactory @Inject constructor(
is StickerContent -> stickerFactory.create(itemContent)
is PollContent -> pollFactory.create(eventTimelineItem, itemContent)
is UnableToDecryptContent -> utdFactory.create(itemContent)
is CallNotifyContent -> TimelineItemCallNotifyContent()
is UnknownContent -> TimelineItemUnknownContent
}
}

View file

@ -20,7 +20,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
import javax.inject.Inject
@ -44,7 +43,7 @@ class TimelineItemContentStickerFactory @Inject constructor(
return TimelineItemStickerContent(
body = content.body,
mediaSource = MediaSource(content.url),
mediaSource = content.source,
thumbnailSource = content.info.thumbnailSource,
mimeType = content.info.mimetype ?: MimeTypes.OctetStream,
blurhash = content.info.blurhash,

View file

@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.groups
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
@ -34,6 +35,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
@ -66,7 +68,8 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean {
is TimelineItemVoiceContent,
TimelineItemRedactedContent,
TimelineItemUnknownContent,
is TimelineItemLegacyCallInviteContent -> false
is TimelineItemLegacyCallInviteContent,
is TimelineItemCallNotifyContent -> false
is TimelineItemProfileChangeContent,
is TimelineItemRoomMembershipContent,
is TimelineItemStateEventContent -> true
@ -93,6 +96,7 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
is RoomMembershipContent,
UnknownContent,
is LegacyCallInviteContent,
CallNotifyContent,
is StateContent -> false
}
}

View file

@ -19,8 +19,8 @@ package io.element.android.features.messages.impl.timeline.model
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
@ -113,7 +113,7 @@ internal fun InReplyToDetails.Ready.metadata(): InReplyToMetadata? = when (event
}
is StickerContent -> InReplyToMetadata.Thumbnail(
AttachmentThumbnailInfo(
thumbnailSource = MediaSource(eventContent.url),
thumbnailSource = eventContent.source,
textContent = eventContent.body,
type = AttachmentThumbnailType.Image,
blurHash = eventContent.info.blurhash,
@ -134,5 +134,6 @@ internal fun InReplyToDetails.Ready.metadata(): InReplyToMetadata? = when (event
is StateContent,
UnknownContent,
is LegacyCallInviteContent,
is CallNotifyContent,
null -> null
}

View file

@ -0,0 +1,21 @@
/*
* 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
*
* http://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.timeline.model.event
class TimelineItemCallNotifyContent : TimelineItemEventContent {
override val type: String = "m.call.notify"
}

View file

@ -42,6 +42,7 @@ fun TimelineItemEventContent.canBeRepliedTo(): Boolean =
when (this) {
is TimelineItemRedactedContent,
is TimelineItemLegacyCallInviteContent,
is TimelineItemCallNotifyContent,
is TimelineItemStateContent -> false
else -> true
}
@ -65,6 +66,7 @@ fun TimelineItemEventContent.canReact(): Boolean =
is TimelineItemStateContent,
is TimelineItemRedactedContent,
is TimelineItemLegacyCallInviteContent,
is TimelineItemCallNotifyContent,
TimelineItemUnknownContent -> false
}

View file

@ -20,6 +20,7 @@ import android.content.Context
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
@ -65,6 +66,7 @@ class DefaultMessageSummaryFormatter @Inject constructor(
is TimelineItemFileContent -> context.getString(CommonStrings.common_file)
is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio)
is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_call_invite)
is TimelineItemCallNotifyContent -> context.getString(CommonStrings.common_call_started)
}.take(MAX_SAFE_LENGTH)
}
}

View file

@ -36,7 +36,7 @@
<string name="screen_room_timeline_no_permission_to_post">"У Вас няма дазволу на публікацыю ў гэтым пакоі"</string>
<string name="screen_room_timeline_reactions_show_less">"Паказаць менш"</string>
<string name="screen_room_timeline_reactions_show_more">"Паказаць больш"</string>
<string name="screen_room_timeline_read_marker_title">"Новы"</string>
<string name="screen_room_timeline_read_marker_title">"Новае"</string>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d змена ў пакоі"</item>
<item quantity="few">"%1$d змены ў пакоі"</item>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Tegevused"</string>
<string name="emoji_picker_category_flags">"Lipud"</string>
<string name="emoji_picker_category_foods">"Toit ja jook"</string>
<string name="emoji_picker_category_nature">"Loomad ja loodus"</string>
<string name="emoji_picker_category_objects">"Esemed"</string>
<string name="emoji_picker_category_people">"Emotikonid ja inimesed"</string>
<string name="emoji_picker_category_places">"Reisimine ja kohad"</string>
<string name="emoji_picker_category_symbols">"Sümbolid"</string>
<string name="screen_report_content_explanation">"Teade selle sõnumi kohta edastatakse sinu koduserveri haldajale. Haldajal ei ole võimalik lugeda krüptitud sõnumite sisu."</string>
<string name="screen_report_content_hint">"Sellest sisust teatamise põhjus"</string>
<string name="screen_room_attachment_source_camera">"Kaamera"</string>
<string name="screen_room_attachment_source_camera_photo">"Tee pilt"</string>
<string name="screen_room_attachment_source_camera_video">"Salvesta video"</string>
<string name="screen_room_attachment_source_files">"Manus"</string>
<string name="screen_room_attachment_source_gallery">"Fotode ja videote galerii"</string>
<string name="screen_room_attachment_source_location">"Asukoht"</string>
<string name="screen_room_attachment_source_poll">"Küsitlus"</string>
<string name="screen_room_attachment_text_formatting">"Tekstivorming"</string>
<string name="screen_room_encrypted_history_banner">"Sõnumite ajalugu pole hetkel saadaval"</string>
<string name="screen_room_encrypted_history_banner_unverified">"Selle jututoa sõnumite ajalugu pole hetkel saadaval. Verifitseeri see seade ja näed tervet oma sõnumiteajalugu."</string>
<string name="screen_room_mentions_at_room_subtitle">"Teavita kogu jututuba"</string>
<string name="screen_room_mentions_at_room_title">"Kõik"</string>
<string name="screen_room_timeline_beginning_of_room">"See on %1$s jututoa algus."</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"See on antud vestluse algus."</string>
<string name="screen_room_timeline_read_marker_title">"Uus"</string>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d jututoa muudatus"</item>
<item quantity="other">"%1$d jututoa muudatust"</item>
</plurals>
<plurals name="screen_room_typing_many_members">
<item quantity="one">"%1$s, %2$s ja veel %3$d huviline"</item>
<item quantity="other">"%1$s, %2$s ja veel %3$d huvilist"</item>
</plurals>
<plurals name="screen_room_typing_notification">
<item quantity="one">"%1$s kirjutab"</item>
<item quantity="other">"%1$s kirjutavad"</item>
</plurals>
</resources>

View file

@ -23,6 +23,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.fixtures.aMessageEvent
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
@ -759,6 +760,39 @@ class ActionListPresenterTest {
)
}
}
@Test
fun `present - compute for call notify`() = runTest {
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = true,
content = TimelineItemCallNotifyContent(),
)
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = true,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
displayEmojiReactions = false,
actions = persistentListOf(
TimelineItemAction.ViewSource
)
)
)
}
}
}
private fun createActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter {

View file

@ -395,7 +395,7 @@ class MessageComposerPresenterTest {
@Test
fun `present - reply message`() = runTest {
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention> ->
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention>, _: Boolean ->
Result.success(Unit)
}
val timeline = FakeTimeline().apply {
@ -426,7 +426,7 @@ class MessageComposerPresenterTest {
assert(replyMessageLambda)
.isCalledOnce()
.with(any(), value(A_REPLY), value(A_REPLY), any())
.with(any(), value(A_REPLY), value(A_REPLY), any(), value(false))
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
@ -909,7 +909,7 @@ class MessageComposerPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - send messages with intentional mentions`() = runTest {
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention> ->
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention>, _: Boolean ->
Result.success(Unit)
}
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
@ -956,7 +956,7 @@ class MessageComposerPresenterTest {
assert(replyMessageLambda)
.isCalledOnce()
.with(any(), any(), any(), value(listOf(Mention.User(A_USER_ID_2))))
.with(any(), any(), any(), value(listOf(Mention.User(A_USER_ID_2))), value(false))
// Check intentional mentions on edit message
skipItems(1)

View file

@ -173,6 +173,7 @@ class TimelineControllerTest {
matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline))
val sut = TimelineController(matrixRoom)
sut.activeTimelineFlow().test {
sut.focusOnEvent(AN_EVENT_ID)
awaitItem().also { state ->
assertThat(state).isEqualTo(liveTimeline)
}

View file

@ -29,6 +29,7 @@ import io.element.android.features.messages.impl.typing.aTypingNotificationState
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
import io.element.android.tests.testutils.EventsRecorder
@ -110,6 +111,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
forceJumpToBottomVisibility: Boolean = false,
) {
setContent {
@ -125,6 +127,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onReadReceiptClick = onReadReceiptClick,
onJoinCallClick = onJoinCallClick,
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
)
}

View file

@ -66,6 +66,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.media.aMediaSource
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
@ -706,9 +707,10 @@ class TimelineItemContentMessageFactoryTest {
return StickerContent(
body = body,
info = inImageInfo,
url = inUrl
source = aMediaSource(url = inUrl),
)
}
private fun createTimelineItemContentStickerFactory() = TimelineItemContentStickerFactory(
fileSizeFormatter = FakeFileSizeFormatter(),
fileExtensionExtractor = FileExtensionExtractorWithoutValidation()

View file

@ -115,7 +115,7 @@ class InReplyToMetadataKtTest {
eventContent = StickerContent(
body = "body",
info = anImageInfo(),
url = "url"
source = aMediaSource(url = "url")
)
).metadata()
}.test {