Merge branch 'main' into wallet

# Conflicts:
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt
#	libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/ResolvedSuggestion.kt
This commit is contained in:
Cobb 2026-04-16 22:05:16 -07:00
commit 0ef6b69a79
912 changed files with 17051 additions and 4425 deletions

View file

@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@ -51,6 +52,7 @@ import androidx.media3.common.Timeline
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import com.bumble.appyx.core.node.LocalNodeTargetVisibility
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.audio.api.AudioFocus
@ -130,6 +132,8 @@ private fun ExoPlayerMediaAudioView(
mutableStateOf(null)
}
val isTargetVisible = LocalNodeTargetVisibility.current
val playableState: PlayableState.Playable by remember {
derivedStateOf {
PlayableState.Playable(
@ -196,13 +200,21 @@ private fun ExoPlayerMediaAudioView(
exoPlayer.pause()
}
}
LaunchedEffect(isTargetVisible) {
if (!isTargetVisible) {
exoPlayer.pause()
}
}
if (localMedia?.uri != null) {
LaunchedEffect(localMedia.uri) {
val mediaItem = MediaItem.fromUri(localMedia.uri)
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
}
} else {
exoPlayer.setMediaItems(emptyList())
LaunchedEffect(Unit) {
exoPlayer.setMediaItems(emptyList())
}
}
val context = LocalContext.current
val waveform = info?.waveform
@ -247,7 +259,7 @@ private fun ExoPlayerMediaAudioView(
}
},
update = { playerView ->
playerView.isVisible = metadata.hasArtwork()
playerView.isVisible = metadata.hasArtwork() && isTargetVisible
},
onRelease = { playerView ->
playerView.player = null
@ -317,16 +329,19 @@ private fun ExoPlayerMediaAudioView(
)
}
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> exoPlayer.addListener(playerListener)
Lifecycle.Event.ON_RESUME -> exoPlayer.prepare()
Lifecycle.Event.ON_PAUSE -> exoPlayer.pause()
Lifecycle.Event.ON_DESTROY -> {
exoPlayer.release()
DisposableEffect(exoPlayer) {
exoPlayer.addListener(playerListener)
onDispose {
if (!exoPlayer.isReleased) {
exoPlayer.removeListener(playerListener)
exoPlayer.release()
}
else -> Unit
}
}
OnLifecycleEvent { _, event ->
if (event == Lifecycle.Event.ON_PAUSE) {
exoPlayer.pause()
}
}
}

View file

@ -23,7 +23,7 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
import io.element.android.libraries.ui.strings.CommonStrings
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
import me.saket.telephoto.zoomable.coil3.ZoomableAsyncImage
import me.saket.telephoto.zoomable.rememberZoomableImageState
@Composable

View file

@ -29,6 +29,14 @@ import io.element.android.libraries.mediaviewer.impl.details.aMediaDeleteConfirm
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
import kotlinx.collections.immutable.toImmutableList
private const val LONG_CAPTION = "This is a very long caption that should be scrollable in the media viewer. " +
"It contains multiple lines of text to demonstrate the scrolling behavior. " +
"Line 1: Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
"Line 2: Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " +
"Line 3: Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. " +
"Line 4: Duis aute irure dolor in reprehenderit in voluptate velit esse cillum. " +
"Line 5: Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia."
open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState> {
override val values: Sequence<MediaViewerState>
get() = sequenceOf(
@ -170,6 +178,22 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
)
)
),
anImageMediaInfo(
senderName = "Alice",
dateSent = "21 NOV, 2024",
caption = LONG_CAPTION,
).let {
aMediaViewerState(
listOf(
aMediaViewerPageData(
downloadedMedia = AsyncData.Success(
LocalMedia(Uri.EMPTY, it)
),
mediaInfo = it,
)
)
)
},
)
}

View file

