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:
parent
6f8de0b2c6
commit
a16dff14f8
291 changed files with 369 additions and 68 deletions
|
|
@ -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.components.retrysendmenu.RetrySendMenuPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.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.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
|
|
@ -392,6 +393,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
is TimelineItemStateContent,
|
is TimelineItemStateContent,
|
||||||
is TimelineItemEncryptedContent,
|
is TimelineItemEncryptedContent,
|
||||||
is TimelineItemLegacyCallInviteContent,
|
is TimelineItemLegacyCallInviteContent,
|
||||||
|
is TimelineItemCallNotifyContent,
|
||||||
is TimelineItemUnknownContent -> null
|
is TimelineItemUnknownContent -> null
|
||||||
}
|
}
|
||||||
val composerMode = MessageComposerMode.Reply(
|
val composerMode = MessageComposerMode.Reply(
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
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.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
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.MessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
|
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.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.CustomReactionBottomSheet
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
|
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 kotlinx.collections.immutable.ImmutableList
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import androidx.compose.material3.Button as Material3Button
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesView(
|
fun MessagesView(
|
||||||
|
|
@ -232,6 +229,7 @@ fun MessagesView(
|
||||||
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
|
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
|
||||||
},
|
},
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
|
onJoinCallClick = onJoinCallClick,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
|
|
@ -324,6 +322,7 @@ private fun MessagesViewContent(
|
||||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
onTimestampClick: (TimelineItem.Event) -> Unit,
|
||||||
onSendLocationClick: () -> Unit,
|
onSendLocationClick: () -> Unit,
|
||||||
onCreatePollClick: () -> Unit,
|
onCreatePollClick: () -> Unit,
|
||||||
|
onJoinCallClick: () -> Unit,
|
||||||
forceJumpToBottomVisibility: Boolean,
|
forceJumpToBottomVisibility: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -396,6 +395,7 @@ private fun MessagesViewContent(
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
|
onJoinCallClick = onJoinCallClick,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
sheetContent = { subcomposing: Boolean ->
|
sheetContent = { subcomposing: Boolean ->
|
||||||
|
|
@ -478,16 +478,11 @@ private fun MessagesViewTopBar(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (callState == RoomCallState.ONGOING) {
|
CallMenuItem(
|
||||||
JoinCallMenuItem(onJoinCallClick = onJoinCallClick)
|
isCallOngoing = callState == RoomCallState.ONGOING,
|
||||||
} else {
|
onClick = onJoinCallClick,
|
||||||
IconButton(onClick = onJoinCallClick, enabled = callState != RoomCallState.DISABLED) {
|
enabled = callState != RoomCallState.DISABLED
|
||||||
Icon(
|
)
|
||||||
imageVector = CompoundIcons.VideoCallSolid(),
|
|
||||||
contentDescription = stringResource(CommonStrings.a11y_start_call),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
},
|
},
|
||||||
windowInsets = WindowInsets(0.dp)
|
windowInsets = WindowInsets(0.dp)
|
||||||
|
|
@ -495,29 +490,20 @@ private fun MessagesViewTopBar(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoinCallMenuItem(
|
private fun CallMenuItem(
|
||||||
onJoinCallClick: () -> Unit,
|
isCallOngoing: Boolean,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Material3Button(
|
if (isCallOngoing) {
|
||||||
onClick = onJoinCallClick,
|
JoinCallMenuItem(onJoinCallClick = onClick)
|
||||||
colors = ButtonDefaults.buttonColors(
|
} else {
|
||||||
contentColor = ElementTheme.colors.bgCanvasDefault,
|
IconButton(onClick = onClick, enabled = enabled) {
|
||||||
containerColor = ElementTheme.colors.iconAccentTertiary
|
Icon(
|
||||||
),
|
imageVector = CompoundIcons.VideoCallSolid(),
|
||||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
|
contentDescription = stringResource(CommonStrings.a11y_start_call),
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
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.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.TimelineItemLegacyCallInviteContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
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.TimelineItemRedactedContent
|
||||||
|
|
@ -86,6 +87,13 @@ class ActionListPresenter @Inject constructor(
|
||||||
val canRedact = timelineItem.isMine && userCanRedactOwn || !timelineItem.isMine && userCanRedactOther
|
val canRedact = timelineItem.isMine && userCanRedactOwn || !timelineItem.isMine && userCanRedactOther
|
||||||
val actions =
|
val actions =
|
||||||
when (timelineItem.content) {
|
when (timelineItem.content) {
|
||||||
|
is TimelineItemCallNotifyContent -> {
|
||||||
|
if (isDeveloperModeEnabled) {
|
||||||
|
listOf(TimelineItemAction.ViewSource)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
is TimelineItemRedactedContent -> {
|
is TimelineItemRedactedContent -> {
|
||||||
if (isDeveloperModeEnabled) {
|
if (isDeveloperModeEnabled) {
|
||||||
listOf(TimelineItemAction.ViewSource)
|
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.sender.SenderNameMode
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.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.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
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 -> {
|
is TimelineItemLegacyCallInviteContent -> {
|
||||||
content = { ContentForBody(textContent) }
|
content = { ContentForBody(textContent) }
|
||||||
}
|
}
|
||||||
|
is TimelineItemCallNotifyContent -> {
|
||||||
|
content = { ContentForBody(stringResource(CommonStrings.common_call_started)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Row(modifier = modifier) {
|
Row(modifier = modifier) {
|
||||||
icon()
|
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.poll.api.actions.SendPollResponseAction
|
||||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
|
@ -85,6 +86,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
|
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
|
||||||
|
|
||||||
val timelineItems by timelineItemsFactory.collectItemsAsState()
|
val timelineItems by timelineItemsFactory.collectItemsAsState()
|
||||||
|
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
|
|
||||||
|
|
@ -196,6 +198,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
isDm = room.isDm,
|
isDm = room.isDm,
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||||
|
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,4 +51,5 @@ data class TimelineRoomInfo(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val userHasPermissionToSendMessage: Boolean,
|
val userHasPermissionToSendMessage: Boolean,
|
||||||
val userHasPermissionToSendReaction: Boolean,
|
val userHasPermissionToSendReaction: Boolean,
|
||||||
|
val isCallOngoing: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -232,4 +232,5 @@ internal fun aTimelineRoomInfo(
|
||||||
name = name,
|
name = name,
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
userHasPermissionToSendReaction = true,
|
userHasPermissionToSendReaction = true,
|
||||||
|
isCallOngoing = false,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ fun TimelineView(
|
||||||
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onJoinCallClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forceJumpToBottomVisibility: Boolean = false
|
forceJumpToBottomVisibility: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
@ -150,6 +151,7 @@ fun TimelineView(
|
||||||
onTimestampClick = onTimestampClick,
|
onTimestampClick = onTimestampClick,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
onSwipeToReply = onSwipeToReply,
|
onSwipeToReply = onSwipeToReply,
|
||||||
|
onJoinCallClick = onJoinCallClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -305,6 +307,7 @@ internal fun TimelineViewPreview(
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
onReadReceiptClick = {},
|
onReadReceiptClick = {},
|
||||||
|
onJoinCallClick = {},
|
||||||
forceJumpToBottomVisibility = true,
|
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,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onSwipeToReply = {},
|
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.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
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.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.TimelineItemLegacyCallInviteContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.libraries.designsystem.text.toPx
|
import io.element.android.libraries.designsystem.text.toPx
|
||||||
|
|
@ -55,6 +56,7 @@ internal fun TimelineItemRow(
|
||||||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
onTimestampClick: (TimelineItem.Event) -> Unit,
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||||
|
onJoinCallClick: () -> Unit,
|
||||||
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
@ -78,37 +80,49 @@ internal fun TimelineItemRow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is TimelineItem.Event -> {
|
is TimelineItem.Event -> {
|
||||||
if (timelineItem.content is TimelineItemStateContent || timelineItem.content is TimelineItemLegacyCallInviteContent) {
|
when (timelineItem.content) {
|
||||||
TimelineItemStateEventRow(
|
is TimelineItemStateContent, is TimelineItemLegacyCallInviteContent -> {
|
||||||
event = timelineItem,
|
TimelineItemStateEventRow(
|
||||||
renderReadReceipts = renderReadReceipts,
|
event = timelineItem,
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
renderReadReceipts = renderReadReceipts,
|
||||||
isHighlighted = timelineItem.isEvent(focusedEventId),
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
onClick = { onClick(timelineItem) },
|
isHighlighted = timelineItem.isEvent(focusedEventId),
|
||||||
onReadReceiptsClick = onReadReceiptClick,
|
onClick = { onClick(timelineItem) },
|
||||||
onLongClick = { onLongClick(timelineItem) },
|
onReadReceiptsClick = onReadReceiptClick,
|
||||||
eventSink = eventSink,
|
onLongClick = { onLongClick(timelineItem) },
|
||||||
)
|
eventSink = eventSink,
|
||||||
} else {
|
)
|
||||||
TimelineItemEventRow(
|
}
|
||||||
event = timelineItem,
|
is TimelineItemCallNotifyContent -> {
|
||||||
timelineRoomInfo = timelineRoomInfo,
|
TimelineItemCallNotifyView(
|
||||||
renderReadReceipts = renderReadReceipts,
|
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
event = timelineItem,
|
||||||
isHighlighted = timelineItem.isEvent(focusedEventId),
|
isCallOngoing = timelineRoomInfo.isCallOngoing,
|
||||||
onClick = { onClick(timelineItem) },
|
onLongClick = onLongClick,
|
||||||
onLongClick = { onLongClick(timelineItem) },
|
onJoinCallClick = onJoinCallClick,
|
||||||
onUserDataClick = onUserDataClick,
|
)
|
||||||
onLinkClick = onLinkClick,
|
}
|
||||||
inReplyToClick = inReplyToClick,
|
else -> {
|
||||||
onReactionClick = onReactionClick,
|
TimelineItemEventRow(
|
||||||
onReactionLongClick = onReactionLongClick,
|
event = timelineItem,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
renderReadReceipts = renderReadReceipts,
|
||||||
onTimestampClick = onTimestampClick,
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
onSwipeToReply = { onSwipeToReply(timelineItem) },
|
isHighlighted = timelineItem.isEvent(focusedEventId),
|
||||||
eventSink = eventSink,
|
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 -> {
|
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.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.rememberPresenter
|
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.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.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
|
|
@ -118,5 +119,6 @@ fun TimelineItemEventContentView(
|
||||||
modifier = modifier
|
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
|
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.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
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.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.EventTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
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.FailedToParseStateContent
|
||||||
|
|
@ -67,6 +69,7 @@ class TimelineItemContentFactory @Inject constructor(
|
||||||
is StickerContent -> stickerFactory.create(itemContent)
|
is StickerContent -> stickerFactory.create(itemContent)
|
||||||
is PollContent -> pollFactory.create(eventTimelineItem, itemContent)
|
is PollContent -> pollFactory.create(eventTimelineItem, itemContent)
|
||||||
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
||||||
|
is CallNotifyContent -> TimelineItemCallNotifyContent()
|
||||||
is UnknownContent -> TimelineItemUnknownContent
|
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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
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.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
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.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
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.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.FailedToParseMessageLikeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
|
||||||
|
|
@ -66,7 +68,8 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean {
|
||||||
is TimelineItemVoiceContent,
|
is TimelineItemVoiceContent,
|
||||||
TimelineItemRedactedContent,
|
TimelineItemRedactedContent,
|
||||||
TimelineItemUnknownContent,
|
TimelineItemUnknownContent,
|
||||||
is TimelineItemLegacyCallInviteContent -> false
|
is TimelineItemLegacyCallInviteContent,
|
||||||
|
is TimelineItemCallNotifyContent -> false
|
||||||
is TimelineItemProfileChangeContent,
|
is TimelineItemProfileChangeContent,
|
||||||
is TimelineItemRoomMembershipContent,
|
is TimelineItemRoomMembershipContent,
|
||||||
is TimelineItemStateEventContent -> true
|
is TimelineItemStateEventContent -> true
|
||||||
|
|
@ -93,6 +96,7 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
|
||||||
is RoomMembershipContent,
|
is RoomMembershipContent,
|
||||||
UnknownContent,
|
UnknownContent,
|
||||||
is LegacyCallInviteContent,
|
is LegacyCallInviteContent,
|
||||||
|
CallNotifyContent,
|
||||||
is StateContent -> false
|
is StateContent -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.res.stringResource
|
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.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.FailedToParseMessageLikeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
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,
|
is StateContent,
|
||||||
UnknownContent,
|
UnknownContent,
|
||||||
is LegacyCallInviteContent,
|
is LegacyCallInviteContent,
|
||||||
|
is CallNotifyContent,
|
||||||
null -> null
|
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) {
|
when (this) {
|
||||||
is TimelineItemRedactedContent,
|
is TimelineItemRedactedContent,
|
||||||
is TimelineItemLegacyCallInviteContent,
|
is TimelineItemLegacyCallInviteContent,
|
||||||
|
is TimelineItemCallNotifyContent,
|
||||||
is TimelineItemStateContent -> false
|
is TimelineItemStateContent -> false
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +66,7 @@ fun TimelineItemEventContent.canReact(): Boolean =
|
||||||
is TimelineItemStateContent,
|
is TimelineItemStateContent,
|
||||||
is TimelineItemRedactedContent,
|
is TimelineItemRedactedContent,
|
||||||
is TimelineItemLegacyCallInviteContent,
|
is TimelineItemLegacyCallInviteContent,
|
||||||
|
is TimelineItemCallNotifyContent,
|
||||||
TimelineItemUnknownContent -> false
|
TimelineItemUnknownContent -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
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.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
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 TimelineItemFileContent -> context.getString(CommonStrings.common_file)
|
||||||
is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio)
|
is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio)
|
||||||
is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_call_invite)
|
is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_call_invite)
|
||||||
|
is TimelineItemCallNotifyContent -> context.getString(CommonStrings.common_call_started)
|
||||||
}.take(MAX_SAFE_LENGTH)
|
}.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.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
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.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.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
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 {
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
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.EnsureNeverCalledWithParam
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
|
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
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(),
|
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
|
||||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
|
||||||
forceJumpToBottomVisibility: Boolean = false,
|
forceJumpToBottomVisibility: Boolean = false,
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
|
|
@ -127,6 +129,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
|
onJoinCallClick = onJoinCallClick,
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
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.eventformatter.impl.mode.RenderingMode
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
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.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.EmoteMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
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.FailedToParseMessageLikeContent
|
||||||
|
|
@ -111,6 +112,8 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||||
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisambiguatedDisplayName, isDmRoom)
|
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisambiguatedDisplayName, isDmRoom)
|
||||||
}
|
}
|
||||||
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_call_invite)
|
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_call_invite)
|
||||||
|
is CallNotifyContent -> sp.getString(CommonStrings.common_call_started)
|
||||||
|
else -> null
|
||||||
}?.take(MAX_SAFE_LENGTH)
|
}?.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.di.SessionScope
|
||||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
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.EventTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
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.FailedToParseStateContent
|
||||||
|
|
@ -63,6 +64,9 @@ class DefaultTimelineEventFormatter @Inject constructor(
|
||||||
is LegacyCallInviteContent -> {
|
is LegacyCallInviteContent -> {
|
||||||
sp.getString(CommonStrings.common_call_invite)
|
sp.getString(CommonStrings.common_call_invite)
|
||||||
}
|
}
|
||||||
|
is CallNotifyContent -> {
|
||||||
|
sp.getString(CommonStrings.common_call_started)
|
||||||
|
}
|
||||||
RedactedContent,
|
RedactedContent,
|
||||||
is StickerContent,
|
is StickerContent,
|
||||||
is PollContent,
|
is PollContent,
|
||||||
|
|
|
||||||
|
|
@ -103,4 +103,6 @@ data class FailedToParseStateContent(
|
||||||
|
|
||||||
data object LegacyCallInviteContent : EventContent
|
data object LegacyCallInviteContent : EventContent
|
||||||
|
|
||||||
|
data object CallNotifyContent : EventContent
|
||||||
|
|
||||||
data object UnknownContent : EventContent
|
data object UnknownContent : EventContent
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.libraries.matrix.impl.timeline.item.event
|
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.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.EventContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
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.FailedToParseStateContent
|
||||||
|
|
@ -125,6 +126,7 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is TimelineItemContentKind.CallInvite -> LegacyCallInviteContent
|
is TimelineItemContentKind.CallInvite -> LegacyCallInviteContent
|
||||||
|
is TimelineItemContentKind.CallNotify -> CallNotifyContent
|
||||||
else -> UnknownContent
|
else -> UnknownContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue