package org.thoughtcrime.securesms.conversation.v2 import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.MotionEvent.ACTION_UP import androidx.activity.viewModels 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.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.LocalTextStyle import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.lifecycleScope import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.JobQueue import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.Avatar import org.thoughtcrime.securesms.ui.CarouselNextButton import org.thoughtcrime.securesms.ui.CarouselPrevButton 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.HorizontalPagerIndicator import org.thoughtcrime.securesms.ui.ItemButton import org.thoughtcrime.securesms.ui.blackAlpha40 import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors import javax.inject.Inject @AndroidEntryPoint class MessageDetailActivity : PassphraseRequiredActionBarActivity() { private var timestamp: Long = 0L @Inject lateinit var storage: Storage private val viewModel: MessageDetailsViewModel by viewModels() companion object { // Extras const val MESSAGE_TIMESTAMP = "message_timestamp" const val ON_REPLY = 1 const val ON_RESEND = 2 const val ON_DELETE = 3 } 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) ComposeView(this) .apply { setContent { MessageDetailsScreen() } } .let(::setContentView) } @Composable private fun MessageDetailsScreen() { val details by viewModel.details.observeAsState(MessageDetails()) MessageDetails( details, onReply = { setResultAndFinish(ON_REPLY) }, onResend = { setResultAndFinish(ON_RESEND) }, onDelete = { setResultAndFinish(ON_DELETE) }, onClickImage = { slide -> MediaPreviewActivity.getPreviewIntent( this, slide, details.mmsRecord, details.sender ) .let(::startActivity) } ) } private fun setResultAndFinish(code: Int) { Bundle().apply { putLong(MESSAGE_TIMESTAMP, timestamp) } .let(Intent()::putExtras) .let { setResult(code, it) } finish() } @Preview @Composable fun PreviewMessageDetails() { MessageDetails( MessageDetails( 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", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg"), ) ) } private fun onAttachmentNeedsDownload(attachmentId: Long, mmsId: Long) { lifecycleScope.launch(Dispatchers.IO) { JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) } } @SuppressLint("ClickableViewAccessibility") @Composable fun MessageDetails( messageDetails: MessageDetails, onReply: () -> Unit = {}, onResend: () -> Unit = {}, onDelete: () -> Unit = {}, onClickImage: (Slide) -> Unit = {}, ) { AppTheme { Column( modifier = Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { messageDetails.mmsRecord?.let { message -> AndroidView( modifier = Modifier.padding(32.dp), factory = { ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply { bind( message, thread = message.individualRecipient, onAttachmentNeedsDownload = ::onAttachmentNeedsDownload, suppressThumbnails = true ) setOnTouchListener { _, event -> if (event.actionMasked == ACTION_UP) onContentClick(event) true } } } ) } Attachments(messageDetails.attachments) { onClickImage(it) } MetadataCell(messageDetails) Buttons( messageDetails.error != null, onReply, onResend, onDelete, ) } } } @Composable fun MetadataCell( messageDetails: MessageDetails, ) { messageDetails.apply { if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { sent?.let { TitledText(it) } received?.let { TitledText(it) } error?.let { TitledErrorText(it) } senderInfo?.let { TitledView("From:") { Row { sender?.let { Avatar(it) } TitledMonospaceText(it) } } } } } } } @Composable fun Buttons( hasError: Boolean, onReply: () -> Unit = {}, onResend: () -> Unit = {}, onDelete: () -> Unit = {}, ) { Cell { Column { ItemButton( "Reply", R.drawable.ic_message_details__reply, onClick = onReply ) Divider() if (hasError) { ItemButton( "Resend", R.drawable.ic_message_details__refresh, onClick = onResend ) Divider() } ItemButton( "Delete", R.drawable.ic_message_details__trash, colors = destructiveButtonColors(), onClick = onDelete ) } } } @Composable fun Attachments(attachments: List, onClick: (Slide) -> Unit) { when(attachments.firstOrNull()?.slide) { is ImageSlide -> Carousel(attachments, onClick) } } @OptIn(ExperimentalFoundationApi::class) @Composable fun Carousel(attachments: List, onClick: (Slide) -> Unit) { val imageAttachments = attachments.filter { it.slide.hasImage() } val pagerState = rememberPagerState { imageAttachments.size } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row { CarouselPrevButton(pagerState) Box(modifier = Modifier.weight(1f)) { CellPager(pagerState, imageAttachments, onClick) HorizontalPagerIndicator(pagerState) ExpandButton( modifier = Modifier .clickable { onClick(imageAttachments[pagerState.currentPage].slide) } .align(Alignment.BottomEnd) .padding(8.dp) ) } CarouselNextButton(pagerState) } FileDetails(attachments, pagerState) } } @OptIn( ExperimentalFoundationApi::class, ExperimentalGlideComposeApi::class ) @Composable private fun CellPager( pagerState: PagerState, imageAttachments: List, onClick: (Slide) -> 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() ?: "image" ) } } } @Composable fun ExpandButton(modifier: Modifier) { Surface( shape = CircleShape, color = blackAlpha40, modifier = modifier ) { Icon( painter = painterResource(id = R.drawable.ic_expand), contentDescription = "" ) } } @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @Composable fun FileDetails(attachments: List, pagerState: PagerState) { attachments[pagerState.currentPage].fileDetails.takeIf { it.isNotEmpty() }?.let { CellWithPaddingAndMargin { FlowRow( verticalArrangement = Arrangement.spacedBy(16.dp), maxItemsInEachRow = 2 ) { it.forEach { TitledText(it, Modifier.weight(1f)) } } } } } @Composable fun TitledErrorText(titledText: TitledText, modifier: Modifier = Modifier) { TitledText( titledText, modifier = modifier, valueStyle = LocalTextStyle.current.copy(color = colorDestructive) ) } @Composable fun TitledMonospaceText(titledText: TitledText, modifier: Modifier = Modifier) { TitledText( titledText, modifier = modifier, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) ) } @Composable fun TitledText( titledText: TitledText, modifier: Modifier = Modifier, valueStyle: TextStyle = LocalTextStyle.current ) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { Title(titledText.title) Text(titledText.value, style = valueStyle) } } @Composable fun TitledView(title: String, modifier: Modifier = Modifier, content: @Composable () -> Unit) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { Title(title) content() } } @Composable fun Title(text: String) { Text(text, fontWeight = FontWeight.Bold) } }