session-android/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt

403 lines
14 KiB
Kotlin
Raw Normal View History

2021-07-13 08:22:10 +02:00
package org.thoughtcrime.securesms.conversation.v2
2023-07-06 07:50:45 +02:00
import android.annotation.SuppressLint
2023-06-30 02:18:48 +02:00
import android.content.Intent
2021-07-13 08:22:10 +02:00
import android.os.Bundle
2023-07-04 14:40:48 +02:00
import android.view.LayoutInflater
2023-07-06 07:50:45 +02:00
import android.view.MotionEvent.ACTION_UP
2023-07-04 14:40:48 +02:00
import androidx.activity.viewModels
2023-06-30 15:19:23 +02:00
import androidx.compose.foundation.ExperimentalFoundationApi
2023-07-04 02:01:14 +02:00
import androidx.compose.foundation.clickable
2023-06-29 04:07:55 +02:00
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
2023-07-10 05:31:46 +02:00
import androidx.compose.foundation.layout.BoxWithConstraints
2023-06-29 04:07:55 +02:00
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
2023-07-09 16:42:42 +02:00
import androidx.compose.foundation.layout.IntrinsicSize
2023-06-29 04:07:55 +02:00
import androidx.compose.foundation.layout.Row
2023-07-03 03:05:50 +02:00
import androidx.compose.foundation.layout.aspectRatio
2023-07-10 05:31:46 +02:00
import androidx.compose.foundation.layout.fillMaxWidth
2023-06-29 04:07:55 +02:00
import androidx.compose.foundation.layout.padding
2023-07-09 16:42:42 +02:00
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
2023-06-30 15:19:23 +02:00
import androidx.compose.foundation.pager.HorizontalPager
2023-07-03 09:49:33 +02:00
import androidx.compose.foundation.pager.PagerState
2023-07-03 03:05:50 +02:00
import androidx.compose.foundation.pager.rememberPagerState
2023-06-29 04:07:55 +02:00
import androidx.compose.foundation.rememberScrollState
2023-07-03 13:55:59 +02:00
import androidx.compose.foundation.shape.CircleShape
2023-06-29 04:07:55 +02:00
import androidx.compose.foundation.verticalScroll
2023-07-03 13:55:59 +02:00
import androidx.compose.material.Icon
2023-06-29 11:44:47 +02:00
import androidx.compose.material.LocalTextStyle
2023-07-03 13:55:59 +02:00
import androidx.compose.material.Surface
2023-06-29 04:07:55 +02:00
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
2023-06-29 14:41:15 +02:00
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
2023-06-29 04:07:55 +02:00
import androidx.compose.ui.Modifier
2023-07-09 16:42:42 +02:00
import androidx.compose.ui.graphics.Color
2023-07-01 10:08:58 +02:00
import androidx.compose.ui.layout.ContentScale
2023-06-29 04:07:55 +02:00
import androidx.compose.ui.platform.ComposeView
2023-07-03 13:55:59 +02:00
import androidx.compose.ui.res.painterResource
2023-07-09 16:42:42 +02:00
import androidx.compose.ui.res.stringResource
2023-06-29 11:44:47 +02:00
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
2023-06-29 04:07:55 +02:00
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
2023-07-09 17:35:09 +02:00
import androidx.compose.ui.tooling.preview.PreviewParameter
2023-06-29 04:07:55 +02:00
import androidx.compose.ui.unit.dp
2023-06-30 03:31:57 +02:00
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.lifecycleScope
2023-07-01 10:08:58 +02:00
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
2021-07-13 08:22:10 +02:00
import network.loki.messenger.R
2023-07-04 14:40:48 +02:00
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import org.session.libsession.database.StorageProtocol
import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
2021-07-13 08:22:10 +02:00
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.database.Storage
2023-06-29 09:41:11 +02:00
import org.thoughtcrime.securesms.ui.AppTheme
2023-07-06 07:50:45 +02:00
import org.thoughtcrime.securesms.ui.Avatar
2023-07-03 11:00:15 +02:00
import org.thoughtcrime.securesms.ui.CarouselNextButton
import org.thoughtcrime.securesms.ui.CarouselPrevButton
2023-06-29 09:41:11 +02:00
import org.thoughtcrime.securesms.ui.Cell
2023-07-03 05:30:11 +02:00
import org.thoughtcrime.securesms.ui.CellNoMargin
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
2023-07-04 04:04:20 +02:00
import org.thoughtcrime.securesms.ui.Divider
2023-07-10 08:39:38 +02:00
import org.thoughtcrime.securesms.ui.GetString
2023-07-04 06:39:40 +02:00
import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator
2023-06-29 09:41:11 +02:00
import org.thoughtcrime.securesms.ui.ItemButton
2023-07-09 17:35:09 +02:00
import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
2023-07-10 08:39:38 +02:00
import org.thoughtcrime.securesms.ui.TitledText
2023-07-04 06:39:40 +02:00
import org.thoughtcrime.securesms.ui.blackAlpha40
2023-06-30 05:55:16 +02:00
import org.thoughtcrime.securesms.ui.colorDestructive
2023-06-29 09:41:11 +02:00
import org.thoughtcrime.securesms.ui.destructiveButtonColors
import javax.inject.Inject
2021-07-13 08:22:10 +02:00
@AndroidEntryPoint
2023-07-03 09:49:33 +02:00
class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
2021-07-13 08:22:10 +02:00
@Inject
lateinit var storage: StorageProtocol
2023-07-04 14:40:48 +02:00
private val viewModel: MessageDetailsViewModel by viewModels()
2021-07-13 08:22:10 +02:00
companion object {
// Extras
const val MESSAGE_TIMESTAMP = "message_timestamp"
2023-06-30 02:18:48 +02:00
const val ON_REPLY = 1
const val ON_RESEND = 2
const val ON_DELETE = 3
2021-07-13 08:22:10 +02:00
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
2023-06-29 04:07:55 +02:00
2021-07-13 08:22:10 +02:00
title = resources.getString(R.string.conversation_context__menu_message_details)
2023-06-30 05:55:16 +02:00
2023-07-21 04:20:42 +02:00
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
2023-06-29 04:07:55 +02:00
2023-07-04 07:16:56 +02:00
ComposeView(this)
.apply { setContent { MessageDetailsScreen() } }
.let(::setContentView)
lifecycleScope.launch {
viewModel.eventFlow.collect {
when (it) {
Event.Finish -> finish()
is Event.StartMediaPreview -> startActivity(
getPreviewIntent(this@MessageDetailActivity, it.args)
)
}
}
Paged conversation recycler, update compile sdk version 31 (#1049) * Update build tools * Update appcompat version * Update dependencies * feat: add paging into conversation recycler and queries to fetch data off-thread * refactor: wip for updating paged results and bucketing messages / fetching enough to display * fix: currently works for scrolling and possibly refreshing? need scroll to message and auto scroll down on insert (at bottom) * fix: search and scrolling to X message works now * build: increase version code and name * fix: re-add refresh, remove the outdated comment * refactor: lets see if 25 size pages increases performance :eyes: * feat: add in some equals overrides for mms records to refresh if media has finished DLing * feat: add scroll to bottom for new messages if we are at the end of the chat * build: update build numbers * fix: update AGP and fix compile errors for sdk version 31 * feat: add log for loki-avatar and loki-fs on upload types and responses * feat: increase build number to match latest installed version * feat: changing props and permission checks for call service * fix: possible service exception when no call ID remote foreground service not terminated * revert: google services version * fix: re-add paging dependency * feat: adding new last seen function and figuring out the last seen for recycler adapter * build: update version names and codes for deploy * refactor: undo the new adapter and query changes to use previous cursor logic. revert this commit to enable new paged adapter * fix: use author's address in typist equality and hashcode for set inclusion * refactor: refactor the select contacts activity * refactor: refactor the select contacts activity * build: update version code * fix: hide all other bound views if deleted * refactor: change voice message tint, upgrade build number * fix: message detail showing up properly * revert: realise copy public key is actually not allowed if open group participant * fix: copy session ID, message detail activity support re-enabled * build: update build version code * build: remove version name * build: update build code * feat: google services version minimum compatible * fix: selection for re-created objects not properly highlighting * fix: foreground CENTER_INSIDE instead of just CENTER for scaletype * build: update version code * fix: don't show error if no error * build: update version code * fix: clear error messages if any on successful send Co-authored-by: charles <charles@oxen.io>
2022-12-19 01:29:05 +01:00
}
2023-06-29 04:07:55 +02:00
}
2023-06-29 14:41:15 +02:00
@Composable
2023-06-30 02:18:48 +02:00
private fun MessageDetailsScreen() {
val state by viewModel.stateFlow.collectAsState()
2023-07-09 16:42:42 +02:00
AppTheme {
MessageDetails(
2023-07-10 05:31:46 +02:00
state = state,
2023-07-09 16:42:42 +02:00
onReply = { setResultAndFinish(ON_REPLY) },
2023-07-10 05:31:46 +02:00
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
2023-07-09 16:42:42 +02:00
onDelete = { setResultAndFinish(ON_DELETE) },
onClickImage = { viewModel.onClickImage(it) },
onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
2023-07-09 16:42:42 +02:00
)
}
2023-06-30 02:18:48 +02:00
}
private fun setResultAndFinish(code: Int) {
2023-07-21 04:20:42 +02:00
Bundle().apply { putLong(MESSAGE_TIMESTAMP, viewModel.timestamp) }
2023-06-30 02:18:48 +02:00
.let(Intent()::putExtras)
2023-06-30 15:19:23 +02:00
.let { setResult(code, it) }
2023-06-30 02:18:48 +02:00
finish()
2023-06-29 04:07:55 +02:00
}
2023-07-09 16:42:42 +02:00
}
2023-06-29 04:07:55 +02:00
2023-07-09 16:42:42 +02:00
@SuppressLint("ClickableViewAccessibility")
@Composable
fun MessageDetails(
2023-07-10 05:31:46 +02:00
state: MessageDetailsState,
2023-07-09 16:42:42 +02:00
onReply: () -> Unit = {},
onResend: (() -> Unit)? = null,
onDelete: () -> Unit = {},
2023-07-10 05:31:46 +02:00
onClickImage: (Int) -> Unit = {},
2023-07-09 16:42:42 +02:00
onAttachmentNeedsDownload: (Long, Long) -> Unit = { _, _ -> }
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
2023-06-30 02:18:48 +02:00
) {
2023-07-10 05:31:46 +02:00
state.record?.let { message ->
2023-07-09 16:42:42 +02:00
AndroidView(
modifier = Modifier.padding(horizontal = 32.dp),
factory = {
ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply {
bind(
message,
2023-07-10 05:31:46 +02:00
thread = state.thread!!,
2023-07-09 16:42:42 +02:00
onAttachmentNeedsDownload = onAttachmentNeedsDownload,
suppressThumbnails = true
)
setOnTouchListener { _, event ->
if (event.actionMasked == ACTION_UP) onContentClick(event)
true
2023-07-04 14:40:48 +02:00
}
2023-07-09 16:42:42 +02:00
}
2023-07-03 13:55:59 +02:00
}
2023-07-09 16:42:42 +02:00
)
}
2023-07-10 05:31:46 +02:00
Carousel(state.imageAttachments) { onClickImage(it) }
2023-07-10 08:39:38 +02:00
state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
CellMetadata(state)
CellButtons(
2023-07-09 16:42:42 +02:00
onReply,
onResend,
onDelete,
)
2023-07-03 13:55:59 +02:00
}
2023-07-09 16:42:42 +02:00
}
2023-07-03 13:55:59 +02:00
2023-07-09 16:42:42 +02:00
@Composable
2023-07-10 08:39:38 +02:00
fun CellMetadata(
2023-07-10 05:31:46 +02:00
state: MessageDetailsState,
2023-07-09 16:42:42 +02:00
) {
2023-07-10 05:31:46 +02:00
state.apply {
2023-07-10 08:39:38 +02:00
if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return
CellWithPaddingAndMargin {
2023-07-09 16:42:42 +02:00
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
2023-07-10 08:39:38 +02:00
TitledText(sent)
TitledText(received)
TitledErrorText(error)
2023-07-09 16:42:42 +02:00
senderInfo?.let {
2023-07-10 08:39:38 +02:00
TitledView(state.fromTitle) {
2023-07-09 16:42:42 +02:00
Row {
sender?.let { Avatar(it) }
TitledMonospaceText(it)
2023-07-04 07:16:56 +02:00
}
}
}
}
2021-07-13 08:22:10 +02:00
}
}
2023-07-09 16:42:42 +02:00
}
2021-07-13 08:22:10 +02:00
2023-07-09 16:42:42 +02:00
@Composable
2023-07-10 08:39:38 +02:00
fun CellButtons(
2023-07-09 16:42:42 +02:00
onReply: () -> Unit = {},
onResend: (() -> Unit)? = null,
onDelete: () -> Unit = {},
) {
Cell {
Column {
ItemButton(
2023-07-10 08:39:38 +02:00
stringResource(R.string.reply),
2023-07-09 16:42:42 +02:00
R.drawable.ic_message_details__reply,
onClick = onReply
)
Divider()
onResend?.let {
2023-07-03 13:55:59 +02:00
ItemButton(
2023-07-10 08:39:38 +02:00
stringResource(R.string.resend),
2023-07-09 16:42:42 +02:00
R.drawable.ic_message_details__refresh,
onClick = it
2023-07-03 13:55:59 +02:00
)
Divider()
2023-06-29 04:07:55 +02:00
}
2023-07-09 16:42:42 +02:00
ItemButton(
2023-07-10 08:39:38 +02:00
stringResource(R.string.delete),
2023-07-09 16:42:42 +02:00
R.drawable.ic_message_details__trash,
colors = destructiveButtonColors(),
onClick = onDelete
)
Paged conversation recycler, update compile sdk version 31 (#1049) * Update build tools * Update appcompat version * Update dependencies * feat: add paging into conversation recycler and queries to fetch data off-thread * refactor: wip for updating paged results and bucketing messages / fetching enough to display * fix: currently works for scrolling and possibly refreshing? need scroll to message and auto scroll down on insert (at bottom) * fix: search and scrolling to X message works now * build: increase version code and name * fix: re-add refresh, remove the outdated comment * refactor: lets see if 25 size pages increases performance :eyes: * feat: add in some equals overrides for mms records to refresh if media has finished DLing * feat: add scroll to bottom for new messages if we are at the end of the chat * build: update build numbers * fix: update AGP and fix compile errors for sdk version 31 * feat: add log for loki-avatar and loki-fs on upload types and responses * feat: increase build number to match latest installed version * feat: changing props and permission checks for call service * fix: possible service exception when no call ID remote foreground service not terminated * revert: google services version * fix: re-add paging dependency * feat: adding new last seen function and figuring out the last seen for recycler adapter * build: update version names and codes for deploy * refactor: undo the new adapter and query changes to use previous cursor logic. revert this commit to enable new paged adapter * fix: use author's address in typist equality and hashcode for set inclusion * refactor: refactor the select contacts activity * refactor: refactor the select contacts activity * build: update version code * fix: hide all other bound views if deleted * refactor: change voice message tint, upgrade build number * fix: message detail showing up properly * revert: realise copy public key is actually not allowed if open group participant * fix: copy session ID, message detail activity support re-enabled * build: update build version code * build: remove version name * build: update build code * feat: google services version minimum compatible * fix: selection for re-created objects not properly highlighting * fix: foreground CENTER_INSIDE instead of just CENTER for scaletype * build: update version code * fix: don't show error if no error * build: update version code * fix: clear error messages if any on successful send Co-authored-by: charles <charles@oxen.io>
2022-12-19 01:29:05 +01:00
}
2021-07-13 08:22:10 +02:00
}
2023-07-09 16:42:42 +02:00
}
2021-07-13 08:22:10 +02:00
2023-07-09 16:42:42 +02:00
@OptIn(ExperimentalFoundationApi::class)
@Composable
2023-07-10 05:31:46 +02:00
fun Carousel(attachments: List<Attachment>, onClick: (Int) -> Unit) {
if (attachments.isEmpty()) return
2021-07-13 08:22:10 +02:00
2023-07-10 05:31:46 +02:00
val pagerState = rememberPagerState { attachments.size }
2023-07-09 16:42:42 +02:00
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Row {
CarouselPrevButton(pagerState)
Box(modifier = Modifier.weight(1f)) {
2023-07-10 05:31:46 +02:00
CellCarousel(pagerState, attachments, onClick)
2023-07-09 16:42:42 +02:00
HorizontalPagerIndicator(pagerState)
ExpandButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(8.dp)
2023-07-10 05:31:46 +02:00
) { onClick(pagerState.currentPage) }
2023-07-09 16:42:42 +02:00
}
CarouselNextButton(pagerState)
2021-07-13 08:22:10 +02:00
}
2023-07-10 05:31:46 +02:00
attachments.getOrNull(pagerState.currentPage)?.fileDetails?.let { FileDetails(it) }
2021-07-13 08:22:10 +02:00
}
2023-07-09 16:42:42 +02:00
}
2023-07-03 03:05:50 +02:00
2023-07-09 16:42:42 +02:00
@OptIn(
ExperimentalFoundationApi::class,
ExperimentalGlideComposeApi::class
)
@Composable
private fun CellCarousel(
pagerState: PagerState,
2023-07-10 05:31:46 +02:00
attachments: List<Attachment>,
onClick: (Int) -> Unit
2023-07-09 16:42:42 +02:00
) {
CellNoMargin {
HorizontalPager(state = pagerState) { i ->
GlideImage(
contentScale = ContentScale.Crop,
modifier = Modifier
.aspectRatio(1f)
2023-07-10 05:31:46 +02:00
.clickable { onClick(i) },
model = attachments[i].uri,
contentDescription = attachments[i].fileName ?: stringResource(id = R.string.image)
2023-07-09 16:42:42 +02:00
)
2023-07-03 09:49:33 +02:00
}
}
2023-07-09 16:42:42 +02:00
}
2023-07-03 09:49:33 +02:00
2023-07-09 16:42:42 +02:00
@Composable
fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
Surface(
shape = CircleShape,
color = blackAlpha40,
modifier = modifier,
contentColor = Color.White,
2023-07-04 14:40:48 +02:00
) {
2023-07-09 16:42:42 +02:00
Icon(
painter = painterResource(id = R.drawable.ic_expand),
2023-07-10 05:31:46 +02:00
contentDescription = stringResource(id = R.string.expand),
2023-07-09 16:42:42 +02:00
modifier = Modifier.clickable { onClick() },
)
2023-07-04 06:39:40 +02:00
}
2023-07-09 16:42:42 +02:00
}
2023-07-03 09:49:33 +02:00
2023-07-09 17:35:09 +02:00
@Preview
@Composable
fun PreviewMessageDetails(
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
) {
PreviewTheme(themeResId) {
2023-07-10 08:39:38 +02:00
MessageDetails(
state = MessageDetailsState(
nonImageAttachmentFileDetails = listOf(
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
TitledText(R.string.message_details_header__file_type, "image/png"),
TitledText(R.string.message_details_header__file_size, "195.6kB"),
TitledText(R.string.message_details_header__resolution, "342x312"),
),
sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"),
received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"),
error = TitledText(R.string.message_details_header__error, "Message failed to send"),
senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
)
)
2023-07-09 17:35:09 +02:00
}
}
2023-07-09 16:42:42 +02:00
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun FileDetails(fileDetails: List<TitledText>) {
if (fileDetails.isEmpty()) return
2023-07-10 18:00:03 +02:00
CellWithPaddingAndMargin(padding = 0.dp) {
FlowRow(
modifier = Modifier.padding(vertical = 24.dp, horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
2023-07-09 16:42:42 +02:00
fileDetails.forEach {
2023-07-10 05:31:46 +02:00
BoxWithConstraints {
TitledText(
it,
modifier = Modifier
.widthIn(min = maxWidth.div(2))
2023-07-10 18:00:03 +02:00
.padding(horizontal = 12.dp)
2023-07-10 05:31:46 +02:00
.width(IntrinsicSize.Max)
)
}
2023-07-03 03:05:50 +02:00
}
}
}
2023-07-09 16:42:42 +02:00
}
2023-07-03 03:05:50 +02:00
2023-07-09 16:42:42 +02:00
@Composable
2023-07-10 08:39:38 +02:00
fun TitledErrorText(titledText: TitledText?) {
2023-07-09 16:42:42 +02:00
TitledText(
titledText,
valueStyle = LocalTextStyle.current.copy(color = colorDestructive)
)
}
2023-07-04 07:41:40 +02:00
2023-07-09 16:42:42 +02:00
@Composable
2023-07-10 08:39:38 +02:00
fun TitledMonospaceText(titledText: TitledText?) {
2023-07-09 16:42:42 +02:00
TitledText(
titledText,
valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
)
}
2023-07-04 07:41:40 +02:00
2023-07-09 16:42:42 +02:00
@Composable
fun TitledText(
2023-07-10 08:39:38 +02:00
titledText: TitledText?,
2023-07-09 16:42:42 +02:00
modifier: Modifier = Modifier,
2023-07-10 08:39:38 +02:00
valueStyle: TextStyle = LocalTextStyle.current,
2023-07-09 16:42:42 +02:00
) {
2023-07-10 08:39:38 +02:00
titledText?.apply {
TitledView(title, modifier) {
Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth())
}
2023-06-29 04:07:55 +02:00
}
2023-07-09 16:42:42 +02:00
}
2023-06-29 09:41:11 +02:00
2023-07-09 16:42:42 +02:00
@Composable
2023-07-10 08:39:38 +02:00
fun TitledView(title: GetString, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
2023-07-09 16:42:42 +02:00
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
Title(title)
content()
2021-07-13 08:22:10 +02:00
}
2023-07-09 16:42:42 +02:00
}
2023-06-29 04:07:55 +02:00
2023-07-09 16:42:42 +02:00
@Composable
2023-07-10 08:39:38 +02:00
fun Title(title: GetString) {
Text(title.string(), fontWeight = FontWeight.Bold)
2023-06-29 09:41:11 +02:00
}