Merge branch 'dev' into ses-1733

This commit is contained in:
Andrew 2024-04-22 11:10:01 +09:30
commit c84f543069
8 changed files with 124 additions and 47 deletions

View File

@ -68,7 +68,7 @@ enum class ExpiryType(
AFTER_SEND( AFTER_SEND(
ExpiryMode::AfterSend, ExpiryMode::AfterSend,
R.string.expiration_type_disappear_after_send, R.string.expiration_type_disappear_after_send,
R.string.expiration_type_disappear_after_read_description, R.string.expiration_type_disappear_after_send_description,
R.string.AccessibilityId_disappear_after_send_option R.string.AccessibilityId_disappear_after_send_option
); );

View File

@ -1251,6 +1251,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// `position` is the adapter position; not the visual position // `position` is the adapter position; not the visual position
private fun handleSwipeToReply(message: MessageRecord) { private fun handleSwipeToReply(message: MessageRecord) {
if (message.isOpenGroupInvitation) return
val recipient = viewModel.recipient ?: return val recipient = viewModel.recipient ?: return
binding?.inputBar?.draftQuote(recipient, message, glide) binding?.inputBar?.draftQuote(recipient, message, glide)
} }

View File

@ -532,7 +532,7 @@ class ConversationReactionOverlay : FrameLayout {
items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select) items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
// Reply // Reply
val canWrite = openGroup == null || openGroup.canWrite val canWrite = openGroup == null || openGroup.canWrite
if (canWrite && !message.isPending && !message.isFailed) { if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) {
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message) items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message)
} }
// Copy message text // Copy message text

View File

