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 ba94353b4e..33690da4a6 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 @@ -10,13 +10,16 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.IntrinsicSize 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 @@ -61,9 +64,6 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.database.Storage -import org.thoughtcrime.securesms.database.ThreadDatabase -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.Avatar import org.thoughtcrime.securesms.ui.CarouselNextButton @@ -103,21 +103,15 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) - timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) - - val messageRecord = - DatabaseComponent.get(this).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: run { - finish() - return - } - - val error = DatabaseComponent.get(this).lokiMessageDatabase() - .getErrorMessage(messageRecord.getId()) - - viewModel.setMessageRecord(messageRecord, error) - title = resources.getString(R.string.conversation_context__menu_message_details) + intent.getLongExtra(MESSAGE_TIMESTAMP, -1L).let(viewModel::setMessageTimestamp) + + if (viewModel.details.value == null) { + finish() + return + } + ComposeView(this) .apply { setContent { MessageDetailsScreen() } } .let(::setContentView) @@ -125,28 +119,27 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { @Composable private fun MessageDetailsScreen() { - val details by viewModel.details.observeAsState(MessageDetails()) - val threadDb = DatabaseComponent.get(this@MessageDetailActivity).threadDatabase() + val state by viewModel.details.observeAsState(MessageDetailsState()) AppTheme { MessageDetails( - threadDb = threadDb, - messageDetails = details, + state = state, onReply = { setResultAndFinish(ON_REPLY) }, - onResend = details.error?.let { { setResultAndFinish(ON_RESEND) } }, + onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } }, onDelete = { setResultAndFinish(ON_DELETE) }, - onClickImage = { slide -> + onClickImage = { i -> + val slide = state.attachments[i].slide // only open to downloaded images if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { // Restart download here (on IO thread) (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> - onAttachmentNeedsDownload(attachment.attachmentId.rowId, details.mmsRecord!!.getId()) + onAttachmentNeedsDownload(attachment.attachmentId.rowId, state.mmsRecord!!.getId()) } } if (!slide.isInProgress) MediaPreviewActivity.getPreviewIntent( this, slide, - details.mmsRecord, - threadDb.getRecipientForThreadId(details.mmsRecord!!.threadId), + state.mmsRecord, + state.thread, ).let(::startActivity) }, onAttachmentNeedsDownload = ::onAttachmentNeedsDownload, @@ -174,7 +167,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { fun PreviewMessageDetails() { AppTheme { MessageDetails( - messageDetails = 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"), @@ -188,12 +181,11 @@ fun PreviewMessageDetails() { @SuppressLint("ClickableViewAccessibility") @Composable fun MessageDetails( - threadDb: ThreadDatabase? = null, - messageDetails: MessageDetails, + state: MessageDetailsState, onReply: () -> Unit = {}, onResend: (() -> Unit)? = null, onDelete: () -> Unit = {}, - onClickImage: (Slide) -> Unit = {}, + onClickImage: (Int) -> Unit = {}, onAttachmentNeedsDownload: (Long, Long) -> Unit = { _, _ -> } ) { Column( @@ -202,14 +194,14 @@ fun MessageDetails( .padding(vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - messageDetails.record?.let { message -> + state.record?.let { message -> AndroidView( modifier = Modifier.padding(horizontal = 32.dp), factory = { ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply { bind( message, - thread = threadDb?.getRecipientForThreadId(message.threadId)!!, + thread = state.thread!!, onAttachmentNeedsDownload = onAttachmentNeedsDownload, suppressThumbnails = true ) @@ -222,8 +214,9 @@ fun MessageDetails( } ) } - Carousel(messageDetails.attachments) { onClickImage(it) } - MetadataCell(messageDetails) + Carousel(state.imageAttachments) { onClickImage(it) } + state.nonImageAttachment?.fileDetails?.let { FileDetails(it) } + MetadataCell(state) Buttons( onReply, onResend, @@ -234,9 +227,9 @@ fun MessageDetails( @Composable fun MetadataCell( - messageDetails: MessageDetails, + state: MessageDetailsState, ) { - messageDetails.apply { + state.apply { if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { sent?.let { TitledText(it) } @@ -289,25 +282,26 @@ fun Buttons( @OptIn(ExperimentalFoundationApi::class) @Composable -fun Carousel(attachments: List, onClick: (Slide) -> Unit) { - val imageAttachments = attachments.filter { it.hasImage() }.takeIf { it.isNotEmpty() } ?: return - val pagerState = rememberPagerState { imageAttachments.size } +fun Carousel(attachments: List, onClick: (Int) -> Unit) { + if (attachments.isEmpty()) return + + val pagerState = rememberPagerState { attachments.size } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row { CarouselPrevButton(pagerState) Box(modifier = Modifier.weight(1f)) { - CellCarousel(pagerState, imageAttachments, onClick) + CellCarousel(pagerState, attachments, onClick) HorizontalPagerIndicator(pagerState) ExpandButton( modifier = Modifier .align(Alignment.BottomEnd) .padding(8.dp) - ) { onClick(imageAttachments[pagerState.currentPage].slide) } + ) { onClick(pagerState.currentPage) } } CarouselNextButton(pagerState) } - FileDetails(attachments, pagerState) + attachments.getOrNull(pagerState.currentPage)?.fileDetails?.let { FileDetails(it) } } } @@ -318,19 +312,18 @@ fun Carousel(attachments: List, onClick: (Slide) -> Unit) { @Composable private fun CellCarousel( pagerState: PagerState, - imageAttachments: List, - onClick: (Slide) -> Unit + attachments: List, + onClick: (Int) -> Unit ) { CellNoMargin { HorizontalPager(state = pagerState) { i -> - val slide = imageAttachments[i].slide GlideImage( contentScale = ContentScale.Crop, modifier = Modifier .aspectRatio(1f) - .clickable { onClick(slide) }, - model = slide.uri, - contentDescription = slide.fileName.orNull() ?: stringResource(id = R.string.image) + .clickable { onClick(i) }, + model = attachments[i].uri, + contentDescription = attachments[i].fileName ?: stringResource(id = R.string.image) ) } } @@ -346,7 +339,7 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) { ) { Icon( painter = painterResource(id = R.drawable.ic_expand), - contentDescription = "", + contentDescription = stringResource(id = R.string.expand), modifier = Modifier.clickable { onClick() }, ) } @@ -379,12 +372,6 @@ fun PreviewMessageDetails( } } -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun FileDetails(attachments: List, pagerState: PagerState) { - FileDetails(attachments[pagerState.currentPage].fileDetails) -} - @OptIn(ExperimentalLayoutApi::class) @Composable fun FileDetails(fileDetails: List) { @@ -393,13 +380,14 @@ fun FileDetails(fileDetails: List) { CellWithPaddingAndMargin { FlowRow(verticalArrangement = Arrangement.spacedBy(16.dp)) { fileDetails.forEach { - TitledText( - it, - modifier = Modifier - .widthIn(min = 100.dp) // set minimum width - .width(IntrinsicSize.Max) // make the text as wide as necessary - .weight(1f) // space evenly - ) + BoxWithConstraints { + TitledText( + it, + modifier = Modifier + .widthIn(min = maxWidth.div(2)) + .width(IntrinsicSize.Max) + ) + } } } } @@ -431,7 +419,7 @@ fun TitledText( ) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { Title(titledText.title) - Text(titledText.value, style = valueStyle) + Text(titledText.value, style = valueStyle, modifier = Modifier.fillMaxWidth()) } } 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 5ff1e05176..6bf4223b4b 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,11 +3,19 @@ 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 org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.database.AttachmentDatabase +import org.thoughtcrime.securesms.database.LokiMessageDatabase +import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.mms.ImageSlide @@ -18,7 +26,7 @@ import javax.inject.Inject data class TitledText(val title: String, val value: String) -data class MessageDetails( +data class MessageDetailsState( val attachments: List = emptyList(), val record: MessageRecord? = null, val mmsRecord: MmsMessageRecord? = null, @@ -26,44 +34,57 @@ data class MessageDetails( val received: TitledText? = null, val error: TitledText? = null, val senderInfo: TitledText? = null, - val sender: Recipient? = 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 + private val attachmentDb: AttachmentDatabase, + private val lokiMessageDatabase: LokiMessageDatabase, + private val mmsSmsDatabase: MmsSmsDatabase, + private val threadDb: ThreadDatabase, ) : ViewModel() { - fun setMessageRecord(record: MessageRecord?, error: String?) { + fun setMessageTimestamp(timestamp: Long) { + mmsSmsDatabase.getMessageForTimestamp(timestamp).let(::setMessageRecord) + } + + fun setMessageRecord(record: MessageRecord?) { val mmsRecord = record as? MmsMessageRecord val slides: List = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() _details.value = record?.run { - MessageDetails( + MessageDetailsState( + attachments = slides.map { Attachment(it, it.details) }, record = record, mmsRecord = mmsRecord, - attachments = slides.map { Attachment(it, it.details) }, sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, - error = error?.let { TitledText("Error:", it) }, - senderInfo = individualRecipient.run { - name?.let { TitledText(it, address.serialize()) } - }, - sender = individualRecipient + error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText("Error:", it) }, + senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, + sender = individualRecipient, + thread = threadDb.getRecipientForThreadId(threadId)!!, ) } } - private var _details = MutableLiveData(MessageDetails()) - val details: LiveData = _details + private var _details = MutableLiveData(MessageDetailsState()) + val details: LiveData = _details private val Slide.details: List get() = listOfNotNull( @@ -88,4 +109,5 @@ class MessageDetailsViewModel @Inject constructor( TimeUnit.MILLISECONDS.toSeconds(it) % 60 ) } + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt index d63b46d8dd..55bc1be62e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -16,8 +16,6 @@ const val classicDark4 = 0xff767676 const val classicDark5 = 0xffA1A2A1 const val classicDark6 = 0xffFFFFFF - - const val classicLight0 = 0xff000000 const val classicLight1 = 0xff6D6D6D const val classicLight2 = 0xffA1A2A1 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 8589f3e7e0..0b712467fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -56,12 +56,6 @@ import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.components.ProfilePictureView import kotlin.math.roundToInt -private val Colors.cellColors: Colors - @Composable - get() = MaterialTheme.colors.copy( - surface = LocalExtraColors.current.settingsBackground, - ) - @Composable fun ItemButton( text: String, @@ -105,20 +99,23 @@ fun CellWithPaddingAndMargin( margin: Dp = 32.dp, content: @Composable () -> Unit ) { - MaterialTheme(colors = MaterialTheme.colors.cellColors) { - Card( - shape = RoundedCornerShape(16.dp), - elevation = 0.dp, - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - .padding(horizontal = margin), - ) { - Box(Modifier.padding(padding)) { content() } - } + Card( + backgroundColor = MaterialTheme.colors.cellColor, + shape = RoundedCornerShape(16.dp), + elevation = 0.dp, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = margin), + ) { + Box(Modifier.padding(padding)) { content() } } } +private val Colors.cellColor: Color + @Composable + get() = LocalExtraColors.current.settingsBackground + @OptIn(ExperimentalFoundationApi::class) @Composable fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1c718932fd..ff5303e6c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ Image Note to Self Version %s + Expand Create session ID