diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 33690da4a6..33ef148245 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.pager.HorizontalPager @@ -72,10 +71,12 @@ import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.CellNoMargin import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin import org.thoughtcrime.securesms.ui.Divider +import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator import org.thoughtcrime.securesms.ui.ItemButton import org.thoughtcrime.securesms.ui.PreviewTheme import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider +import org.thoughtcrime.securesms.ui.TitledText import org.thoughtcrime.securesms.ui.blackAlpha40 import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors @@ -160,22 +161,6 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) } } - -} - -@Composable -fun PreviewMessageDetails() { - AppTheme { - MessageDetails( - state = MessageDetailsState( - attachments = listOf(), - sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), - received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), - error = TitledText("Error:", "Message failed to send"), - senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"), - ) - ) - } } @SuppressLint("ClickableViewAccessibility") @@ -215,9 +200,9 @@ fun MessageDetails( ) } Carousel(state.imageAttachments) { onClickImage(it) } - state.nonImageAttachment?.fileDetails?.let { FileDetails(it) } - MetadataCell(state) - Buttons( + state.nonImageAttachmentFileDetails?.let { FileDetails(it) } + CellMetadata(state) + CellButtons( onReply, onResend, onDelete, @@ -226,17 +211,18 @@ fun MessageDetails( } @Composable -fun MetadataCell( +fun CellMetadata( state: MessageDetailsState, ) { state.apply { - if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { + if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return + CellWithPaddingAndMargin { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - sent?.let { TitledText(it) } - received?.let { TitledText(it) } - error?.let { TitledErrorText(it) } + TitledText(sent) + TitledText(received) + TitledErrorText(error) senderInfo?.let { - TitledView(stringResource(id = R.string.message_details_header__from)) { + TitledView(state.fromTitle) { Row { sender?.let { Avatar(it) } TitledMonospaceText(it) @@ -249,7 +235,7 @@ fun MetadataCell( } @Composable -fun Buttons( +fun CellButtons( onReply: () -> Unit = {}, onResend: (() -> Unit)? = null, onDelete: () -> Unit = {}, @@ -257,21 +243,21 @@ fun Buttons( Cell { Column { ItemButton( - stringResource(id = R.string.reply), + stringResource(R.string.reply), R.drawable.ic_message_details__reply, onClick = onReply ) Divider() onResend?.let { ItemButton( - stringResource(id = R.string.resend), + stringResource(R.string.resend), R.drawable.ic_message_details__refresh, onClick = it ) Divider() } ItemButton( - stringResource(id = R.string.delete), + stringResource(R.string.delete), R.drawable.ic_message_details__trash, colors = destructiveButtonColors(), onClick = onDelete @@ -345,22 +331,6 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) { } } -@Preview -@Composable -fun PreviewFileDetails( - @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int -) { - PreviewTheme(themeResId) { - FileDetails( - fileDetails = listOf( - TitledText("File Id:", "Screen Shot 2023-07-06 at 11.35.50 am.png"), - TitledText("File Type:", "image/png"), - TitledText("File Size:", "195.6kB"), - TitledText("Resolution:", "342x312"), - ) - ) - } -} @Preview @Composable @@ -368,7 +338,20 @@ fun PreviewMessageDetails( @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int ) { PreviewTheme(themeResId) { - PreviewMessageDetails() + 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"), + ) + ) } } @@ -394,37 +377,36 @@ fun FileDetails(fileDetails: List) { } @Composable -fun TitledErrorText(titledText: TitledText, modifier: Modifier = Modifier) { +fun TitledErrorText(titledText: TitledText?) { TitledText( titledText, - modifier = modifier, valueStyle = LocalTextStyle.current.copy(color = colorDestructive) ) } @Composable -fun TitledMonospaceText(titledText: TitledText, modifier: Modifier = Modifier) { +fun TitledMonospaceText(titledText: TitledText?) { TitledText( titledText, - modifier = modifier, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) ) } @Composable fun TitledText( - titledText: TitledText, + titledText: TitledText?, modifier: Modifier = Modifier, - valueStyle: TextStyle = LocalTextStyle.current + valueStyle: TextStyle = LocalTextStyle.current, ) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { - Title(titledText.title) - Text(titledText.value, style = valueStyle, modifier = Modifier.fillMaxWidth()) + titledText?.apply { + TitledView(title, modifier) { + Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth()) + } } } @Composable -fun TitledView(title: String, modifier: Modifier = Modifier, content: @Composable () -> Unit) { +fun TitledView(title: GetString, modifier: Modifier = Modifier, content: @Composable () -> Unit) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { Title(title) content() @@ -432,6 +414,6 @@ fun TitledView(title: String, modifier: Modifier = Modifier, content: @Composabl } @Composable -fun Title(text: String, modifier: Modifier = Modifier) { - Text(text, modifier = modifier, fontWeight = FontWeight.Bold) +fun Title(title: GetString) { + Text(title.string(), fontWeight = FontWeight.Bold) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index 6bf4223b4b..890c82c0a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -3,12 +3,8 @@ package org.thoughtcrime.securesms.conversation.v2 import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.lifecycleScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.session.libsession.messaging.jobs.AttachmentDownloadJob -import org.session.libsession.messaging.jobs.JobQueue +import network.loki.messenger.R import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.Recipient @@ -20,37 +16,12 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.Slide -import java.util.* +import org.thoughtcrime.securesms.ui.GetString +import org.thoughtcrime.securesms.ui.TitledText +import java.util.Date import java.util.concurrent.TimeUnit import javax.inject.Inject -data class TitledText(val title: String, val value: String) - -data class MessageDetailsState( - val attachments: List = emptyList(), - val record: MessageRecord? = null, - val mmsRecord: MmsMessageRecord? = null, - val sent: TitledText? = null, - val received: TitledText? = null, - val error: TitledText? = null, - val senderInfo: TitledText? = null, - val sender: Recipient? = null, - val thread: Recipient? = null, -) { - val imageAttachments = attachments.filter { it.hasImage() } - val nonImageAttachment: Attachment? = attachments.firstOrNull { !it.hasImage() } -} - -data class Attachment( - val slide: Slide, - val fileDetails: List -) { - val fileName: String? get() = slide.fileName.orNull() - val uri get() = slide.uri - - fun hasImage() = slide is ImageSlide -} - @HiltViewModel class MessageDetailsViewModel @Inject constructor( private val attachmentDb: AttachmentDatabase, @@ -60,22 +31,18 @@ class MessageDetailsViewModel @Inject constructor( ) : ViewModel() { fun setMessageTimestamp(timestamp: Long) { - mmsSmsDatabase.getMessageForTimestamp(timestamp).let(::setMessageRecord) - } - - fun setMessageRecord(record: MessageRecord?) { + val record = mmsSmsDatabase.getMessageForTimestamp(timestamp) ?: return val mmsRecord = record as? MmsMessageRecord - val slides: List = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() + _details.value = record.run { + val slides = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() - _details.value = record?.run { MessageDetailsState( attachments = slides.map { Attachment(it, it.details) }, record = record, - mmsRecord = mmsRecord, - sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, - received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, - error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText("Error:", it) }, + sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) }, + received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) }, + error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) }, senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, sender = individualRecipient, thread = threadDb.getRecipientForThreadId(threadId)!!, @@ -88,13 +55,14 @@ class MessageDetailsViewModel @Inject constructor( private val Slide.details: List get() = listOfNotNull( - fileName.orNull()?.let { TitledText("File Id:", it) }, - TitledText("File Type:", asAttachment().contentType), - TitledText("File Size:", Util.getPrettyFileSize(fileSize)), - takeIf { it.hasImage() } - .run { asAttachment().run { "${width}x$height" } } - .let { TitledText("Resolution:", it) }, - attachmentDb.duration(this)?.let { TitledText("Duration:", it) }, + fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) }, + TitledText(R.string.message_details_header__file_type, asAttachment().contentType), + TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)), + takeIf { it is ImageSlide } + ?.let(Slide::asAttachment) + ?.run { "${width}x$height" } + ?.let { TitledText(R.string.message_details_header__resolution, it) }, + attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) }, ) private fun AttachmentDatabase.duration(slide: Slide): String? = @@ -111,3 +79,29 @@ class MessageDetailsViewModel @Inject constructor( } } + +data class MessageDetailsState( + val attachments: List = emptyList(), + val imageAttachments: List = attachments.filter { it.hasImage() }, + val nonImageAttachmentFileDetails: List? = attachments.firstOrNull { !it.hasImage() }?.fileDetails, + val record: MessageRecord? = null, + val mmsRecord: MmsMessageRecord? = record as? MmsMessageRecord, + val sent: TitledText? = null, + val received: TitledText? = null, + val error: TitledText? = null, + val senderInfo: TitledText? = null, + val sender: Recipient? = null, + val thread: Recipient? = null, +) { + val fromTitle = GetString(R.string.message_details_header__from) +} + +data class Attachment( + val slide: Slide, + val fileDetails: List +) { + val fileName: String? get() = slide.fileName.orNull() + val uri get() = slide.uri + + fun hasImage() = slide is ImageSlide +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 0b712467fb..883e3a2098 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.FlingBehavior @@ -46,6 +47,7 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -61,6 +63,7 @@ fun ItemButton( text: String, @DrawableRes icon: Int, colors: ButtonColors = transparentButtonColors(), + contentDescription: String = text, onClick: () -> Unit ) { TextButton( @@ -76,7 +79,7 @@ fun ItemButton( .fillMaxHeight()) { Icon( painter = painterResource(id = icon), - contentDescription = "", + contentDescription = contentDescription, modifier = Modifier.align(Alignment.Center) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt new file mode 100644 index 0000000000..44ff4a42d8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.ui + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource + +/** + * Compatibility class to allow ViewModels to use strings and string resources interchangeably. + */ +sealed class GetString { + @Composable + abstract fun string(): String + data class FromString(val string: String): GetString() { + @Composable + override fun string(): String = string + } + data class FromResId(@StringRes val resId: Int): GetString() { + @Composable + override fun string(): String = stringResource(resId) + + } +} + +fun GetString(@StringRes resId: Int) = GetString.FromResId(resId) +fun GetString(string: String) = GetString.FromString(string) + + +/** + * Represents some text with an associated title. + */ +data class TitledText(val title: GetString, val text: String) { + constructor(title: String, text: String): this(GetString(title), text) + constructor(@StringRes title: Int, text: String): this(GetString(title), text) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt index 2216c404e2..64bbd21d8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.ui import android.content.Context import androidx.annotation.AttrRes -import androidx.annotation.StyleRes import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -13,13 +12,10 @@ import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import com.google.android.material.color.MaterialColors import network.loki.messenger.R -import org.thoughtcrime.securesms.conversation.v2.PreviewMessageDetails val LocalExtraColors = staticCompositionLocalOf { error("No Custom Attribute value provided") } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ff5303e6c3..5afcb40b7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -520,6 +520,11 @@ To: From: With: + File Id: + File Type: + File Size: + Resolution: + Duration: Create passphrase