@ -123,7 +123,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
AppTheme { AppTheme {
MessageDetails( MessageDetails(
state = state, state = state,
onReply = { setResultAndFinish(ON_REPLY) }, onReply = if (state.canReply) { { setResultAndFinish(ON_REPLY) } } else null,
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } }, onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
onDelete = { setResultAndFinish(ON_DELETE) }, onDelete = { setResultAndFinish(ON_DELETE) },
onClickImage = { viewModel.onClickImage(it) }, onClickImage = { viewModel.onClickImage(it) },
@ -145,7 +145,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
@Composable @Composable
fun MessageDetails( fun MessageDetails(
state: MessageDetailsState, state: MessageDetailsState,
onReply: () -> Unit = {}, onReply: (() -> Unit)? = null,
onResend: (() -> Unit)? = null, onResend: (() -> Unit)? = null,
onDelete: () -> Unit = {}, onDelete: () -> Unit = {},
onClickImage: (Int) -> Unit = {}, onClickImage: (Int) -> Unit = {},
@ -214,18 +214,20 @@ fun CellMetadata(
@Composable @Composable
fun CellButtons( fun CellButtons(
onReply: () -> Unit = {}, onReply: (() -> Unit)? = null,
onResend: (() -> Unit)? = null, onResend: (() -> Unit)? = null,
onDelete: () -> Unit = {}, onDelete: () -> Unit = {},
) { ) {
Cell { Cell {
Column { Column {
ItemButton( onReply?.let {
stringResource(R.string.reply), ItemButton(
R.drawable.ic_message_details__reply, stringResource(R.string.reply),
onClick = onReply R.drawable.ic_message_details__reply,
) onClick = it
Divider() )
Divider()
}
onResend?.let { onResend?.let {
ItemButton( ItemButton(
stringResource(R.string.resend), stringResource(R.string.resend),

View File

@ -117,7 +117,7 @@ class MessageDetailsViewModel @Inject constructor(
Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide) Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide)
fun onClickImage(index: Int) { fun onClickImage(index: Int) {
val state = state.value ?: return val state = state.value
val mmsRecord = state.mmsRecord ?: return val mmsRecord = state.mmsRecord ?: return
val slide = mmsRecord.slideDeck.slides[index] ?: return val slide = mmsRecord.slideDeck.slides[index] ?: return
// only open to downloaded images // only open to downloaded images
@ -158,6 +158,7 @@ data class MessageDetailsState(
val thread: Recipient? = null, val thread: Recipient? = null,
) { ) {
val fromTitle = GetString(R.string.message_details_header__from) val fromTitle = GetString(R.string.message_details_header__from)
val canReply = record?.isOpenGroupInvitation != true
} }
data class Attachment( data class Attachment(

View File

@ -77,7 +77,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide()) && firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
// Reply // Reply
menu.findItem(R.id.menu_context_reply).isVisible = menu.findItem(R.id.menu_context_reply).isVisible =
(selectedItems.size == 1 && !firstMessage.isPending && !firstMessage.isFailed) (selectedItems.size == 1 && !firstMessage.isPending && !firstMessage.isFailed && !firstMessage.isOpenGroupInvitation)
} }
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu): Boolean { override fun onPrepareActionMode(mode: ActionMode?, menu: Menu): Boolean {

View File

@ -37,6 +37,7 @@ import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.modifyLayoutParams import org.session.libsession.utilities.modifyLayoutParams
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase
@ -65,6 +66,7 @@ private const val TAG = "VisibleMessageView"
@AndroidEntryPoint @AndroidEntryPoint
class VisibleMessageView : LinearLayout { class VisibleMessageView : LinearLayout {
private var replyDisabled: Boolean = false
@Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var lokiThreadDb: LokiThreadDatabase @Inject lateinit var lokiThreadDb: LokiThreadDatabase
@Inject lateinit var lokiApiDb: LokiAPIDatabase @Inject lateinit var lokiApiDb: LokiAPIDatabase
@ -135,6 +137,7 @@ class VisibleMessageView : LinearLayout {
onAttachmentNeedsDownload: (Long, Long) -> Unit, onAttachmentNeedsDownload: (Long, Long) -> Unit,
lastSentMessageId: Long lastSentMessageId: Long
) { ) {
replyDisabled = message.isOpenGroupInvitation
val threadID = message.threadId val threadID = message.threadId
val thread = threadDb.getRecipientForThreadId(threadID) ?: return val thread = threadDb.getRecipientForThreadId(threadID) ?: return
val isGroupThread = thread.isGroupRecipient val isGroupThread = thread.isGroupRecipient
@ -206,7 +209,7 @@ class VisibleMessageView : LinearLayout {
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
binding.dateBreakTextView.isVisible = showDateBreak binding.dateBreakTextView.isVisible = showDateBreak
// Message status indicator // Update message status indicator
showStatusMessage(message) showStatusMessage(message)
// Emoji Reactions // Emoji Reactions
@ -243,44 +246,101 @@ class VisibleMessageView : LinearLayout {
onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() } onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() }
} }
// Method to display or hide the status of a message.
// Note: Although most commonly used to display the delivery status of a message, we also use the
// message status area to display the disappearing messages state - so in this latter case we'll
// be displaying the "Sent" and the animating clock icon for outgoing messages or "Read" and the
// animated clock icon for incoming messages.
private fun showStatusMessage(message: MessageRecord) { private fun showStatusMessage(message: MessageRecord) {
// We'll start by hiding everything and then only make visible what we need
binding.messageStatusTextView.isVisible = false
binding.messageStatusImageView.isVisible = false
binding.expirationTimerView.isVisible = false
val scheduledToDisappear = message.expiresIn > 0 // Get details regarding how we should display the message (it's delivery icon, icon tint colour, and
// the resource string for what text to display (R.string.delivery_status_sent etc.).
val (iconID, iconColor, textId) = getMessageStatusInfo(message)
// If we get any nulls then a message isn't one with a state that we care about (i.e., control messages
// etc.) - so bail. See: `DisplayRecord.is<WHATEVER>` for the full suite of message state methods.
// Also: We set all delivery status elements visibility to false just to make sure we don't display any
// stale data.
if (textId == null) return
binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> { binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> {
gravity = if (message.isOutgoing) Gravity.END else Gravity.START gravity = if (message.isOutgoing) Gravity.END else Gravity.START
} }
binding.statusContainer.modifyLayoutParams<ConstraintLayout.LayoutParams> { binding.statusContainer.modifyLayoutParams<ConstraintLayout.LayoutParams> {
horizontalBias = if (message.isOutgoing) 1f else 0f horizontalBias = if (message.isOutgoing) 1f else 0f
} }
binding.expirationTimerView.isGone = true // If the message is incoming AND it is not scheduled to disappear then don't show any status or timer details
val scheduledToDisappear = message.expiresIn > 0
if (message.isIncoming && !scheduledToDisappear) return
if (message.isOutgoing || scheduledToDisappear) { // Set text & icons as appropriate for the message state. Note: Possible message states we care
val (iconID, iconColor, textId) = getMessageStatusImage(message) // about are: isFailed, isSyncFailed, isPending, isSyncing, isResyncing, isRead, and isSent.
textId?.let(binding.messageStatusTextView::setText) textId.let(binding.messageStatusTextView::setText)
iconColor?.let(binding.messageStatusTextView::setTextColor) iconColor?.let(binding.messageStatusTextView::setTextColor)
iconID?.let { ContextCompat.getDrawable(context, it) } iconID?.let { ContextCompat.getDrawable(context, it) }
?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this } ?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
?.let(binding.messageStatusImageView::setImageDrawable) ?.let(binding.messageStatusImageView::setImageDrawable)
// Always show the delivery status of the last sent message // Potential options at this point are that the message is:
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context) // i.) incoming AND scheduled to disappear.
val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId) // ii.) outgoing but NOT scheduled to disappear, or
val isLastSentMessage = lastSentMessageId == message.id // iii.) outgoing AND scheduled to disappear.
binding.messageStatusTextView.isVisible = textId != null && (isLastSentMessage || scheduledToDisappear) // ----- Case i..) Message is incoming and scheduled to disappear -----
val showTimer = scheduledToDisappear && !message.isPending if (message.isIncoming && scheduledToDisappear) {
binding.messageStatusImageView.isVisible = iconID != null && !showTimer && (!message.isSent || isLastSentMessage) // Display the status ('Read') and the show the timer only (no delivery icon)
binding.messageStatusTextView.isVisible = true
binding.messageStatusImageView.bringToFront() binding.expirationTimerView.isVisible = true
binding.expirationTimerView.bringToFront() binding.expirationTimerView.bringToFront()
binding.expirationTimerView.isVisible = showTimer updateExpirationTimer(message)
if (showTimer) updateExpirationTimer(message) return
} else { }
binding.messageStatusTextView.isVisible = false
binding.messageStatusImageView.isVisible = false // --- If we got here then we know the message is outgoing ---
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
val isLastSentMessage = lastSentMessageId == message.id
// ----- Case ii.) Message is outgoing but NOT scheduled to disappear -----
if (!scheduledToDisappear) {
// If this isn't a disappearing message then we never show the timer
// If the message has NOT been successfully sent then always show the delivery status text and icon..
val neitherSentNorRead = !(message.isSent || message.isRead)
if (neitherSentNorRead) {
binding.messageStatusTextView.isVisible = true
binding.messageStatusImageView.isVisible = true
} else {
// ..but if the message HAS been successfully sent or read then only display the delivery status
// text and image if this is the last sent message.
binding.messageStatusTextView.isVisible = isLastSentMessage
binding.messageStatusImageView.isVisible = isLastSentMessage
if (isLastSentMessage) { binding.messageStatusImageView.bringToFront() }
}
}
else // ----- Case iii.) Message is outgoing AND scheduled to disappear -----
{
// Always display the delivery status text on all outgoing disappearing messages
binding.messageStatusTextView.isVisible = true
// If the message is sent or has been read..
val sentOrRead = message.isSent || message.isRead
if (sentOrRead) {
// ..then display the timer icon for this disappearing message (but keep the message status icon hidden)
binding.expirationTimerView.isVisible = true
binding.expirationTimerView.bringToFront()
updateExpirationTimer(message)
} else {
// If the message has NOT been sent or read (or it has failed) then show the delivery status icon rather than the timer icon
binding.messageStatusImageView.isVisible = true
binding.messageStatusImageView.bringToFront()
}
} }
} }
@ -302,10 +362,9 @@ class VisibleMessageView : LinearLayout {
@ColorInt val iconTint: Int?, @ColorInt val iconTint: Int?,
@StringRes val messageText: Int?) @StringRes val messageText: Int?)
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo = when { private fun getMessageStatusInfo(message: MessageRecord): MessageStatusInfo = when {
message.isFailed -> message.isFailed ->
MessageStatusInfo( MessageStatusInfo(R.drawable.ic_delivery_status_failed,
R.drawable.ic_delivery_status_failed,
resources.getColor(R.color.destructive, context.theme), resources.getColor(R.color.destructive, context.theme),
R.string.delivery_status_failed R.string.delivery_status_failed
) )
@ -318,24 +377,32 @@ class VisibleMessageView : LinearLayout {
message.isPending -> message.isPending ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_sending, R.drawable.ic_delivery_status_sending,
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_sending
) )
message.isResyncing -> message.isSyncing || message.isResyncing ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_sending, R.drawable.ic_delivery_status_sending,
context.getColor(R.color.accent_orange), R.string.delivery_status_syncing context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_sending // We COULD tell the user that we're `syncing` (R.string.delivery_status_syncing) but it will likely make more sense to them if we say "Sending"
) )
message.isRead || !message.isOutgoing -> message.isRead || message.isIncoming ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_read, R.drawable.ic_delivery_status_read,
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_read
) )
else -> message.isSent ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_sent, R.drawable.ic_delivery_status_sent,
context.getColorFromAttr(R.attr.message_status_color), context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_sent R.string.delivery_status_sent
) )
else -> {
// The message isn't one we care about for message statuses we display to the user (i.e.,
// control messages etc. - see the `DisplayRecord.is<WHATEVER>` suite of methods for options).
MessageStatusInfo(null, null, null)
}
} }
private fun updateExpirationTimer(message: MessageRecord) { private fun updateExpirationTimer(message: MessageRecord) {
@ -409,6 +476,7 @@ class VisibleMessageView : LinearLayout {
} else { } else {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
} }
if (replyDisabled) return
if (translationX > 0) { return } // Only allow swipes to the left if (translationX > 0) { return } // Only allow swipes to the left
// The idea here is to asymptotically approach a maximum drag distance // The idea here is to asymptotically approach a maximum drag distance
val damping = 50.0f val damping = 50.0f

View File

@ -114,6 +114,11 @@ public abstract class DisplayRecord {
public boolean isOutgoing() { public boolean isOutgoing() {
return MmsSmsColumns.Types.isOutgoingMessageType(type); return MmsSmsColumns.Types.isOutgoingMessageType(type);
} }
public boolean isIncoming() {
return !MmsSmsColumns.Types.isOutgoingMessageType(type);
}
public boolean isGroupUpdateMessage() { public boolean isGroupUpdateMessage() {
return SmsDatabase.Types.isGroupUpdateMessage(type); return SmsDatabase.Types.isGroupUpdateMessage(type);
} }