Add strings

This commit is contained in:
andrew 2023-07-10 16:09:38 +09:30
parent a1e8ad2c37
commit 09b321530d
6 changed files with 128 additions and 114 deletions

View File

@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.pager.HorizontalPager 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.CellNoMargin
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator
import org.thoughtcrime.securesms.ui.ItemButton import org.thoughtcrime.securesms.ui.ItemButton
import org.thoughtcrime.securesms.ui.PreviewTheme import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
import org.thoughtcrime.securesms.ui.TitledText
import org.thoughtcrime.securesms.ui.blackAlpha40 import org.thoughtcrime.securesms.ui.blackAlpha40
import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.colorDestructive
import org.thoughtcrime.securesms.ui.destructiveButtonColors import org.thoughtcrime.securesms.ui.destructiveButtonColors
@ -160,22 +161,6 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) 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") @SuppressLint("ClickableViewAccessibility")
@ -215,9 +200,9 @@ fun MessageDetails(
) )
} }
Carousel(state.imageAttachments) { onClickImage(it) } Carousel(state.imageAttachments) { onClickImage(it) }
state.nonImageAttachment?.fileDetails?.let { FileDetails(it) } state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
MetadataCell(state) CellMetadata(state)
Buttons( CellButtons(
onReply, onReply,
onResend, onResend,
onDelete, onDelete,
@ -226,17 +211,18 @@ fun MessageDetails(
} }
@Composable @Composable
fun MetadataCell( fun CellMetadata(
state: MessageDetailsState, state: MessageDetailsState,
) { ) {
state.apply { 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)) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
sent?.let { TitledText(it) } TitledText(sent)
received?.let { TitledText(it) } TitledText(received)
error?.let { TitledErrorText(it) } TitledErrorText(error)
senderInfo?.let { senderInfo?.let {
TitledView(stringResource(id = R.string.message_details_header__from)) { TitledView(state.fromTitle) {
Row { Row {
sender?.let { Avatar(it) } sender?.let { Avatar(it) }
TitledMonospaceText(it) TitledMonospaceText(it)
@ -249,7 +235,7 @@ fun MetadataCell(
} }
@Composable @Composable
fun Buttons( fun CellButtons(
onReply: () -> Unit = {}, onReply: () -> Unit = {},
onResend: (() -> Unit)? = null, onResend: (() -> Unit)? = null,
onDelete: () -> Unit = {}, onDelete: () -> Unit = {},
@ -257,21 +243,21 @@ fun Buttons(
Cell { Cell {
Column { Column {
ItemButton( ItemButton(
stringResource(id = R.string.reply), stringResource(R.string.reply),
R.drawable.ic_message_details__reply, R.drawable.ic_message_details__reply,
onClick = onReply onClick = onReply
) )
Divider() Divider()
onResend?.let { onResend?.let {
ItemButton( ItemButton(
stringResource(id = R.string.resend), stringResource(R.string.resend),
R.drawable.ic_message_details__refresh, R.drawable.ic_message_details__refresh,
onClick = it onClick = it
) )
Divider() Divider()
} }
ItemButton( ItemButton(
stringResource(id = R.string.delete), stringResource(R.string.delete),
R.drawable.ic_message_details__trash, R.drawable.ic_message_details__trash,
colors = destructiveButtonColors(), colors = destructiveButtonColors(),
onClick = onDelete 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 @Preview
@Composable @Composable
@ -368,7 +338,20 @@ fun PreviewMessageDetails(
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
) { ) {
PreviewTheme(themeResId) { 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<TitledText>) {
} }
@Composable @Composable
fun TitledErrorText(titledText: TitledText, modifier: Modifier = Modifier) { fun TitledErrorText(titledText: TitledText?) {
TitledText( TitledText(
titledText, titledText,
modifier = modifier,
valueStyle = LocalTextStyle.current.copy(color = colorDestructive) valueStyle = LocalTextStyle.current.copy(color = colorDestructive)
) )
} }
@Composable @Composable
fun TitledMonospaceText(titledText: TitledText, modifier: Modifier = Modifier) { fun TitledMonospaceText(titledText: TitledText?) {
TitledText( TitledText(
titledText, titledText,
modifier = modifier,
valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
) )
} }
@Composable @Composable
fun TitledText( fun TitledText(
titledText: TitledText, titledText: TitledText?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
valueStyle: TextStyle = LocalTextStyle.current valueStyle: TextStyle = LocalTextStyle.current,
) { ) {
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { titledText?.apply {
Title(titledText.title) TitledView(title, modifier) {
Text(titledText.value, style = valueStyle, modifier = Modifier.fillMaxWidth()) Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth())
}
} }
} }
@Composable @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)) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
Title(title) Title(title)
content() content()
@ -432,6 +414,6 @@ fun TitledView(title: String, modifier: Modifier = Modifier, content: @Composabl
} }
@Composable @Composable
fun Title(text: String, modifier: Modifier = Modifier) { fun Title(title: GetString) {
Text(text, modifier = modifier, fontWeight = FontWeight.Bold) Text(title.string(), fontWeight = FontWeight.Bold)
} }

View File

@ -3,12 +3,8 @@ package org.thoughtcrime.securesms.conversation.v2
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import network.loki.messenger.R
import kotlinx.coroutines.launch
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.Util import org.session.libsession.utilities.Util
import org.session.libsession.utilities.recipients.Recipient 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.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.Slide 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 java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
data class TitledText(val title: String, val value: String)
data class MessageDetailsState(
val attachments: List<Attachment> = 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<TitledText>
) {
val fileName: String? get() = slide.fileName.orNull()
val uri get() = slide.uri
fun hasImage() = slide is ImageSlide
}
@HiltViewModel @HiltViewModel
class MessageDetailsViewModel @Inject constructor( class MessageDetailsViewModel @Inject constructor(
private val attachmentDb: AttachmentDatabase, private val attachmentDb: AttachmentDatabase,
@ -60,22 +31,18 @@ class MessageDetailsViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
fun setMessageTimestamp(timestamp: Long) { fun setMessageTimestamp(timestamp: Long) {
mmsSmsDatabase.getMessageForTimestamp(timestamp).let(::setMessageRecord) val record = mmsSmsDatabase.getMessageForTimestamp(timestamp) ?: return
}
fun setMessageRecord(record: MessageRecord?) {
val mmsRecord = record as? MmsMessageRecord val mmsRecord = record as? MmsMessageRecord
val slides: List<Slide> = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() _details.value = record.run {
val slides = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList()
_details.value = record?.run {
MessageDetailsState( MessageDetailsState(
attachments = slides.map { Attachment(it, it.details) }, attachments = slides.map { Attachment(it, it.details) },
record = record, record = record,
mmsRecord = mmsRecord, sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) },
sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) },
received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) },
error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText("Error:", it) },
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
sender = individualRecipient, sender = individualRecipient,
thread = threadDb.getRecipientForThreadId(threadId)!!, thread = threadDb.getRecipientForThreadId(threadId)!!,
@ -88,13 +55,14 @@ class MessageDetailsViewModel @Inject constructor(
private val Slide.details: List<TitledText> private val Slide.details: List<TitledText>
get() = listOfNotNull( get() = listOfNotNull(
fileName.orNull()?.let { TitledText("File Id:", it) }, fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) },
TitledText("File Type:", asAttachment().contentType), TitledText(R.string.message_details_header__file_type, asAttachment().contentType),
TitledText("File Size:", Util.getPrettyFileSize(fileSize)), TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)),
takeIf { it.hasImage() } takeIf { it is ImageSlide }
.run { asAttachment().run { "${width}x$height" } } ?.let(Slide::asAttachment)
.let { TitledText("Resolution:", it) }, ?.run { "${width}x$height" }
attachmentDb.duration(this)?.let { TitledText("Duration:", it) }, ?.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? = private fun AttachmentDatabase.duration(slide: Slide): String? =
@ -111,3 +79,29 @@ class MessageDetailsViewModel @Inject constructor(
} }
} }
data class MessageDetailsState(
val attachments: List<Attachment> = emptyList(),
val imageAttachments: List<Attachment> = attachments.filter { it.hasImage() },
val nonImageAttachmentFileDetails: List<TitledText>? = 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<TitledText>
) {
val fileName: String? get() = slide.fileName.orNull()
val uri get() = slide.uri
fun hasImage() = slide is ImageSlide
}

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.ui package org.thoughtcrime.securesms.ui
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.FlingBehavior 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.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource 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.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
@ -61,6 +63,7 @@ fun ItemButton(
text: String, text: String,
@DrawableRes icon: Int, @DrawableRes icon: Int,
colors: ButtonColors = transparentButtonColors(), colors: ButtonColors = transparentButtonColors(),
contentDescription: String = text,
onClick: () -> Unit onClick: () -> Unit
) { ) {
TextButton( TextButton(
@ -76,7 +79,7 @@ fun ItemButton(
.fillMaxHeight()) { .fillMaxHeight()) {
Icon( Icon(
painter = painterResource(id = icon), painter = painterResource(id = icon),
contentDescription = "", contentDescription = contentDescription,
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
} }

View File

@ -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)
}

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.ui
import android.content.Context import android.content.Context
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.StyleRes
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -13,13 +12,10 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.conversation.v2.PreviewMessageDetails
val LocalExtraColors = staticCompositionLocalOf<ExtraColors> { error("No Custom Attribute value provided") } val LocalExtraColors = staticCompositionLocalOf<ExtraColors> { error("No Custom Attribute value provided") }

View File

@ -520,6 +520,11 @@
<string name="message_details_header__to">To:</string> <string name="message_details_header__to">To:</string>
<string name="message_details_header__from">From:</string> <string name="message_details_header__from">From:</string>
<string name="message_details_header__with">With:</string> <string name="message_details_header__with">With:</string>
<string name="message_details_header__file_id">File Id:</string>
<string name="message_details_header__file_type">File Type:</string>
<string name="message_details_header__file_size">File Size:</string>
<string name="message_details_header__resolution">Resolution:</string>
<string name="message_details_header__duration">Duration:</string>
<!-- AndroidManifest.xml --> <!-- AndroidManifest.xml -->
<string name="AndroidManifest__create_passphrase">Create passphrase</string> <string name="AndroidManifest__create_passphrase">Create passphrase</string>