@ -17,18 +17,24 @@ import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@ -39,14 +45,17 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@ -69,6 +78,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
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.hasCompactHeightWindowSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.media.MediaSource
@ -102,8 +112,9 @@ fun MediaViewerView(
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
var showOverlay by remember { mutableStateOf(true) }
val defaultBottomPaddingInPixels = if (LocalInspectionMode.current) 303 else 0
val currentData = state.listData.getOrNull(state.currentIndex)
val defaultBottomPaddingInPixels = if (LocalInspectionMode.current && !hasCompactHeightWindowSize()) 303 else 0
BackHandler { onBackClick() }
Scaffold(
modifier,
@ -153,10 +164,11 @@ fun MediaViewerView(
// So we need to update this value only when the `settledPage` value changes. It seems like a bug that needs to be fixed in Compose.
page == pagerState.settledPage
}
val navigationBarPadding = WindowInsets.navigationBars.getBottom(LocalDensity.current)
MediaViewerPage(
isDisplayed = isDisplayed,
showOverlay = showOverlay,
bottomPaddingInPixels = bottomPaddingInPixels,
bottomPaddingInPixels = (bottomPaddingInPixels - navigationBarPadding).coerceAtLeast(0),
data = dataForPage,
textFileViewer = textFileViewer,
onDismiss = onBackClick,
@ -175,9 +187,7 @@ fun MediaViewerView(
// Bottom bar
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding()
modifier = Modifier.fillMaxSize()
) {
MediaViewerBottomBar(
modifier = Modifier.align(Alignment.BottomCenter),
@ -538,19 +548,46 @@ private fun MediaViewerBottomBar(
if (showDivider) {
HorizontalDivider()
}
Text(
val scrollState = rememberScrollState()
val showBottomShadow by remember { derivedStateOf { scrollState.value < scrollState.maxValue } }
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = caption,
maxLines = 5,
overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodyLgRegular,
)
.heightIn(max = if (hasCompactHeightWindowSize()) maxCaptionHeightLandscape else maxCaptionHeightPortrait),
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.verticalScroll(scrollState)
.navigationBarsPadding(),
text = caption,
style = ElementTheme.typography.fontBodyLgRegular,
)
if (showBottomShadow) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.align(Alignment.BottomCenter)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
bgCanvasWithTransparency,
),
),
),
)
}
}
}
}
}
private val maxCaptionHeightPortrait = 200.dp
private val maxCaptionHeightLandscape = 128.dp
@Composable
private fun ThumbnailView(
thumbnailSource: MediaSource?,
@ -604,3 +641,14 @@ internal fun MediaViewerViewPreview(@PreviewParameter(MediaViewerStateProvider::
onBackClick = {},
)
}
@Preview(device = "${Devices.PHONE}, orientation=landscape")
@Composable
internal fun MediaViewerViewLandscapePreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = ElementPreviewDark {
MediaViewerView(
state = state,
audioFocus = null,
textFileViewer = { _, _ -> },
onBackClick = {},
)
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_media_browser_delete_confirmation_subtitle">"このファイルはルームから削除され、他のユーザーは確認することができなくなります。"</string>
<string name="screen_media_browser_delete_confirmation_title">"ファイルを削除しますか?"</string>
<string name="screen_media_browser_download_error_message">"インターネット接続を確認した上、再度お試しください。"</string>
<string name="screen_media_browser_files_empty_state_subtitle">"このルームに投稿された文書ファイルや音声ファイル・メッセージはここに表示されます。"</string>
<string name="screen_media_browser_files_empty_state_title">"アップロードされたファイルはありません"</string>
<string name="screen_media_browser_list_loading_files">"ファイルを読み込み中…"</string>
<string name="screen_media_browser_list_loading_media">"メディアを読み込み中…"</string>
<string name="screen_media_browser_list_mode_files">"ファイル"</string>
<string name="screen_media_browser_list_mode_media">"メディア"</string>
<string name="screen_media_browser_media_empty_state_subtitle">"このルームに投稿された画像と動画はここに表示されます。"</string>
<string name="screen_media_browser_media_empty_state_title">"アップロードされたメディアはありません"</string>
<string name="screen_media_browser_title">"ファイルとメディア"</string>
<string name="screen_media_details_file_format">"ファイル形式"</string>
<string name="screen_media_details_filename">"ファイル名"</string>
<string name="screen_media_details_no_more_files_to_show">"これ以上ファイルはありません"</string>
<string name="screen_media_details_no_more_media_to_show">"これ以上メディアはありません"</string>
<string name="screen_media_details_uploaded_by">"アップロード元"</string>
<string name="screen_media_details_uploaded_on">"アップロード先"</string>
</resources>