Add timeline item for m.call.notify events (#2986)
* Add timeline item for `m.call.notify` events * Update screenshots --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
committed by
GitHub
parent
6f8de0b2c6
commit
a16dff14f8
@@ -49,6 +49,7 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot
|
||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
||||
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
|
||||
@@ -392,6 +393,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
is TimelineItemStateContent,
|
||||
is TimelineItemEncryptedContent,
|
||||
is TimelineItemLegacyCallInviteContent,
|
||||
is TimelineItemCallNotifyContent,
|
||||
is TimelineItemUnknownContent -> null
|
||||
}
|
||||
val composerMode = MessageComposerMode.Reply(
|
||||
|
||||
@@ -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
|
||||
@@ -110,7 +108,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(
|
||||
@@ -232,6 +229,7 @@ fun MessagesView(
|
||||
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
|
||||
},
|
||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
@@ -324,6 +322,7 @@ private fun MessagesViewContent(
|
||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
||||
onSendLocationClick: () -> Unit,
|
||||
onCreatePollClick: () -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
forceJumpToBottomVisibility: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||
@@ -396,6 +395,7 @@ private fun MessagesViewContent(
|
||||
onReadReceiptClick = onReadReceiptClick,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
)
|
||||
},
|
||||
sheetContent = { subcomposing: Boolean ->
|
||||
@@ -478,16 +478,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)
|
||||
@@ -495,29 +490,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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,4 +51,5 @@ data class TimelineRoomInfo(
|
||||
val name: String?,
|
||||
val userHasPermissionToSendMessage: Boolean,
|
||||
val userHasPermissionToSendReaction: Boolean,
|
||||
val isCallOngoing: Boolean,
|
||||
)
|
||||
|
||||
@@ -232,4 +232,5 @@ internal fun aTimelineRoomInfo(
|
||||
name = name,
|
||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
userHasPermissionToSendReaction = true,
|
||||
isCallOngoing = false,
|
||||
)
|
||||
|
||||
@@ -89,6 +89,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
|
||||
) {
|
||||
@@ -150,6 +151,7 @@ fun TimelineView(
|
||||
onTimestampClick = onTimestampClick,
|
||||
eventSink = state.eventSink,
|
||||
onSwipeToReply = onSwipeToReply,
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -305,6 +307,7 @@ internal fun TimelineViewPreview(
|
||||
onReactionLongClick = { _, _ -> },
|
||||
onMoreReactionsClick = {},
|
||||
onReadReceiptClick = {},
|
||||
onJoinCallClick = {},
|
||||
forceJumpToBottomVisibility = true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,7 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||
onReadReceiptClick = onReadReceiptClick,
|
||||
eventSink = eventSink,
|
||||
onSwipeToReply = {},
|
||||
onJoinCallClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -55,6 +56,7 @@ internal fun TimelineItemRow(
|
||||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -78,37 +80,49 @@ 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,
|
||||
onTimestampClick = onTimestampClick,
|
||||
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,
|
||||
onTimestampClick = onTimestampClick,
|
||||
onSwipeToReply = { onSwipeToReply(timelineItem) },
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TimelineItem.GroupedEvents -> {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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
|
||||
@@ -133,5 +134,6 @@ internal fun InReplyToDetails.Ready.metadata(): InReplyToMetadata? = when (event
|
||||
is StateContent,
|
||||
UnknownContent,
|
||||
is LegacyCallInviteContent,
|
||||
is CallNotifyContent,
|
||||
null -> null
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -111,6 +112,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 {
|
||||
@@ -127,6 +129,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
|
||||
onReactionLongClick = onReactionLongClick,
|
||||
onMoreReactionsClick = onMoreReactionsClick,
|
||||
onReadReceiptClick = onReadReceiptClick,
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
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.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
@@ -111,6 +112,8 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisambiguatedDisplayName, isDmRoom)
|
||||
}
|
||||
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_call_invite)
|
||||
is CallNotifyContent -> sp.getString(CommonStrings.common_call_started)
|
||||
else -> null
|
||||
}?.take(MAX_SAFE_LENGTH)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
||||
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
|
||||
@@ -63,6 +64,9 @@ class DefaultTimelineEventFormatter @Inject constructor(
|
||||
is LegacyCallInviteContent -> {
|
||||
sp.getString(CommonStrings.common_call_invite)
|
||||
}
|
||||
is CallNotifyContent -> {
|
||||
sp.getString(CommonStrings.common_call_started)
|
||||
}
|
||||
RedactedContent,
|
||||
is StickerContent,
|
||||
is PollContent,
|
||||
|
||||
@@ -103,4 +103,6 @@ data class FailedToParseStateContent(
|
||||
|
||||
data object LegacyCallInviteContent : EventContent
|
||||
|
||||
data object CallNotifyContent : EventContent
|
||||
|
||||
data object UnknownContent : EventContent
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.libraries.matrix.impl.timeline.item.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
@@ -125,6 +126,7 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
|
||||
)
|
||||
}
|
||||
is TimelineItemContentKind.CallInvite -> LegacyCallInviteContent
|
||||
is TimelineItemContentKind.CallNotify -> CallNotifyContent
|
||||
else -> UnknownContent
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user