diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 1cc84eee78..07fc178721 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -11,12 +11,10 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background -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.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -27,30 +25,23 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter 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.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView @@ -69,7 +60,6 @@ import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBan import io.element.android.features.messages.impl.timeline.FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineView -import io.element.android.features.messages.impl.timeline.components.CallMenuItem 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 @@ -77,30 +67,23 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.topbars.MessagesViewTopBar +import io.element.android.features.messages.impl.topbars.ThreadTopBar import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView -import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.androidutils.ui.hideKeyboard import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayout import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayoutState -import io.element.android.libraries.designsystem.components.avatar.Avatar -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.components.avatar.AvatarType -import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.rememberExpandableBottomSheetLayoutState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toAnnotatedString import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle -import io.element.android.libraries.designsystem.theme.components.HorizontalDivider -import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.HideKeyboardWhenDisposed import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.OnLifecycleEvent @@ -113,14 +96,9 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.matrix.ui.components.aMatrixUserList -import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.wysiwyg.link.Link -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import timber.log.Timber import kotlin.time.Duration.Companion.milliseconds @@ -517,154 +495,6 @@ private fun MessagesViewComposerBottomSheetContents( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun MessagesViewTopBar( - roomName: String?, - roomAvatar: AvatarData, - isTombstoned: Boolean, - heroes: ImmutableList, - roomCallState: RoomCallState, - dmUserIdentityState: IdentityState?, - onRoomDetailsClick: () -> Unit, - onJoinCallClick: () -> Unit, - onBackClick: () -> Unit, -) { - TopAppBar( - navigationIcon = { - BackButton(onClick = onBackClick) - }, - title = { - val roundedCornerShape = RoundedCornerShape(8.dp) - Row( - modifier = Modifier - .clip(roundedCornerShape) - .clickable { onRoomDetailsClick() }, - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - val titleModifier = Modifier.weight(1f, fill = false) - RoomAvatarAndNameRow( - roomName = roomName, - roomAvatar = roomAvatar, - isTombstoned = isTombstoned, - heroes = heroes, - modifier = titleModifier - ) - - when (dmUserIdentityState) { - IdentityState.Verified -> { - Icon( - imageVector = CompoundIcons.Verified(), - tint = ElementTheme.colors.iconSuccessPrimary, - contentDescription = null, - ) - } - IdentityState.VerificationViolation -> { - Icon( - imageVector = CompoundIcons.ErrorSolid(), - tint = ElementTheme.colors.iconCriticalPrimary, - contentDescription = null, - ) - } - else -> Unit - } - } - }, - actions = { - CallMenuItem( - roomCallState = roomCallState, - onJoinCallClick = onJoinCallClick, - ) - Spacer(Modifier.width(8.dp)) - }, - windowInsets = WindowInsets(0.dp) - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ThreadTopBar( - roomName: String?, - roomAvatarData: AvatarData, - heroes: ImmutableList, - isTombstoned: Boolean, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { - TopAppBar( - modifier = modifier, - navigationIcon = { - BackButton(onClick = onBackClick) - }, - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - Avatar( - avatarData = roomAvatarData, - avatarType = AvatarType.Room( - heroes = heroes, - isTombstoned = isTombstoned, - ), - ) - Column( - modifier = Modifier.fillMaxWidth() - .padding(horizontal = 8.dp) - .semantics { - heading() - }, - ) { - Text( - text = stringResource(CommonStrings.common_thread), - style = ElementTheme.typography.fontBodyLgMedium, - ) - Text( - text = roomName ?: stringResource(CommonStrings.common_no_room_name), - style = ElementTheme.typography.fontBodySmRegular, - fontStyle = FontStyle.Italic.takeIf { roomName == null }, - color = ElementTheme.colors.textSecondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } - ) -} - -@Composable -private fun RoomAvatarAndNameRow( - roomName: String?, - roomAvatar: AvatarData, - heroes: ImmutableList, - isTombstoned: Boolean, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Avatar( - avatarData = roomAvatar, - avatarType = AvatarType.Room( - heroes = heroes, - isTombstoned = isTombstoned, - ), - ) - Text( - modifier = Modifier - .padding(horizontal = 8.dp) - .semantics { - heading() - }, - text = roomName ?: stringResource(CommonStrings.common_no_room_name), - style = ElementTheme.typography.fontBodyLgMedium, - fontStyle = FontStyle.Italic.takeIf { roomName == null }, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } -} - @Composable private fun CantSendMessageBanner() { Row( @@ -719,58 +549,3 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) knockRequestsBannerView = {}, ) } - -@PreviewsDayNight -@Composable -internal fun ThreadTopBarPreview() { - ElementPreview { - val name = "Room name" - val initialsAvatarData = AvatarData( - id = "id", - name = name, - url = null, - size = AvatarSize.TimelineRoom, - ) - Column { - ThreadTopBar( - roomName = name, - roomAvatarData = initialsAvatarData, - heroes = persistentListOf(), - isTombstoned = false, - onBackClick = {}, - ) - HorizontalDivider() - ThreadTopBar( - roomName = name, - roomAvatarData = initialsAvatarData, - heroes = aMatrixUserList().map { it.getAvatarData(AvatarSize.TimelineRoom) }.toImmutableList(), - isTombstoned = false, - onBackClick = {}, - ) - HorizontalDivider() - ThreadTopBar( - roomName = null, - roomAvatarData = initialsAvatarData, - heroes = persistentListOf(), - isTombstoned = false, - onBackClick = {}, - ) - HorizontalDivider() - ThreadTopBar( - roomName = name, - roomAvatarData = initialsAvatarData.copy(url = "https://some-avatar.jpg"), - heroes = persistentListOf(), - isTombstoned = false, - onBackClick = {}, - ) - HorizontalDivider() - ThreadTopBar( - roomName = name, - roomAvatarData = initialsAvatarData, - heroes = persistentListOf(), - isTombstoned = true, - onBackClick = {}, - ) - } - } -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt new file mode 100644 index 0000000000..a65f732576 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.topbars + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextOverflow +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.features.messages.impl.timeline.components.CallMenuItem +import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun MessagesViewTopBar( + roomName: String?, + roomAvatar: AvatarData, + isTombstoned: Boolean, + heroes: ImmutableList, + roomCallState: RoomCallState, + dmUserIdentityState: IdentityState?, + onRoomDetailsClick: () -> Unit, + onJoinCallClick: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + navigationIcon = { + BackButton(onClick = onBackClick) + }, + title = { + val roundedCornerShape = RoundedCornerShape(8.dp) + Row( + modifier = Modifier + .clip(roundedCornerShape) + .clickable { onRoomDetailsClick() }, + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val titleModifier = Modifier.weight(1f, fill = false) + RoomAvatarAndNameRow( + roomName = roomName, + roomAvatar = roomAvatar, + isTombstoned = isTombstoned, + heroes = heroes, + modifier = titleModifier + ) + + when (dmUserIdentityState) { + IdentityState.Verified -> { + Icon( + imageVector = CompoundIcons.Verified(), + tint = ElementTheme.colors.iconSuccessPrimary, + contentDescription = null, + ) + } + IdentityState.VerificationViolation -> { + Icon( + imageVector = CompoundIcons.ErrorSolid(), + tint = ElementTheme.colors.iconCriticalPrimary, + contentDescription = null, + ) + } + else -> Unit + } + } + }, + actions = { + CallMenuItem( + roomCallState = roomCallState, + onJoinCallClick = onJoinCallClick, + ) + Spacer(Modifier.width(8.dp)) + }, + windowInsets = WindowInsets(0.dp) + ) +} + +@Composable +private fun RoomAvatarAndNameRow( + roomName: String?, + roomAvatar: AvatarData, + heroes: ImmutableList, + isTombstoned: Boolean, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Avatar( + avatarData = roomAvatar, + avatarType = AvatarType.Room( + heroes = heroes, + isTombstoned = isTombstoned, + ), + ) + Text( + modifier = Modifier + .padding(horizontal = 8.dp) + .semantics { + heading() + }, + text = roomName ?: stringResource(CommonStrings.common_no_room_name), + style = ElementTheme.typography.fontBodyLgMedium, + fontStyle = FontStyle.Italic.takeIf { roomName == null }, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/ThreadTopBar.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/ThreadTopBar.kt new file mode 100644 index 0000000000..b2c8f9758d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/ThreadTopBar.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.topbars + +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.material3.ExperimentalMaterial3Api +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.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun ThreadTopBar( + roomName: String?, + roomAvatarData: AvatarData, + heroes: ImmutableList, + isTombstoned: Boolean, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + navigationIcon = { + BackButton(onClick = onBackClick) + }, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Avatar( + avatarData = roomAvatarData, + avatarType = AvatarType.Room( + heroes = heroes, + isTombstoned = isTombstoned, + ), + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .semantics { + heading() + }, + ) { + Text( + text = stringResource(CommonStrings.common_thread), + style = ElementTheme.typography.fontBodyLgMedium, + ) + Text( + text = roomName ?: stringResource(CommonStrings.common_no_room_name), + style = ElementTheme.typography.fontBodySmRegular, + fontStyle = FontStyle.Italic.takeIf { roomName == null }, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + ) +} + +@PreviewsDayNight +@Composable +internal fun ThreadTopBarPreview() = ElementPreview { + val name = "Room name" + val initialsAvatarData = AvatarData( + id = "id", + name = name, + url = null, + size = AvatarSize.TimelineRoom, + ) + Column { + ThreadTopBar( + roomName = name, + roomAvatarData = initialsAvatarData, + heroes = persistentListOf(), + isTombstoned = false, + onBackClick = {}, + ) + HorizontalDivider() + ThreadTopBar( + roomName = name, + roomAvatarData = initialsAvatarData, + heroes = aMatrixUserList().map { it.getAvatarData(AvatarSize.TimelineRoom) }.toImmutableList(), + isTombstoned = false, + onBackClick = {}, + ) + HorizontalDivider() + ThreadTopBar( + roomName = null, + roomAvatarData = initialsAvatarData, + heroes = persistentListOf(), + isTombstoned = false, + onBackClick = {}, + ) + HorizontalDivider() + ThreadTopBar( + roomName = name, + roomAvatarData = initialsAvatarData.copy(url = "https://some-avatar.jpg"), + heroes = persistentListOf(), + isTombstoned = false, + onBackClick = {}, + ) + HorizontalDivider() + ThreadTopBar( + roomName = name, + roomAvatarData = initialsAvatarData, + heroes = persistentListOf(), + isTombstoned = true, + onBackClick = {}, + ) + } +}