feat: implement HorizontalPager for tab navigation in OtherProfileScreen
This commit is contained in:
@@ -19,6 +19,8 @@ import androidx.compose.animation.core.animateFloatAsState
|
|||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -98,6 +100,7 @@ import compose.icons.tablericons.*
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
@@ -183,6 +186,25 @@ fun OtherProfileScreen(
|
|||||||
var showImageViewer by remember { mutableStateOf(false) }
|
var showImageViewer by remember { mutableStateOf(false) }
|
||||||
var imageViewerInitialIndex by remember { mutableIntStateOf(0) }
|
var imageViewerInitialIndex by remember { mutableIntStateOf(0) }
|
||||||
var selectedTab by remember { mutableStateOf(OtherProfileTab.MEDIA) }
|
var selectedTab by remember { mutableStateOf(OtherProfileTab.MEDIA) }
|
||||||
|
val pagerState = rememberPagerState(
|
||||||
|
initialPage = 0,
|
||||||
|
pageCount = { OtherProfileTab.entries.size }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tab click → animate pager
|
||||||
|
LaunchedEffect(selectedTab) {
|
||||||
|
val page = OtherProfileTab.entries.indexOf(selectedTab)
|
||||||
|
if (pagerState.currentPage != page) {
|
||||||
|
pagerState.animateScrollToPage(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pager swipe → update tab
|
||||||
|
LaunchedEffect(pagerState) {
|
||||||
|
snapshotFlow { pagerState.currentPage }.collect { page ->
|
||||||
|
selectedTab = OtherProfileTab.entries[page]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(showImageViewer) {
|
LaunchedEffect(showImageViewer) {
|
||||||
onSwipeBackEnabledChanged(!showImageViewer)
|
onSwipeBackEnabledChanged(!showImageViewer)
|
||||||
@@ -711,217 +733,218 @@ fun OtherProfileScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
// TAB CONTENT — inlined directly into LazyColumn items
|
// TAB CONTENT — HorizontalPager for swipe between tabs
|
||||||
// for true virtualization (only visible items compose)
|
// On first tab (Media) swipe-right triggers swipe-back;
|
||||||
|
// on other tabs swipe-right goes to the previous tab.
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
when (selectedTab) {
|
item(key = "tab_pager") {
|
||||||
OtherProfileTab.MEDIA -> {
|
HorizontalPager(
|
||||||
if (sharedContent.mediaPhotos.isEmpty()) {
|
state = pagerState,
|
||||||
item(key = "media_empty") {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
OtherProfileEmptyState(
|
verticalAlignment = Alignment.Top
|
||||||
animationAssetPath = "lottie/saved.json",
|
) { page ->
|
||||||
title = "No shared media yet",
|
when (page) {
|
||||||
subtitle = "Photos from your chat will appear here.",
|
// ── MEDIA ──
|
||||||
isDarkTheme = isDarkTheme
|
0 -> {
|
||||||
)
|
if (sharedContent.mediaPhotos.isEmpty()) {
|
||||||
}
|
OtherProfileEmptyState(
|
||||||
} else {
|
animationAssetPath = "lottie/saved.json",
|
||||||
items(
|
title = "No shared media yet",
|
||||||
items = mediaIndexedRows,
|
subtitle = "Photos from your chat will appear here.",
|
||||||
key = { (idx, _) -> "media_row_$idx" }
|
isDarkTheme = isDarkTheme
|
||||||
) { (rowIdx, rowPhotos) ->
|
)
|
||||||
Row(
|
} else {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
horizontalArrangement = Arrangement.spacedBy(mediaSpacing)
|
mediaIndexedRows.forEach { (rowIdx, rowPhotos) ->
|
||||||
) {
|
Row(
|
||||||
rowPhotos.forEachIndexed { colIdx, media ->
|
modifier = Modifier.fillMaxWidth(),
|
||||||
val globalIndex = rowIdx * mediaColumns + colIdx
|
horizontalArrangement = Arrangement.spacedBy(mediaSpacing)
|
||||||
|
) {
|
||||||
|
rowPhotos.forEachIndexed { colIdx, media ->
|
||||||
|
val globalIndex = rowIdx * mediaColumns + colIdx
|
||||||
|
|
||||||
// Check cache first
|
val cachedBitmap = mediaBitmapStates[media.key]
|
||||||
val cachedBitmap = mediaBitmapStates[media.key]
|
?: SharedMediaBitmapCache.get(media.key)
|
||||||
?: SharedMediaBitmapCache.get(media.key)
|
|
||||||
|
|
||||||
// Only launch decode for items not yet cached
|
if (cachedBitmap == null && !mediaBitmapStates.containsKey(media.key)) {
|
||||||
if (cachedBitmap == null && !mediaBitmapStates.containsKey(media.key)) {
|
LaunchedEffect(media.key) {
|
||||||
LaunchedEffect(media.key) {
|
mediaDecodeSemaphore.withPermit {
|
||||||
mediaDecodeSemaphore.withPermit {
|
val bitmap = withContext(Dispatchers.IO) {
|
||||||
val bitmap = withContext(Dispatchers.IO) {
|
resolveSharedPhotoBitmap(
|
||||||
resolveSharedPhotoBitmap(
|
context = context,
|
||||||
context = context,
|
media = media,
|
||||||
media = media,
|
accountPublicKey = activeAccountPublicKey,
|
||||||
accountPublicKey = activeAccountPublicKey,
|
accountPrivateKey = activeAccountPrivateKey
|
||||||
accountPrivateKey = activeAccountPrivateKey
|
)
|
||||||
)
|
}
|
||||||
|
mediaBitmapStates[media.key] = bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mediaBitmapStates[media.key] = bitmap
|
|
||||||
|
val resolvedBitmap = cachedBitmap ?: mediaBitmapStates[media.key]
|
||||||
|
val model = remember(media.localUri, media.blob) {
|
||||||
|
resolveSharedMediaModel(media.localUri, media.blob)
|
||||||
|
}
|
||||||
|
val previewBitmap = remember(media.preview) {
|
||||||
|
if (media.preview.isNotBlank()) {
|
||||||
|
runCatching {
|
||||||
|
val bytes = Base64.decode(media.preview, Base64.DEFAULT)
|
||||||
|
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||||
|
}.getOrNull()
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
val isLoaded = resolvedBitmap != null || model != null
|
||||||
|
val imageAlpha by animateFloatAsState(
|
||||||
|
targetValue = if (isLoaded) 1f else 0f,
|
||||||
|
animationSpec = tween(300),
|
||||||
|
label = "media_fade"
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(mediaCellSize)
|
||||||
|
.clip(RoundedCornerShape(0.dp))
|
||||||
|
.clickable(
|
||||||
|
enabled = model != null || resolvedBitmap != null || media.attachmentId.isNotBlank()
|
||||||
|
) {
|
||||||
|
imageViewerInitialIndex = globalIndex
|
||||||
|
showImageViewer = true
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (previewBitmap != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = previewBitmap.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
if (isDarkTheme) Color(0xFF1E1E1E)
|
||||||
|
else Color(0xFFECECEC)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (resolvedBitmap != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = resolvedBitmap.asImageBitmap(),
|
||||||
|
contentDescription = "Shared media",
|
||||||
|
modifier = Modifier.fillMaxSize().graphicsLayer { alpha = imageAlpha },
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else if (model != null) {
|
||||||
|
coil.compose.AsyncImage(
|
||||||
|
model = model,
|
||||||
|
contentDescription = "Shared media",
|
||||||
|
modifier = Modifier.fillMaxSize().graphicsLayer { alpha = imageAlpha },
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repeat(mediaColumns - rowPhotos.size) {
|
||||||
|
Spacer(modifier = Modifier.size(mediaCellSize))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ── FILES ──
|
||||||
|
1 -> {
|
||||||
|
if (sharedContent.files.isEmpty()) {
|
||||||
|
OtherProfileEmptyState(
|
||||||
|
animationAssetPath = "lottie/folder.json",
|
||||||
|
title = "No shared files",
|
||||||
|
subtitle = "Documents from this chat will appear here.",
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val fileTextColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
val fileSecondary = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
|
val fileDivider = if (isDarkTheme) Color(0xFF333333) else Color(0xFFD9DDE5)
|
||||||
|
|
||||||
val resolvedBitmap = cachedBitmap ?: mediaBitmapStates[media.key]
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
val model = remember(media.localUri, media.blob) {
|
sharedContent.files.forEachIndexed { index, file ->
|
||||||
resolveSharedMediaModel(media.localUri, media.blob)
|
Column {
|
||||||
}
|
Row(
|
||||||
// Decode blurred preview from base64 (small image ~4-16px)
|
|
||||||
val previewBitmap = remember(media.preview) {
|
|
||||||
if (media.preview.isNotBlank()) {
|
|
||||||
runCatching {
|
|
||||||
val bytes = Base64.decode(media.preview, Base64.DEFAULT)
|
|
||||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
||||||
}.getOrNull()
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
val isLoaded = resolvedBitmap != null || model != null
|
|
||||||
// Animate alpha for smooth fade-in
|
|
||||||
val imageAlpha by animateFloatAsState(
|
|
||||||
targetValue = if (isLoaded) 1f else 0f,
|
|
||||||
animationSpec = tween(300),
|
|
||||||
label = "media_fade"
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(mediaCellSize)
|
|
||||||
.clip(RoundedCornerShape(0.dp))
|
|
||||||
.clickable(
|
|
||||||
enabled = model != null || resolvedBitmap != null || media.attachmentId.isNotBlank()
|
|
||||||
) {
|
|
||||||
imageViewerInitialIndex = globalIndex
|
|
||||||
showImageViewer = true
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
// Blurred preview placeholder (always shown initially)
|
|
||||||
if (previewBitmap != null) {
|
|
||||||
Image(
|
|
||||||
bitmap = previewBitmap.asImageBitmap(),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Fallback shimmer if no preview
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxWidth()
|
||||||
.background(
|
.clickable {
|
||||||
if (isDarkTheme) Color(0xFF1E1E1E)
|
val opened = openSharedFile(context, file)
|
||||||
else Color(0xFFECECEC)
|
if (!opened) {
|
||||||
)
|
Toast.makeText(context, "File is not available on this device", Toast.LENGTH_SHORT).show()
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
// Full quality image fades in on top
|
.padding(horizontal = 20.dp, vertical = 12.dp),
|
||||||
if (resolvedBitmap != null) {
|
verticalAlignment = Alignment.CenterVertically
|
||||||
Image(
|
) {
|
||||||
bitmap = resolvedBitmap.asImageBitmap(),
|
Box(
|
||||||
contentDescription = "Shared media",
|
modifier = Modifier.size(36.dp).clip(CircleShape)
|
||||||
modifier = Modifier.fillMaxSize().graphicsLayer { alpha = imageAlpha },
|
.background(PrimaryBlue.copy(alpha = if (isDarkTheme) 0.25f else 0.12f)),
|
||||||
contentScale = ContentScale.Crop
|
contentAlignment = Alignment.Center
|
||||||
)
|
) {
|
||||||
} else if (model != null) {
|
Icon(painter = TelegramIcons.File, contentDescription = null, tint = PrimaryBlue, modifier = Modifier.size(18.dp))
|
||||||
coil.compose.AsyncImage(
|
}
|
||||||
model = model,
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
contentDescription = "Shared media",
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
modifier = Modifier.fillMaxSize().graphicsLayer { alpha = imageAlpha },
|
Text(text = file.fileName, color = fileTextColor, fontSize = 15.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
contentScale = ContentScale.Crop
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
)
|
Text(text = "${formatFileSize(file.sizeBytes)} • ${formatTimestamp(file.timestamp)}", color = fileSecondary, fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Icon(imageVector = TablerIcons.ChevronRight, contentDescription = null, tint = fileSecondary.copy(alpha = 0.6f), modifier = Modifier.size(16.dp))
|
||||||
|
}
|
||||||
|
if (index != sharedContent.files.lastIndex) {
|
||||||
|
Divider(color = fileDivider, thickness = 0.5.dp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fill remaining cells in last incomplete row
|
|
||||||
repeat(mediaColumns - rowPhotos.size) {
|
|
||||||
Spacer(modifier = Modifier.size(mediaCellSize))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// ── LINKS ──
|
||||||
}
|
2 -> {
|
||||||
OtherProfileTab.FILES -> {
|
if (sharedContent.links.isEmpty()) {
|
||||||
if (sharedContent.files.isEmpty()) {
|
OtherProfileEmptyState(
|
||||||
item(key = "files_empty") {
|
animationAssetPath = "lottie/earth.json",
|
||||||
OtherProfileEmptyState(
|
title = "No shared links",
|
||||||
animationAssetPath = "lottie/folder.json",
|
subtitle = "Links from your messages will appear here.",
|
||||||
title = "No shared files",
|
isDarkTheme = isDarkTheme
|
||||||
subtitle = "Documents from this chat will appear here.",
|
)
|
||||||
isDarkTheme = isDarkTheme
|
} else {
|
||||||
)
|
val linkSecondary = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
}
|
val linkDivider = if (isDarkTheme) Color(0xFF333333) else Color(0xFFD9DDE5)
|
||||||
} else {
|
|
||||||
val fileTextColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val fileSecondary = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
|
||||||
val fileDivider = if (isDarkTheme) Color(0xFF333333) else Color(0xFFD9DDE5)
|
|
||||||
|
|
||||||
itemsIndexed(sharedContent.files, key = { _, f -> f.key }) { index, file ->
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column {
|
sharedContent.links.forEachIndexed { index, link ->
|
||||||
Row(
|
Column {
|
||||||
modifier = Modifier
|
Column(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
val opened = openSharedFile(context, file)
|
.clickable {
|
||||||
if (!opened) {
|
val normalizedLink = if (link.url.startsWith("http://", ignoreCase = true) || link.url.startsWith("https://", ignoreCase = true)) link.url else "https://${link.url}"
|
||||||
Toast.makeText(context, "File is not available on this device", Toast.LENGTH_SHORT).show()
|
val opened = runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(normalizedLink))) }.isSuccess
|
||||||
}
|
if (!opened) {
|
||||||
}
|
Toast.makeText(context, "Unable to open this link", Toast.LENGTH_SHORT).show()
|
||||||
.padding(horizontal = 20.dp, vertical = 12.dp),
|
}
|
||||||
verticalAlignment = Alignment.CenterVertically
|
}
|
||||||
) {
|
.padding(horizontal = 20.dp, vertical = 12.dp)
|
||||||
Box(
|
) {
|
||||||
modifier = Modifier.size(36.dp).clip(CircleShape)
|
Text(text = link.url, color = PrimaryBlue, fontSize = 15.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis, textDecoration = TextDecoration.Underline)
|
||||||
.background(PrimaryBlue.copy(alpha = if (isDarkTheme) 0.25f else 0.12f)),
|
Spacer(modifier = Modifier.height(3.dp))
|
||||||
contentAlignment = Alignment.Center
|
Text(text = formatTimestamp(link.timestamp), color = linkSecondary, fontSize = 12.sp)
|
||||||
) {
|
}
|
||||||
Icon(painter = TelegramIcons.File, contentDescription = null, tint = PrimaryBlue, modifier = Modifier.size(18.dp))
|
if (index != sharedContent.links.lastIndex) {
|
||||||
|
Divider(color = linkDivider, thickness = 0.5.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(text = file.fileName, color = fileTextColor, fontSize = 15.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
|
||||||
Text(text = "${formatFileSize(file.sizeBytes)} • ${formatTimestamp(file.timestamp)}", color = fileSecondary, fontSize = 12.sp)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Icon(imageVector = TablerIcons.ChevronRight, contentDescription = null, tint = fileSecondary.copy(alpha = 0.6f), modifier = Modifier.size(16.dp))
|
|
||||||
}
|
|
||||||
if (index != sharedContent.files.lastIndex) {
|
|
||||||
Divider(color = fileDivider, thickness = 0.5.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OtherProfileTab.LINKS -> {
|
|
||||||
if (sharedContent.links.isEmpty()) {
|
|
||||||
item(key = "links_empty") {
|
|
||||||
OtherProfileEmptyState(
|
|
||||||
animationAssetPath = "lottie/earth.json",
|
|
||||||
title = "No shared links",
|
|
||||||
subtitle = "Links from your messages will appear here.",
|
|
||||||
isDarkTheme = isDarkTheme
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val linkSecondary = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
|
||||||
val linkDivider = if (isDarkTheme) Color(0xFF333333) else Color(0xFFD9DDE5)
|
|
||||||
|
|
||||||
itemsIndexed(sharedContent.links, key = { _, l -> l.key }) { index, link ->
|
|
||||||
Column {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
val normalizedLink = if (link.url.startsWith("http://", ignoreCase = true) || link.url.startsWith("https://", ignoreCase = true)) link.url else "https://${link.url}"
|
|
||||||
val opened = runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(normalizedLink))) }.isSuccess
|
|
||||||
if (!opened) {
|
|
||||||
Toast.makeText(context, "Unable to open this link", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 20.dp, vertical = 12.dp)
|
|
||||||
) {
|
|
||||||
Text(text = link.url, color = PrimaryBlue, fontSize = 15.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis, textDecoration = TextDecoration.Underline)
|
|
||||||
Spacer(modifier = Modifier.height(3.dp))
|
|
||||||
Text(text = formatTimestamp(link.timestamp), color = linkSecondary, fontSize = 12.sp)
|
|
||||||
}
|
|
||||||
if (index != sharedContent.links.lastIndex) {
|
|
||||||
Divider(color = linkDivider, thickness = 0.5.dp)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user