Merge branch 'dev' into on-2

This commit is contained in:
Andrew 2024-04-24 17:43:04 +09:30
commit 89d93bc80d
44 changed files with 428 additions and 344 deletions

View File

@ -31,8 +31,8 @@ configurations.all {
exclude module: "commons-logging" exclude module: "commons-logging"
} }
def canonicalVersionCode = 369 def canonicalVersionCode = 370
def canonicalVersionName = "1.18.1" def canonicalVersionName = "1.18.2"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -39,15 +39,20 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
public int getDesiredTheme() { public int getDesiredTheme() {
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences()); ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
int userSelectedTheme = themeState.getTheme(); int userSelectedTheme = themeState.getTheme();
// If the user has configured Session to follow the system light/dark theme mode then do so..
if (themeState.getFollowSystem()) { if (themeState.getFollowSystem()) {
// do light or dark based on the selected theme
// Use light or dark versions of the user's theme based on light-mode / dark-mode settings
boolean isDayUi = UiModeUtilities.isDayUiMode(this); boolean isDayUi = UiModeUtilities.isDayUiMode(this);
if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) { if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) {
return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark; return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark;
} else { } else {
return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark; return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark;
} }
} else { }
else // ..otherwise just return their selected theme.
{
return userSelectedTheme; return userSelectedTheme;
} }
} }

View File

@ -10,6 +10,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.Button import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.LinearLayout.VERTICAL import android.widget.LinearLayout.VERTICAL
import android.widget.Space
import android.widget.TextView import android.widget.TextView
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
@ -18,7 +19,6 @@ import androidx.annotation.StyleRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.view.setMargins import androidx.core.view.setMargins
import androidx.core.view.setPadding
import androidx.core.view.updateMargins import androidx.core.view.updateMargins
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import network.loki.messenger.R import network.loki.messenger.R
@ -33,13 +33,16 @@ class SessionDialogBuilder(val context: Context) {
private val dp20 = toPx(20, context.resources) private val dp20 = toPx(20, context.resources)
private val dp40 = toPx(40, context.resources) private val dp40 = toPx(40, context.resources)
private val dp60 = toPx(60, context.resources)
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context) private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
private var dialog: AlertDialog? = null private var dialog: AlertDialog? = null
private fun dismiss() = dialog?.dismiss() private fun dismiss() = dialog?.dismiss()
private val topView = LinearLayout(context).apply { orientation = VERTICAL } private val topView = LinearLayout(context)
.apply { setPadding(0, dp20, 0, 0) }
.apply { orientation = VERTICAL }
.also(dialogBuilder::setCustomTitle) .also(dialogBuilder::setCustomTitle)
private val contentView = LinearLayout(context).apply { orientation = VERTICAL } private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
private val buttonLayout = LinearLayout(context) private val buttonLayout = LinearLayout(context)
@ -55,14 +58,14 @@ class SessionDialogBuilder(val context: Context) {
fun title(text: CharSequence?) = title(text?.toString()) fun title(text: CharSequence?) = title(text?.toString())
fun title(text: String?) { fun title(text: String?) {
text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20) } text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20, 0, dp20, 0) }
} }
fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style) fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
fun text(text: CharSequence?, @StyleRes style: Int = 0) { fun text(text: CharSequence?, @StyleRes style: Int = 0) {
text(text, style) { text(text, style) {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
.apply { updateMargins(dp40, 0, dp40, dp20) } .apply { updateMargins(dp40, 0, dp40, 0) }
} }
} }
@ -74,6 +77,10 @@ class SessionDialogBuilder(val context: Context) {
textAlignment = View.TEXT_ALIGNMENT_CENTER textAlignment = View.TEXT_ALIGNMENT_CENTER
modify() modify()
}.let(topView::addView) }.let(topView::addView)
Space(context).apply {
layoutParams = LinearLayout.LayoutParams(0, dp20)
}.let(topView::addView)
} }
fun htmlText(@StringRes id: Int, @StyleRes style: Int = 0, modify: TextView.() -> Unit = {}) { text(context.resources.getText(id)) } fun htmlText(@StringRes id: Int, @StyleRes style: Int = 0, modify: TextView.() -> Unit = {}) { text(context.resources.getText(id)) }
@ -128,8 +135,7 @@ class SessionDialogBuilder(val context: Context) {
) = Button(context, null, 0, style).apply { ) = Button(context, null, 0, style).apply {
setText(text) setText(text)
contentDescription = resources.getString(contentDescriptionRes) contentDescription = resources.getString(contentDescriptionRes)
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1f) layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
.apply { setMargins(toPx(20, resources)) }
setOnClickListener { setOnClickListener {
listener.invoke() listener.invoke()
if (dismiss) dismiss() if (dismiss) dismiss()

View File

@ -17,6 +17,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.getColorFromAttr
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
@ -84,7 +85,7 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
context.theme.resolveAttribute(item.iconRes, typedValue, true) context.theme.resolveAttribute(item.iconRes, typedValue, true)
icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId)) icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId))
icon.imageTintList = color?.let(ColorStateList::valueOf) icon.imageTintList = ColorStateList.valueOf(color ?: context.getColorFromAttr(android.R.attr.textColor))
} }
item.contentDescription?.let(context.resources::getString)?.let { itemView.contentDescription = it } item.contentDescription?.let(context.resources::getString)?.let { itemView.contentDescription = it }
title.setText(item.title) title.setText(item.title)

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

@ -35,11 +35,28 @@ class ContactListAdapter(
binding.profilePictureView.update(contact.recipient) binding.profilePictureView.update(contact.recipient)
binding.nameTextView.text = contact.displayName binding.nameTextView.text = contact.displayName
binding.root.setOnClickListener { listener(contact.recipient) } binding.root.setOnClickListener { listener(contact.recipient) }
// TODO: When we implement deleting contacts (hide might be safest) then probably set a long-click listener here w/ something like:
/*
binding.root.setOnLongClickListener {
Log.w("[ACL]", "Long clicked on contact ${contact.recipient.name}")
binding.contentView.context.showSessionDialog {
title("Delete Contact")
text("Are you sure you want to delete this contact?")
button(R.string.delete) {
val contacts = configFactory.contacts ?: return
contacts.upsertContact(contact.recipient.address.serialize()) { priority = PRIORITY_HIDDEN }
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
endActionMode()
}
cancelButton(::endActionMode)
}
true
}
*/
} }
fun unbind() { fun unbind() { binding.profilePictureView.recycle() }
binding.profilePictureView.recycle()
}
} }
class HeaderViewHolder( class HeaderViewHolder(
@ -52,15 +69,11 @@ class ContactListAdapter(
} }
} }
override fun getItemCount(): Int { override fun getItemCount(): Int { return items.size }
return items.size
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) { override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder) super.onViewRecycled(holder)
if (holder is ContactViewHolder) { if (holder is ContactViewHolder) { holder.unbind() }
holder.unbind()
}
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
@ -72,13 +85,9 @@ class ContactListAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == ViewType.Contact) { return if (viewType == ViewType.Contact) {
ContactViewHolder( ContactViewHolder(ViewContactBinding.inflate(LayoutInflater.from(context), parent, false))
ViewContactBinding.inflate(LayoutInflater.from(context), parent, false)
)
} else { } else {
HeaderViewHolder( HeaderViewHolder(ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false))
ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false)
)
} }
} }

View File

@ -1252,6 +1252,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 {
onReply?.let {
ItemButton( ItemButton(
stringResource(R.string.reply), stringResource(R.string.reply),
R.drawable.ic_message_details__reply, R.drawable.ic_message_details__reply,
onClick = onReply 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

@ -140,6 +140,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
} }
fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) { fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) {
quoteView?.let(binding.inputBarAdditionalContentContainer::removeView)
quote = message quote = message
// If we already have a link preview View then clear the 'additional content' layout so that // If we already have a link preview View then clear the 'additional content' layout so that
@ -178,7 +180,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
// message we'll bail early if a link preview View already exists and just let // message we'll bail early if a link preview View already exists and just let
// `updateLinkPreview` get called to update the existing View. // `updateLinkPreview` get called to update the existing View.
if (linkPreview != null && linkPreviewDraftView != null) return if (linkPreview != null && linkPreviewDraftView != null) return
linkPreviewDraftView?.let(binding.inputBarAdditionalContentContainer::removeView)
linkPreviewDraftView = LinkPreviewDraftView(context).also { it.delegate = this } linkPreviewDraftView = LinkPreviewDraftView(context).also { it.delegate = this }
// Add the link preview View. Note: If there's already a quote View in the 'additional // Add the link preview View. Note: If there's already a quote View in the 'additional

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:
// i.) incoming AND scheduled to disappear.
// ii.) outgoing but NOT scheduled to disappear, or
// iii.) outgoing AND scheduled to disappear.
// ----- Case i..) Message is incoming and scheduled to disappear -----
if (message.isIncoming && scheduledToDisappear) {
// Display the status ('Read') and the show the timer only (no delivery icon)
binding.messageStatusTextView.isVisible = true
binding.expirationTimerView.isVisible = true
binding.expirationTimerView.bringToFront()
updateExpirationTimer(message)
return
}
// --- If we got here then we know the message is outgoing ---
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context) val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId) val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
val isLastSentMessage = lastSentMessageId == message.id val isLastSentMessage = lastSentMessageId == message.id
binding.messageStatusTextView.isVisible = textId != null && (isLastSentMessage || scheduledToDisappear) // ----- Case ii.) Message is outgoing but NOT scheduled to disappear -----
val showTimer = scheduledToDisappear && !message.isPending if (!scheduledToDisappear) {
binding.messageStatusImageView.isVisible = iconID != null && !showTimer && (!message.isSent || isLastSentMessage) // If this isn't a disappearing message then we never show the timer
binding.messageStatusImageView.bringToFront() // If the message has NOT been successfully sent then always show the delivery status text and icon..
binding.expirationTimerView.bringToFront() val neitherSentNorRead = !(message.isSent || message.isRead)
binding.expirationTimerView.isVisible = showTimer if (neitherSentNorRead) {
if (showTimer) updateExpirationTimer(message) binding.messageStatusTextView.isVisible = true
binding.messageStatusImageView.isVisible = true
} else { } else {
binding.messageStatusTextView.isVisible = false // ..but if the message HAS been successfully sent or read then only display the delivery status
binding.messageStatusImageView.isVisible = false // 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

@ -4,18 +4,24 @@ import android.content.Context
import android.graphics.Typeface import android.graphics.Typeface
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.util.Range import android.util.Range
import androidx.appcompat.widget.ThemeUtils
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.combine.Tuple2 import nl.komponents.kovenant.combine.Tuple2
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ThemeUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.getAccentColor
import org.thoughtcrime.securesms.util.getColorResourceIdFromAttr
import org.thoughtcrime.securesms.util.getMessageTextColourAttr
import java.util.regex.Pattern import java.util.regex.Pattern
object MentionUtilities { object MentionUtilities {
@ -58,15 +64,37 @@ object MentionUtilities {
} }
} }
val result = SpannableString(text) val result = SpannableString(text)
val isLightMode = UiModeUtilities.isDayUiMode(context)
val color = if (isOutgoingMessage) { var mentionTextColour: Int? = null
ResourcesCompat.getColor(context.resources, if (isLightMode) R.color.white else R.color.black, context.theme) // In dark themes..
} else { if (ThemeUtil.isDarkTheme(context)) {
context.getAccentColor() // ..we use the standard outgoing message colour for outgoing messages..
if (isOutgoingMessage) {
val mentionTextColourAttributeId = getMessageTextColourAttr(true)
val mentionTextColourResourceId = getColorResourceIdFromAttr(context, mentionTextColourAttributeId)
mentionTextColour = ResourcesCompat.getColor(context.resources, mentionTextColourResourceId, context.theme)
} }
else // ..but we use the accent colour for incoming messages (i.e., someone mentioning us)..
{
mentionTextColour = context.getAccentColor()
}
}
else // ..while in light themes we always just use the incoming or outgoing message text colour for mentions.
{
val mentionTextColourAttributeId = getMessageTextColourAttr(isOutgoingMessage)
val mentionTextColourResourceId = getColorResourceIdFromAttr(context, mentionTextColourAttributeId)
mentionTextColour = ResourcesCompat.getColor(context.resources, mentionTextColourResourceId, context.theme)
}
for (mention in mentions) { for (mention in mentions) {
result.setSpan(ForegroundColorSpan(color), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) result.setSpan(ForegroundColorSpan(mentionTextColour), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// If we're using a light theme then we change the background colour of the mention to be the accent colour
if (ThemeUtil.isLightTheme(context)) {
val backgroundColour = context.getAccentColor();
result.setSpan(BackgroundColorSpan(backgroundColour), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
} }
return result return result
} }

View File

@ -320,6 +320,19 @@ public class MmsSmsDatabase extends Database {
return -1; return -1;
} }
public long getLastMessageTimestamp(long threadId) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) {
if (cursor.moveToFirst()) {
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT));
}
}
return -1;
}
public Cursor getUnread() { public Cursor getUnread() {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC"; String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC";
String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0"; String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0";

View File

@ -22,15 +22,11 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteStatement; import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.session.libsession.messaging.calls.CallMessageType; import org.session.libsession.messaging.calls.CallMessageType;
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage; import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
@ -51,7 +47,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
@ -634,7 +629,7 @@ public class SmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(messageId); long threadId = getThreadIdForMessage(messageId);
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
notifyConversationListeners(threadId); notifyConversationListeners(threadId);
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true); boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, false);
return threadDeleted; return threadDeleted;
} }
@ -701,12 +696,7 @@ public class SmsDatabase extends MessagingDatabase {
} }
} }
/*package */void deleteThread(long threadId) { void deleteMessagesInThreadBeforeDate(long threadId, long date) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
}
/*package*/void deleteMessagesInThreadBeforeDate(long threadId, long date) {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = THREAD_ID + " = ? AND (CASE " + TYPE; String where = THREAD_ID + " = ? AND (CASE " + TYPE;
@ -719,7 +709,12 @@ public class SmsDatabase extends MessagingDatabase {
db.delete(TABLE_NAME, where, new String[] {threadId + ""}); db.delete(TABLE_NAME, where, new String[] {threadId + ""});
} }
/*package*/ void deleteThreads(Set<Long> threadIds) { void deleteThread(long threadId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
}
void deleteThreads(Set<Long> threadIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = ""; String where = "";
@ -727,23 +722,23 @@ public class SmsDatabase extends MessagingDatabase {
where += THREAD_ID + " = '" + threadId + "' OR "; where += THREAD_ID + " = '" + threadId + "' OR ";
} }
where = where.substring(0, where.length() - 4); where = where.substring(0, where.length() - 4); // Remove the final: "' OR "
db.delete(TABLE_NAME, where, null); db.delete(TABLE_NAME, where, null);
} }
/*package */ void deleteAllThreads() { void deleteAllThreads() {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null); db.delete(TABLE_NAME, null, null);
} }
/*package*/ SQLiteDatabase beginTransaction() { SQLiteDatabase beginTransaction() {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.beginTransaction(); database.beginTransaction();
return database; return database;
} }
/*package*/ void endTransaction(SQLiteDatabase database) { void endTransaction(SQLiteDatabase database) {
database.setTransactionSuccessful(); database.setTransactionSuccessful();
database.endTransaction(); database.endTransaction();
} }

View File

@ -99,7 +99,7 @@ private const val TAG = "Storage"
open class Storage( open class Storage(
context: Context, context: Context,
helper: SQLCipherOpenHelper, helper: SQLCipherOpenHelper,
private val configFactory: ConfigFactory val configFactory: ConfigFactory
) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener { ) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener {
override fun threadCreated(address: Address, threadId: Long) { override fun threadCreated(address: Address, threadId: Long) {
@ -1371,31 +1371,31 @@ open class Storage(
val threadDB = DatabaseComponent.get(context).threadDatabase() val threadDB = DatabaseComponent.get(context).threadDatabase()
val groupDB = DatabaseComponent.get(context).groupDatabase() val groupDB = DatabaseComponent.get(context).groupDatabase()
threadDB.deleteConversation(threadID) threadDB.deleteConversation(threadID)
val recipient = getRecipientForThread(threadID) ?: return
when { val recipient = getRecipientForThread(threadID)
recipient.isContactRecipient -> { if (recipient == null) {
if (recipient.isLocalNumber) return Log.w(TAG, "Got null recipient when deleting conversation - aborting.");
val contacts = configFactory.contacts ?: return return
contacts.upsertContact(recipient.address.serialize()) { priority = PRIORITY_HIDDEN }
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
} }
recipient.isClosedGroupRecipient -> {
// TODO: handle closed group // There is nothing further we need to do if this is a 1-on-1 conversation, and it's not
// possible to delete communities in this manner so bail.
if (recipient.isContactRecipient || recipient.isCommunityRecipient) return
// If we get here then this is a closed group conversation (i.e., recipient.isClosedGroupRecipient)
val volatile = configFactory.convoVolatile ?: return val volatile = configFactory.convoVolatile ?: return
val groups = configFactory.userGroups ?: return val groups = configFactory.userGroups ?: return
val groupID = recipient.address.toGroupString() val groupID = recipient.address.toGroupString()
val closedGroup = getGroup(groupID) val closedGroup = getGroup(groupID)
val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
if (closedGroup != null) { if (closedGroup != null) {
groupDB.delete(groupID) // TODO: Should we delete the group? (seems odd to leave it) groupDB.delete(groupID)
volatile.eraseLegacyClosedGroup(groupPublicKey) volatile.eraseLegacyClosedGroup(groupPublicKey)
groups.eraseLegacyGroup(groupPublicKey) groups.eraseLegacyGroup(groupPublicKey)
} else { } else {
Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}") Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
} }
} }
}
}
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri { override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
return PartAuthority.getAttachmentDataUri(attachmentId) return PartAuthority.getAttachmentDataUri(attachmentId)

View File

@ -26,14 +26,10 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.MergeCursor; import android.database.MergeCursor;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.session.libsession.snode.SnodeAPI; import org.session.libsession.snode.SnodeAPI;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
@ -61,7 +57,6 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.util.SessionMetaProtocol; import org.thoughtcrime.securesms.util.SessionMetaProtocol;
import java.io.Closeable; import java.io.Closeable;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -83,7 +78,7 @@ public class ThreadDatabase extends Database {
public static final String TABLE_NAME = "thread"; public static final String TABLE_NAME = "thread";
public static final String ID = "_id"; public static final String ID = "_id";
public static final String DATE = "date"; public static final String THREAD_CREATION_DATE = "date";
public static final String MESSAGE_COUNT = "message_count"; public static final String MESSAGE_COUNT = "message_count";
public static final String ADDRESS = "recipient_ids"; public static final String ADDRESS = "recipient_ids";
public static final String SNIPPET = "snippet"; public static final String SNIPPET = "snippet";
@ -91,7 +86,7 @@ public class ThreadDatabase extends Database {
public static final String READ = "read"; public static final String READ = "read";
public static final String UNREAD_COUNT = "unread_count"; public static final String UNREAD_COUNT = "unread_count";
public static final String UNREAD_MENTION_COUNT = "unread_mention_count"; public static final String UNREAD_MENTION_COUNT = "unread_mention_count";
public static final String TYPE = "type"; public static final String DISTRIBUTION_TYPE = "type"; // See: DistributionTypes.kt
private static final String ERROR = "error"; private static final String ERROR = "error";
public static final String SNIPPET_TYPE = "snippet_type"; public static final String SNIPPET_TYPE = "snippet_type";
public static final String SNIPPET_URI = "snippet_uri"; public static final String SNIPPET_URI = "snippet_uri";
@ -105,23 +100,23 @@ public class ThreadDatabase extends Database {
public static final String IS_PINNED = "is_pinned"; public static final String IS_PINNED = "is_pinned";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " + ID + " INTEGER PRIMARY KEY, " + THREAD_CREATION_DATE + " INTEGER DEFAULT 0, " +
MESSAGE_COUNT + " INTEGER DEFAULT 0, " + ADDRESS + " TEXT, " + SNIPPET + " TEXT, " + MESSAGE_COUNT + " INTEGER DEFAULT 0, " + ADDRESS + " TEXT, " + SNIPPET + " TEXT, " +
SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " + SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " +
TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " + DISTRIBUTION_TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " + SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " + ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " + DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " + LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " +
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNREAD_COUNT + " INTEGER DEFAULT 0);"; READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNREAD_COUNT + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = { public static final String[] CREATE_INDEXES = {
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + ADDRESS + ");", "CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + ADDRESS + ");",
"CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");", "CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");",
}; };
private static final String[] THREAD_PROJECTION = { private static final String[] THREAD_PROJECTION = {
ID, DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, UNREAD_MENTION_COUNT, TYPE, ERROR, SNIPPET_TYPE, ID, THREAD_CREATION_DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, UNREAD_MENTION_COUNT, DISTRIBUTION_TYPE, ERROR, SNIPPET_TYPE,
SNIPPET_URI, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, IS_PINNED SNIPPET_URI, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, IS_PINNED
}; };
@ -158,11 +153,10 @@ public class ThreadDatabase extends Database {
ContentValues contentValues = new ContentValues(4); ContentValues contentValues = new ContentValues(4);
long date = SnodeAPI.getNowWithOffset(); long date = SnodeAPI.getNowWithOffset();
contentValues.put(DATE, date - date % 1000); contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
contentValues.put(ADDRESS, address.serialize()); contentValues.put(ADDRESS, address.serialize());
if (group) if (group) contentValues.put(DISTRIBUTION_TYPE, distributionType);
contentValues.put(TYPE, distributionType);
contentValues.put(MESSAGE_COUNT, 0); contentValues.put(MESSAGE_COUNT, 0);
@ -175,7 +169,7 @@ public class ThreadDatabase extends Database {
long expiresIn, int readReceiptCount) long expiresIn, int readReceiptCount)
{ {
ContentValues contentValues = new ContentValues(7); ContentValues contentValues = new ContentValues(7);
contentValues.put(DATE, date - date % 1000); contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
contentValues.put(MESSAGE_COUNT, count); contentValues.put(MESSAGE_COUNT, count);
if (!body.isEmpty()) { if (!body.isEmpty()) {
contentValues.put(SNIPPET, body); contentValues.put(SNIPPET, body);
@ -187,9 +181,7 @@ public class ThreadDatabase extends Database {
contentValues.put(READ_RECEIPT_COUNT, readReceiptCount); contentValues.put(READ_RECEIPT_COUNT, readReceiptCount);
contentValues.put(EXPIRES_IN, expiresIn); contentValues.put(EXPIRES_IN, expiresIn);
if (unarchive) { if (unarchive) { contentValues.put(ARCHIVED, 0); }
contentValues.put(ARCHIVED, 0);
}
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""}); db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""});
@ -199,7 +191,7 @@ public class ThreadDatabase extends Database {
public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) { public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) {
ContentValues contentValues = new ContentValues(4); ContentValues contentValues = new ContentValues(4);
contentValues.put(DATE, date - date % 1000); contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
if (!snippet.isEmpty()) { if (!snippet.isEmpty()) {
contentValues.put(SNIPPET, snippet); contentValues.put(SNIPPET, snippet);
} }
@ -230,9 +222,7 @@ public class ThreadDatabase extends Database {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = ""; String where = "";
for (long threadId : threadIds) { for (long threadId : threadIds) { where += ID + " = '" + threadId + "' OR "; }
where += ID + " = '" + threadId + "' OR ";
}
where = where.substring(0, where.length() - 4); where = where.substring(0, where.length() - 4);
@ -358,7 +348,7 @@ public class ThreadDatabase extends Database {
public void setDistributionType(long threadId, int distributionType) { public void setDistributionType(long threadId, int distributionType) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(TYPE, distributionType); contentValues.put(DISTRIBUTION_TYPE, distributionType);
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""}); db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
@ -367,7 +357,7 @@ public class ThreadDatabase extends Database {
public void setDate(long threadId, long date) { public void setDate(long threadId, long date) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(DATE, date); contentValues.put(THREAD_CREATION_DATE, date);
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
int updated = db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""}); int updated = db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
if (updated > 0) notifyConversationListListeners(); if (updated > 0) notifyConversationListListeners();
@ -375,11 +365,11 @@ public class ThreadDatabase extends Database {
public int getDistributionType(long threadId) { public int getDistributionType(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{TYPE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null); Cursor cursor = db.query(TABLE_NAME, new String[]{DISTRIBUTION_TYPE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
try { try {
if (cursor != null && cursor.moveToNext()) { if (cursor != null && cursor.moveToNext()) {
return cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)); return cursor.getInt(cursor.getColumnIndexOrThrow(DISTRIBUTION_TYPE));
} }
return DistributionTypes.DEFAULT; return DistributionTypes.DEFAULT;
@ -469,7 +459,7 @@ public class ThreadDatabase extends Database {
Cursor cursor = null; Cursor cursor = null;
try { try {
String where = "SELECT " + DATE + " FROM " + TABLE_NAME + String where = "SELECT " + THREAD_CREATION_DATE + " FROM " + TABLE_NAME +
" LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME + " LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME +
" ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS + " ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS +
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
@ -477,7 +467,7 @@ public class ThreadDatabase extends Database {
" WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " + " WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " +
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " +
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " +
GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL ORDER BY " + DATE + " DESC LIMIT 1"; GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL ORDER BY " + THREAD_CREATION_DATE + " DESC LIMIT 1";
cursor = db.rawQuery(where, null); cursor = db.rawQuery(where, null);
if (cursor != null && cursor.moveToFirst()) if (cursor != null && cursor.moveToFirst())
@ -595,7 +585,7 @@ public class ThreadDatabase extends Database {
public Long getLastUpdated(long threadId) { public Long getLastUpdated(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{DATE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null); Cursor cursor = db.query(TABLE_NAME, new String[]{THREAD_CREATION_DATE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
try { try {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
@ -736,7 +726,7 @@ public class ThreadDatabase extends Database {
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
long count = mmsSmsDatabase.getConversationCount(threadId); long count = mmsSmsDatabase.getConversationCount(threadId);
boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && deleteThreadOnEmpty(threadId); boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && possibleToDeleteThreadOnEmpty(threadId);
if (count == 0 && shouldDeleteEmptyThread) { if (count == 0 && shouldDeleteEmptyThread) {
deleteThread(threadId); deleteThread(threadId);
@ -810,7 +800,7 @@ public class ThreadDatabase extends Database {
return setLastSeen(threadId, lastSeenTime); return setLastSeen(threadId, lastSeenTime);
} }
private boolean deleteThreadOnEmpty(long threadId) { private boolean possibleToDeleteThreadOnEmpty(long threadId) {
Recipient threadRecipient = getRecipientForThreadId(threadId); Recipient threadRecipient = getRecipientForThreadId(threadId);
return threadRecipient != null && !threadRecipient.isCommunityRecipient(); return threadRecipient != null && !threadRecipient.isCommunityRecipient();
} }
@ -855,7 +845,7 @@ public class ThreadDatabase extends Database {
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
" ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID +
" WHERE " + where + " WHERE " + where +
" ORDER BY " + TABLE_NAME + "." + IS_PINNED + " DESC, " + TABLE_NAME + "." + DATE + " DESC"; " ORDER BY " + TABLE_NAME + "." + IS_PINNED + " DESC, " + TABLE_NAME + "." + THREAD_CREATION_DATE + " DESC";
if (limit > 0) { if (limit > 0) {
query += " LIMIT " + limit; query += " LIMIT " + limit;
@ -900,7 +890,7 @@ public class ThreadDatabase extends Database {
public ThreadRecord getCurrent() { public ThreadRecord getCurrent() {
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID)); long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID));
int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE)); int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DISTRIBUTION_TYPE));
Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.ADDRESS))); Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.ADDRESS)));
Optional<RecipientSettings> settings; Optional<RecipientSettings> settings;
@ -916,7 +906,7 @@ public class ThreadDatabase extends Database {
Recipient recipient = Recipient.from(context, address, settings, groupRecord, true); Recipient recipient = Recipient.from(context, address, settings, groupRecord, true);
String body = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET)); String body = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET));
long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE)); long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.THREAD_CREATION_DATE));
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)); long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT)); int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT));
int unreadMentionCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_MENTION_COUNT)); int unreadMentionCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_MENTION_COUNT));
@ -934,7 +924,17 @@ public class ThreadDatabase extends Database {
readReceiptCount = 0; readReceiptCount = 0;
} }
return new ThreadRecord(body, snippetUri, recipient, date, count, MessageRecord lastMessage = null;
if (count > 0) {
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
long messageTimestamp = mmsSmsDatabase.getLastMessageTimestamp(threadId);
if (messageTimestamp > 0) {
lastMessage = mmsSmsDatabase.getMessageForTimestamp(messageTimestamp);
}
}
return new ThreadRecord(body, snippetUri, lastMessage, recipient, date, count,
unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type, unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type,
distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned); distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned);
} }

View File

@ -357,7 +357,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS);
executeStatements(db, AttachmentDatabase.CREATE_INDEXS); executeStatements(db, AttachmentDatabase.CREATE_INDEXS);
executeStatements(db, ThreadDatabase.CREATE_INDEXS); executeStatements(db, ThreadDatabase.CREATE_INDEXES);
executeStatements(db, DraftDatabase.CREATE_INDEXS); executeStatements(db, DraftDatabase.CREATE_INDEXS);
executeStatements(db, GroupDatabase.CREATE_INDEXS); executeStatements(db, GroupDatabase.CREATE_INDEXS);
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES); executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);

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

View File

@ -31,6 +31,7 @@ import org.session.libsession.messaging.utilities.UpdateMessageData;
import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.IdentityKeyMismatch;
import org.session.libsession.utilities.NetworkFailure; import org.session.libsession.utilities.NetworkFailure;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -120,7 +121,8 @@ public abstract class MessageRecord extends DisplayRecord {
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing())); return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
} else if (isExpirationTimerUpdate()) { } else if (isExpirationTimerUpdate()) {
int seconds = (int) (getExpiresIn() / 1000); int seconds = (int) (getExpiresIn() / 1000);
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getRecipient(), getIndividualRecipient().getAddress().serialize(), isOutgoing(), getTimestamp(), expireStarted)); boolean isGroup = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(getThreadId()).isGroupRecipient();
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, isGroup, getIndividualRecipient().getAddress().serialize(), isOutgoing(), getTimestamp(), expireStarted));
} else if (isDataExtractionNotification()) { } else if (isDataExtractionNotification()) {
if (isScreenshotNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize()))); if (isScreenshotNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize())));
else if (isMediaSavedNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize()))); else if (isMediaSavedNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize())));

View File

@ -43,6 +43,7 @@ import network.loki.messenger.R;
public class ThreadRecord extends DisplayRecord { public class ThreadRecord extends DisplayRecord {
private @Nullable final Uri snippetUri; private @Nullable final Uri snippetUri;
public @Nullable final MessageRecord lastMessage;
private final long count; private final long count;
private final int unreadCount; private final int unreadCount;
private final int unreadMentionCount; private final int unreadMentionCount;
@ -54,13 +55,14 @@ public class ThreadRecord extends DisplayRecord {
private final int initialRecipientHash; private final int initialRecipientHash;
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
@NonNull Recipient recipient, long date, long count, int unreadCount, @Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status, int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
long snippetType, int distributionType, boolean archived, long expiresIn, long snippetType, int distributionType, boolean archived, long expiresIn,
long lastSeen, int readReceiptCount, boolean pinned) long lastSeen, int readReceiptCount, boolean pinned)
{ {
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount); super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
this.snippetUri = snippetUri; this.snippetUri = snippetUri;
this.lastMessage = lastMessage;
this.count = count; this.count = count;
this.unreadCount = unreadCount; this.unreadCount = unreadCount;
this.unreadMentionCount = unreadMentionCount; this.unreadMentionCount = unreadMentionCount;

View File

@ -4,6 +4,8 @@ import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.text.SpannableString
import android.text.TextUtils
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
@ -89,7 +91,7 @@ class ConversationView : LinearLayout {
|| (configFactory.convoVolatile?.getConversationUnread(thread) == true) || (configFactory.convoVolatile?.getConversationUnread(thread) == true)
binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup) binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup)
val senderDisplayName = getUserDisplayName(thread.recipient) val senderDisplayName = getTitle(thread.recipient)
?: thread.recipient.address.toString() ?: thread.recipient.address.toString()
binding.conversationViewDisplayNameTextView.text = senderDisplayName binding.conversationViewDisplayNameTextView.text = senderDisplayName
binding.timestampTextView.text = thread.date.takeIf { it != 0L }?.let { DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), it) } binding.timestampTextView.text = thread.date.takeIf { it != 0L }?.let { DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), it) }
@ -101,9 +103,7 @@ class ConversationView : LinearLayout {
R.drawable.ic_notifications_mentions R.drawable.ic_notifications_mentions
} }
binding.muteIndicatorImageView.setImageResource(drawableRes) binding.muteIndicatorImageView.setImageResource(drawableRes)
val rawSnippet = thread.getDisplayBody(context) binding.snippetTextView.text = highlightMentions(thread.getSnippet(), thread.threadId, context)
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
binding.snippetTextView.text = snippet
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
if (isTyping) { if (isTyping) {
@ -131,12 +131,21 @@ class ConversationView : LinearLayout {
binding.profilePictureView.recycle() binding.profilePictureView.recycle()
} }
private fun getUserDisplayName(recipient: Recipient): String? { private fun getTitle(recipient: Recipient): String? = when {
return if (recipient.isLocalNumber) { recipient.isLocalNumber -> context.getString(R.string.note_to_self)
context.getString(R.string.note_to_self) else -> recipient.toShortString() // Internally uses the Contact API
} else {
recipient.toShortString() // Internally uses the Contact API
} }
private fun ThreadRecord.getSnippet(): CharSequence =
concatSnippet(getSnippetPrefix(), getDisplayBody(context))
private fun concatSnippet(prefix: CharSequence?, body: CharSequence): CharSequence =
prefix?.let { TextUtils.concat(it, ": ", body) } ?: body
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null
lastMessage?.isOutgoing == true -> resources.getString(R.string.MessageRecord_you)
else -> lastMessage?.individualRecipient?.toShortString()
} }
// endregion // endregion
} }

View File

@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.home.search
import android.content.Context import android.content.Context
import android.text.Editable import android.text.Editable
import android.text.InputFilter
import android.text.InputFilter.LengthFilter
import android.text.TextWatcher import android.text.TextWatcher
import android.util.AttributeSet import android.util.AttributeSet
import android.view.KeyEvent import android.view.KeyEvent
@ -34,6 +36,7 @@ class GlobalSearchInputLayout @JvmOverloads constructor(
binding.searchInput.onFocusChangeListener = this binding.searchInput.onFocusChangeListener = this
binding.searchInput.addTextChangedListener(this) binding.searchInput.addTextChangedListener(this)
binding.searchInput.setOnEditorActionListener(this) binding.searchInput.setOnEditorActionListener(this)
binding.searchInput.setFilters( arrayOf<InputFilter>(LengthFilter(100)) ) // 100 char search limit
binding.searchCancel.setOnClickListener(this) binding.searchCancel.setOnClickListener(this)
binding.searchClear.setOnClickListener(this) binding.searchClear.setOnClickListener(this)
} }

View File

@ -24,8 +24,7 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
private val executor = viewModelScope + SupervisorJob() private val executor = viewModelScope + SupervisorJob()
private val _result: MutableStateFlow<GlobalSearchResult> = private val _result: MutableStateFlow<GlobalSearchResult> = MutableStateFlow(GlobalSearchResult.EMPTY)
MutableStateFlow(GlobalSearchResult.EMPTY)
val result: StateFlow<GlobalSearchResult> = _result val result: StateFlow<GlobalSearchResult> = _result
@ -41,13 +40,14 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
_queryText _queryText
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) .buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
.mapLatest { query -> .mapLatest { query ->
if (query.trim().length < 2) { // Early exit on empty search query
if (query.trim().isEmpty()) {
SearchResult.EMPTY SearchResult.EMPTY
} else { } else {
// user input delay here in case we get a new query within a few hundred ms // User input delay in case we get a new query within a few hundred ms this
// this coroutine will be cancelled and expensive query will not be run if typing quickly // coroutine will be cancelled and the expensive query will not be run.
// first query of 2 characters will be instant however
delay(300) delay(300)
val settableFuture = SettableFuture<SearchResult>() val settableFuture = SettableFuture<SearchResult>()
searchRepository.query(query.toString(), settableFuture::set) searchRepository.query(query.toString(), settableFuture::set)
try { try {
@ -64,6 +64,4 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
} }
.launchIn(executor) .launchIn(executor)
} }
} }

View File

@ -61,11 +61,15 @@ class MarkReadReceiver : BroadcastReceiver() {
val mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase() val mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
val threadDb = DatabaseComponent.get(context).threadDatabase()
// start disappear after read messages except TimerUpdates in groups. // start disappear after read messages except TimerUpdates in groups.
markedReadMessages markedReadMessages
.filter { it.expiryType == ExpiryType.AFTER_READ } .filter { it.expiryType == ExpiryType.AFTER_READ }
.map { it.syncMessageId } .map { it.syncMessageId }
.filter { mmsSmsDatabase.getMessageForTimestamp(it.timetamp)?.run { isExpirationTimerUpdate && recipient.isClosedGroupRecipient } == false } .filter { mmsSmsDatabase.getMessageForTimestamp(it.timetamp)?.run {
isExpirationTimerUpdate && threadDb.getRecipientForThreadId(threadId)?.isGroupRecipient == true } == false
}
.forEach { messageExpirationManager.startDisappearAfterRead(it.timetamp, it.address.serialize()) } .forEach { messageExpirationManager.startDisappearAfterRead(it.timetamp, it.address.serialize()) }
hashToDisappearAfterReadMessage(context, markedReadMessages)?.let { hashToDisappearAfterReadMessage(context, markedReadMessages)?.let {

View File

@ -4,19 +4,14 @@ import network.loki.messenger.libsession_util.util.ExpiryMode
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import app.cash.copper.Query import app.cash.copper.Query
import app.cash.copper.flow.observeQuery import app.cash.copper.flow.observeQuery
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.MessageRequestResponse
@ -32,9 +27,7 @@ import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
@ -51,7 +44,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import javax.inject.Inject import javax.inject.Inject
interface ConversationRepository { interface ConversationRepository {
@ -239,7 +231,7 @@ class DefaultConversationRepository @Inject constructor(
.success { .success {
continuation.resume(ResultOf.Success(Unit)) continuation.resume(ResultOf.Success(Unit))
}.fail { error -> }.fail { error ->
Log.w("[onversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..") Log.w("ConversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..")
continuation.resumeWithException(error) continuation.resumeWithException(error)
} }
} }
@ -330,9 +322,7 @@ class DefaultConversationRepository @Inject constructor(
while (reader.next != null) { while (reader.next != null) {
deleteMessageRequest(reader.current) deleteMessageRequest(reader.current)
val recipient = reader.current.recipient val recipient = reader.current.recipient
if (block) { if (block) { setBlocked(recipient, true) }
setBlocked(recipient, true)
}
} }
} }
return ResultOf.Success(Unit) return ResultOf.Success(Unit)
@ -359,9 +349,7 @@ class DefaultConversationRepository @Inject constructor(
val cursor = mmsSmsDb.getConversation(threadId, true) val cursor = mmsSmsDb.getConversation(threadId, true)
mmsSmsDb.readerFor(cursor).use { reader -> mmsSmsDb.readerFor(cursor).use { reader ->
while (reader.next != null) { while (reader.next != null) {
if (!reader.current.isOutgoing) { if (!reader.current.isOutgoing) { return true }
return true
}
} }
} }
return false return false

View File

@ -4,12 +4,8 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.DatabaseUtils; import android.database.DatabaseUtils;
import android.database.MergeCursor; import android.database.MergeCursor;
import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.session.libsession.messaging.contacts.Contact; import org.session.libsession.messaging.contacts.Contact;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.GroupRecord; import org.session.libsession.utilities.GroupRecord;
@ -27,37 +23,25 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.search.model.MessageResult; import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.search.model.SearchResult; import org.thoughtcrime.securesms.search.model.SearchResult;
import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Stopwatch;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import kotlin.Pair; import kotlin.Pair;
/** // Class to manage data retrieval for search
* Manages data retrieval for search.
*/
public class SearchRepository { public class SearchRepository {
private static final String TAG = SearchRepository.class.getSimpleName(); private static final String TAG = SearchRepository.class.getSimpleName();
private static final Set<Character> BANNED_CHARACTERS = new HashSet<>(); private static final Set<Character> BANNED_CHARACTERS = new HashSet<>();
static { static {
// Several ranges of invalid ASCII characters // Construct a list containing several ranges of invalid ASCII characters
for (int i = 33; i <= 47; i++) { // See: https://www.ascii-code.com/
BANNED_CHARACTERS.add((char) i); for (int i = 33; i <= 47; i++) { BANNED_CHARACTERS.add((char) i); } // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /
} for (int i = 58; i <= 64; i++) { BANNED_CHARACTERS.add((char) i); } // :, ;, <, =, >, ?, @
for (int i = 58; i <= 64; i++) { for (int i = 91; i <= 96; i++) { BANNED_CHARACTERS.add((char) i); } // [, \, ], ^, _, `
BANNED_CHARACTERS.add((char) i); for (int i = 123; i <= 126; i++) { BANNED_CHARACTERS.add((char) i); } // {, |, }, ~
}
for (int i = 91; i <= 96; i++) {
BANNED_CHARACTERS.add((char) i);
}
for (int i = 123; i <= 126; i++) {
BANNED_CHARACTERS.add((char) i);
}
} }
private final Context context; private final Context context;
@ -86,35 +70,25 @@ public class SearchRepository {
} }
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) { public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
if (TextUtils.isEmpty(query)) { // If the sanitized search is empty then abort without search
String cleanQuery = sanitizeQuery(query).trim();
if (cleanQuery.isEmpty()) {
callback.onResult(SearchResult.EMPTY); callback.onResult(SearchResult.EMPTY);
return; return;
} }
executor.execute(() -> { executor.execute(() -> {
Stopwatch timer = new Stopwatch("FtsQuery"); Stopwatch timer = new Stopwatch("FtsQuery");
String cleanQuery = sanitizeQuery(query);
// If the search is for a single character and it was stripped by `sanitizeQuery` then abort
// the search for an empty string to avoid SQLite error.
if (cleanQuery.length() == 0)
{
Log.d(TAG, "Aborting empty search query.");
timer.stop(TAG);
return;
}
timer.split("clean"); timer.split("clean");
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery); Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
timer.split("contacts"); timer.split("Contacts");
CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond()); CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond());
timer.split("conversations"); timer.split("Conversations");
CursorList<MessageResult> messages = queryMessages(cleanQuery); CursorList<MessageResult> messages = queryMessages(cleanQuery);
timer.split("messages"); timer.split("Messages");
timer.stop(TAG); timer.stop(TAG);
@ -123,23 +97,20 @@ public class SearchRepository {
} }
public void query(@NonNull String query, long threadId, @NonNull Callback<CursorList<MessageResult>> callback) { public void query(@NonNull String query, long threadId, @NonNull Callback<CursorList<MessageResult>> callback) {
if (TextUtils.isEmpty(query)) { // If the sanitized search query is empty then abort the search
String cleanQuery = sanitizeQuery(query).trim();
if (cleanQuery.isEmpty()) {
callback.onResult(CursorList.emptyList()); callback.onResult(CursorList.emptyList());
return; return;
} }
executor.execute(() -> { executor.execute(() -> {
// If the sanitized search query is empty then abort the search to prevent SQLite errors.
String cleanQuery = sanitizeQuery(query).trim();
if (cleanQuery.isEmpty()) { return; }
CursorList<MessageResult> messages = queryMessages(cleanQuery, threadId); CursorList<MessageResult> messages = queryMessages(cleanQuery, threadId);
callback.onResult(messages); callback.onResult(messages);
}); });
} }
private Pair<CursorList<Contact>, List<String>> queryContacts(String query) { private Pair<CursorList<Contact>, List<String>> queryContacts(String query) {
Cursor contacts = contactDatabase.queryContactsByName(query); Cursor contacts = contactDatabase.queryContactsByName(query);
List<Address> contactList = new ArrayList<>(); List<Address> contactList = new ArrayList<>();
List<String> contactStrings = new ArrayList<>(); List<String> contactStrings = new ArrayList<>();
@ -166,7 +137,6 @@ public class SearchRepository {
MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients}); MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients});
return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings); return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings);
} }
private CursorList<GroupRecord> queryConversations(@NonNull String query, List<String> matchingAddresses) { private CursorList<GroupRecord> queryConversations(@NonNull String query, List<String> matchingAddresses) {
@ -189,9 +159,7 @@ public class SearchRepository {
membersGroupList.close(); membersGroupList.close();
} }
Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses)); Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses));
return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase)) return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase))
: CursorList.emptyList(); : CursorList.emptyList();
} }
@ -256,9 +224,7 @@ public class SearchRepository {
private final Context context; private final Context context;
RecipientModelBuilder(@NonNull Context context) { RecipientModelBuilder(@NonNull Context context) { this.context = context; }
this.context = context;
}
@Override @Override
public Recipient build(@NonNull Cursor cursor) { public Recipient build(@NonNull Cursor cursor) {
@ -301,9 +267,7 @@ public class SearchRepository {
private final Context context; private final Context context;
MessageModelBuilder(@NonNull Context context) { MessageModelBuilder(@NonNull Context context) { this.context = context; }
this.context = context;
}
@Override @Override
public MessageResult build(@NonNull Cursor cursor) { public MessageResult build(@NonNull Cursor cursor) {

View File

@ -151,8 +151,8 @@ class ExpiringMessageManager(context: Context) : MessageExpirationManagerProtoco
val userPublicKey = getLocalNumber(context) val userPublicKey = getLocalNumber(context)
val senderPublicKey = message.sender val senderPublicKey = message.sender
val sentTimestamp = if (message.sentTimestamp == null) 0 else message.sentTimestamp!! val sentTimestamp = message.sentTimestamp ?: 0
val expireStartedAt = if (expiryMode is AfterSend || message.isSenderSelf) sentTimestamp else 0 val expireStartedAt = if ((expiryMode is AfterSend || message.isSenderSelf) && !message.isGroup) sentTimestamp else 0
// Notify the user // Notify the user
if (senderPublicKey == null || userPublicKey == senderPublicKey) { if (senderPublicKey == null || userPublicKey == senderPublicKey) {

View File

@ -37,12 +37,10 @@ public class Stopwatch {
for (int i = 1; i < splits.size(); i++) { for (int i = 1; i < splits.size(); i++) {
out.append(splits.get(i).label).append(": "); out.append(splits.get(i).label).append(": ");
out.append(splits.get(i).time - splits.get(i - 1).time); out.append(splits.get(i).time - splits.get(i - 1).time);
out.append(" "); out.append("ms ");
} }
out.append("total: ").append(splits.get(splits.size() - 1).time - startTime).append("ms.");
out.append("total: ").append(splits.get(splits.size() - 1).time - startTime);
} }
Log.d(tag, out.toString()); Log.d(tag, out.toString());
} }

View File

@ -9,12 +9,15 @@ import android.graphics.Bitmap
import android.graphics.PointF import android.graphics.PointF
import android.graphics.Rect import android.graphics.Rect
import android.util.Size import android.util.Size
import android.util.TypedValue
import android.view.View import android.view.View
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.DimenRes import androidx.annotation.DimenRes
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.annotation.AttrRes
import androidx.annotation.ColorRes
import androidx.core.graphics.applyCanvas import androidx.core.graphics.applyCanvas
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -32,6 +35,20 @@ val View.hitRect: Rect
@ColorInt @ColorInt
fun Context.getAccentColor() = getColorFromAttr(R.attr.colorAccent) fun Context.getAccentColor() = getColorFromAttr(R.attr.colorAccent)
// Method to grab the appropriate attribute for a message colour.
// Note: This is an attribute, NOT a resource Id - see `getColorResourceIdFromAttr` for that.
@AttrRes
fun getMessageTextColourAttr(messageIsOutgoing: Boolean): Int =
if (messageIsOutgoing) R.attr.message_sent_text_color else R.attr.message_received_text_color
// Method to get an actual R.id.<SOME_COLOUR> resource Id from an attribute such as R.attr.message_sent_text_color etc.
@ColorRes
fun getColorResourceIdFromAttr(context: Context, attr: Int): Int {
val typedValue = TypedValue()
context.theme.resolveAttribute(attr, typedValue, true)
return typedValue.resourceId
}
fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) { fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) {
val startSize = resources.getDimension(startSizeID) val startSize = resources.getDimension(startSizeID)
val endSize = resources.getDimension(endSizeID) val endSize = resources.getDimension(endSizeID)
@ -70,7 +87,6 @@ fun View.hideKeyboard() {
imm.hideSoftInputFromWindow(this.windowToken, 0) imm.hideSoftInputFromWindow(this.windowToken, 0)
} }
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888, longestWidth: Int = 2000): Bitmap { fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888, longestWidth: Int = 2000): Bitmap {
val size = Size(measuredWidth, measuredHeight).coerceAtMost(longestWidth) val size = Size(measuredWidth, measuredHeight).coerceAtMost(longestWidth)
val scale = size.width / measuredWidth.toFloat() val scale = size.width / measuredWidth.toFloat()

View File

@ -4,7 +4,6 @@
<item android:id="@android:id/mask"> <item android:id="@android:id/mask">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="?android:textColorPrimary"/> <solid android:color="?android:textColorPrimary"/>
<corners android:radius="@dimen/medium_button_corner_radius" />
</shape> </shape>
</item> </item>
</ripple> </ripple>

View File

@ -4,7 +4,6 @@
<item android:id="@android:id/mask"> <item android:id="@android:id/mask">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="?android:textColorPrimary"/> <solid android:color="?android:textColorPrimary"/>
<corners android:radius="@dimen/medium_button_corner_radius" />
</shape> </shape>
</item> </item>
</ripple> </ripple>

View File

@ -6,8 +6,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical" android:orientation="vertical"
android:elevation="4dp" android:elevation="4dp">
android:padding="@dimen/medium_spacing">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -21,6 +20,8 @@
android:id="@+id/dialogDescriptionText" android:id="@+id/dialogDescriptionText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginTop="@dimen/large_spacing" android:layout_marginTop="@dimen/large_spacing"
android:text="@string/dialog_clear_all_data_message" android:text="@string/dialog_clear_all_data_message"
android:textAlignment="center" android:textAlignment="center"
@ -46,16 +47,15 @@
style="@style/Widget.Session.Button.Dialog.DestructiveText" style="@style/Widget.Session.Button.Dialog.DestructiveText"
android:id="@+id/clearAllDataButton" android:id="@+id/clearAllDataButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/dialog_button_height"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/dialog_clear_all_data_clear" /> android:text="@string/dialog_clear_all_data_clear" />
<Button <Button
style="@style/Widget.Session.Button.Dialog.UnimportantText" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/dialog_button_height"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/cancel" /> android:text="@string/cancel" />

View File

@ -38,7 +38,7 @@
style="@style/Widget.Session.Button.Dialog.UnimportantText" style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/dialog_button_height"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/cancel" /> android:text="@string/cancel" />
@ -46,7 +46,7 @@
style="@style/Widget.Session.Button.Dialog.DestructiveText" style="@style/Widget.Session.Button.Dialog.DestructiveText"
android:id="@+id/sendSeedButton" android:id="@+id/sendSeedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/small_button_height" android:layout_height="@dimen/dialog_button_height"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginStart="@dimen/medium_spacing" android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/dialog_send_seed_send_button_title" /> android:text="@string/dialog_send_seed_send_button_title" />

View File

@ -48,7 +48,7 @@
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
android:textSize="@dimen/very_small_font_size" android:textSize="@dimen/very_small_font_size"
android:textStyle="bold" android:textStyle="bold"
tools:text="@string/MessageRecord_you_disabled_disappearing_messages" /> tools:text="You disabled disappearing messages" />
<FrameLayout <FrameLayout
android:id="@+id/call_view" android:id="@+id/call_view"

View File

@ -11,6 +11,7 @@
<dimen name="massive_font_size">50sp</dimen> <dimen name="massive_font_size">50sp</dimen>
<!-- Element Sizes --> <!-- Element Sizes -->
<dimen name="dialog_button_height">60dp</dimen>
<dimen name="small_button_height">34dp</dimen> <dimen name="small_button_height">34dp</dimen>
<dimen name="medium_button_height">38dp</dimen> <dimen name="medium_button_height">38dp</dimen>
<dimen name="large_button_height">54dp</dimen> <dimen name="large_button_height">54dp</dimen>

View File

@ -119,6 +119,7 @@
<item name="android:textAllCaps">false</item> <item name="android:textAllCaps">false</item>
<item name="android:textSize">@dimen/small_font_size</item> <item name="android:textSize">@dimen/small_font_size</item>
<item name="android:textColor">?android:textColorPrimary</item> <item name="android:textColor">?android:textColorPrimary</item>
<item name="android:textStyle">bold</item>
</style> </style>
<style name="Widget.Session.Button.Dialog.UnimportantText"> <style name="Widget.Session.Button.Dialog.UnimportantText">

View File

@ -2,7 +2,7 @@ package org.session.libsession.messaging.mentions
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import java.util.* import java.util.Locale
object MentionsManager { object MentionsManager {
var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys

View File

@ -10,11 +10,11 @@ import org.session.libsession.messaging.calls.CallMessageType.CALL_INCOMING
import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED
import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.messages.ExpirationConfiguration.Companion.isNewConfigEnabled
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage.Kind.SCREENSHOT
import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.getExpirationTypeDisplayValue import org.session.libsession.utilities.getExpirationTypeDisplayValue
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.truncateIdForDisplay import org.session.libsession.utilities.truncateIdForDisplay
object UpdateMessageBuilder { object UpdateMessageBuilder {
@ -31,47 +31,35 @@ object UpdateMessageBuilder {
else getSenderName(senderId!!) else getSenderName(senderId!!)
return when (updateData) { return when (updateData) {
is UpdateMessageData.Kind.GroupCreation -> if (isOutgoing) { is UpdateMessageData.Kind.GroupCreation -> {
context.getString(R.string.MessageRecord_you_created_a_new_group) if (isOutgoing) context.getString(R.string.MessageRecord_you_created_a_new_group)
} else { else context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
} }
is UpdateMessageData.Kind.GroupNameChange -> if (isOutgoing) { is UpdateMessageData.Kind.GroupNameChange -> {
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name) if (isOutgoing) context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
} else { else context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
} }
is UpdateMessageData.Kind.GroupMemberAdded -> { is UpdateMessageData.Kind.GroupMemberAdded -> {
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName) val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
if (isOutgoing) { if (isOutgoing) context.getString(R.string.MessageRecord_you_added_s_to_the_group, members)
context.getString(R.string.MessageRecord_you_added_s_to_the_group, members) else context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
} else {
context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
}
} }
is UpdateMessageData.Kind.GroupMemberRemoved -> { is UpdateMessageData.Kind.GroupMemberRemoved -> {
val userPublicKey = storage.getUserPublicKey()!! val userPublicKey = storage.getUserPublicKey()!!
// 1st case: you are part of the removed members // 1st case: you are part of the removed members
return if (userPublicKey in updateData.updatedMembers) { return if (userPublicKey in updateData.updatedMembers) {
if (isOutgoing) { if (isOutgoing) context.getString(R.string.MessageRecord_left_group)
context.getString(R.string.MessageRecord_left_group) else context.getString(R.string.MessageRecord_you_were_removed_from_the_group)
} else {
context.getString(R.string.MessageRecord_you_were_removed_from_the_group)
}
} else { } else {
// 2nd case: you are not part of the removed members // 2nd case: you are not part of the removed members
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName) val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
if (isOutgoing) { if (isOutgoing) context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members)
context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members) else context.getString(R.string.MessageRecord_s_removed_s_from_the_group, senderName, members)
} else {
context.getString(R.string.MessageRecord_s_removed_s_from_the_group, senderName, members)
} }
} }
} is UpdateMessageData.Kind.GroupMemberLeft -> {
is UpdateMessageData.Kind.GroupMemberLeft -> if (isOutgoing) { if (isOutgoing) context.getString(R.string.MessageRecord_left_group)
context.getString(R.string.MessageRecord_left_group) else context.getString(R.string.ConversationItem_group_action_left, senderName)
} else {
context.getString(R.string.ConversationItem_group_action_left, senderName)
} }
else -> return "" else -> return ""
} }
@ -80,7 +68,7 @@ object UpdateMessageBuilder {
fun buildExpirationTimerMessage( fun buildExpirationTimerMessage(
context: Context, context: Context,
duration: Long, duration: Long,
recipient: Recipient, isGroup: Boolean,
senderId: String? = null, senderId: String? = null,
isOutgoing: Boolean = false, isOutgoing: Boolean = false,
timestamp: Long, timestamp: Long,
@ -89,44 +77,28 @@ object UpdateMessageBuilder {
if (!isOutgoing && senderId == null) return "" if (!isOutgoing && senderId == null) return ""
val senderName = if (isOutgoing) context.getString(R.string.MessageRecord_you) else getSenderName(senderId!!) val senderName = if (isOutgoing) context.getString(R.string.MessageRecord_you) else getSenderName(senderId!!)
return if (duration <= 0) { return if (duration <= 0) {
if (isOutgoing) { if (isOutgoing) context.getString(if (isGroup) R.string.MessageRecord_you_turned_off_disappearing_messages else R.string.MessageRecord_you_turned_off_disappearing_messages_1_on_1)
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages) else context.getString(if (isGroup) R.string.MessageRecord_s_turned_off_disappearing_messages else R.string.MessageRecord_s_turned_off_disappearing_messages_1_on_1, senderName)
else context.getString(if (recipient.is1on1) R.string.MessageRecord_you_turned_off_disappearing_messages_1_on_1 else R.string.MessageRecord_you_turned_off_disappearing_messages)
} else {
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, senderName)
else context.getString(if (recipient.is1on1) R.string.MessageRecord_s_turned_off_disappearing_messages_1_on_1 else R.string.MessageRecord_s_turned_off_disappearing_messages, senderName)
}
} else { } else {
val time = ExpirationUtil.getExpirationDisplayValue(context, duration.toInt()) val time = ExpirationUtil.getExpirationDisplayValue(context, duration.toInt())
val action = context.getExpirationTypeDisplayValue(timestamp == expireStarted) val action = context.getExpirationTypeDisplayValue(timestamp >= expireStarted)
if (isOutgoing) { if (isOutgoing) context.getString(
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time) if (isGroup) R.string.MessageRecord_you_set_messages_to_disappear_s_after_s else R.string.MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1,
else context.getString(
if (recipient.is1on1) R.string.MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1 else R.string.MessageRecord_you_set_messages_to_disappear_s_after_s,
time, time,
action action
) ) else context.getString(
} else { if (isGroup) R.string.MessageRecord_s_set_messages_to_disappear_s_after_s else R.string.MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1,
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, senderName, time)
else context.getString(
if (recipient.is1on1) R.string.MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1 else R.string.MessageRecord_s_set_messages_to_disappear_s_after_s,
senderName, senderName,
time, time,
action action
) )
} }
} }
}
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null): String { fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null) = when (kind) {
val senderName = getSenderName(senderId!!) SCREENSHOT -> R.string.MessageRecord_s_took_a_screenshot
return when (kind) { MEDIA_SAVED -> R.string.MessageRecord_media_saved_by_s
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT -> }.let { context.getString(it, getSenderName(senderId!!)) }
context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName)
DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED ->
context.getString(R.string.MessageRecord_media_saved_by_s, senderName)
}
}
fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String = fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String =
when (type) { when (type) {

View File

@ -27,6 +27,10 @@ public class ThemeUtil {
return getAttributeText(context, R.attr.theme_type, "light").equals("dark"); return getAttributeText(context, R.attr.theme_type, "light").equals("dark");
} }
public static boolean isLightTheme(@NonNull Context context) {
return getAttributeText(context, R.attr.theme_type, "light").equals("light");
}
public static boolean getThemedBoolean(@NonNull Context context, @AttrRes int attr) { public static boolean getThemedBoolean(@NonNull Context context, @AttrRes int attr) {
TypedValue typedValue = new TypedValue(); TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme(); Resources.Theme theme = context.getTheme();

View File

@ -15,10 +15,6 @@
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string> <string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
<string name="MessageRecord_called_s">Vous avez appelé %s</string> <string name="MessageRecord_called_s">Vous avez appelé %s</string>
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string> <string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
<string name="MessageRecord_you_disabled_disappearing_messages">Vous avez désactivé les messages éphémères.</string>
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string>
<string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini lexpiration des messages éphémères à %1$s</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini lexpiration des messages éphémères à %2$s</string>
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string> <string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string> <string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
<!-- expiration --> <!-- expiration -->

View File

@ -15,10 +15,6 @@
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string> <string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
<string name="MessageRecord_called_s">Vous avez appelé %s</string> <string name="MessageRecord_called_s">Vous avez appelé %s</string>
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string> <string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
<string name="MessageRecord_you_disabled_disappearing_messages">Vous avez désactivé les messages éphémères.</string>
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string>
<string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini lexpiration des messages éphémères à %1$s</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini lexpiration des messages éphémères à %2$s</string>
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string> <string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string> <string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
<!-- expiration --> <!-- expiration -->

View File

@ -17,17 +17,13 @@
<string name="MessageRecord_missed_call_from">Missed call from %s</string> <string name="MessageRecord_missed_call_from">Missed call from %s</string>
<string name="MessageRecord_follow_setting">Follow Setting</string> <string name="MessageRecord_follow_setting">Follow Setting</string>
<string name="AccessibilityId_follow_setting">Follow setting</string> <string name="AccessibilityId_follow_setting">Follow setting</string>
<string name="MessageRecord_you_disabled_disappearing_messages">You disabled disappearing messages.</string>
<string name="MessageRecord_you_turned_off_disappearing_messages">You have turned off disappearing messages.</string> <string name="MessageRecord_you_turned_off_disappearing_messages">You have turned off disappearing messages.</string>
<string name="MessageRecord_you_turned_off_disappearing_messages_1_on_1">You turned off disappearing messages. Messages you send will no longer disappear.</string> <string name="MessageRecord_you_turned_off_disappearing_messages_1_on_1">You turned off disappearing messages. Messages you send will no longer disappear.</string>
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s disabled disappearing messages.</string>
<string name="MessageRecord_s_turned_off_disappearing_messages">%1$s turned off disappearing messages.</string> <string name="MessageRecord_s_turned_off_disappearing_messages">%1$s turned off disappearing messages.</string>
<string name="MessageRecord_s_turned_off_disappearing_messages_1_on_1">%1$s has turned off disappearing messages. Messages they send will no longer disappear.</string> <string name="MessageRecord_s_turned_off_disappearing_messages_1_on_1">%1$s has turned off disappearing messages. Messages they send will no longer disappear.</string>
<string name="MessageRecord_you_set_disappearing_message_time_to_s">You set the disappearing message timer to %1$s</string>
<string name="MessageRecord_you_set_messages_to_disappear_s_after_s">You have set messages to disappear %1$s after they have been %2$s</string> <string name="MessageRecord_you_set_messages_to_disappear_s_after_s">You have set messages to disappear %1$s after they have been %2$s</string>
<string name="MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1">You set your messages to disappear %1$s after they have been %2$s.</string> <string name="MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1">You set your messages to disappear %1$s after they have been %2$s.</string>
<string name="MessageRecord_you_changed_messages_to_disappear_s_after_s">You have changed messages to disappear %1$s after they have been %2$s</string> <string name="MessageRecord_you_changed_messages_to_disappear_s_after_s">You have changed messages to disappear %1$s after they have been %2$s</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set the disappearing message timer to %2$s</string>
<string name="MessageRecord_s_set_messages_to_disappear_s_after_s">%1$s has set messages to disappear %2$s after they have been %3$s</string> <string name="MessageRecord_s_set_messages_to_disappear_s_after_s">%1$s has set messages to disappear %2$s after they have been %3$s</string>
<string name="MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1">%1$s has set their messages to disappear %2$s after they have been %3$s.</string> <string name="MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1">%1$s has set their messages to disappear %2$s after they have been %3$s.</string>
<string name="MessageRecord_s_changed_messages_to_disappear_s_after_s">%1$s has changed messages to disappear %2$s after they have been %3$s</string> <string name="MessageRecord_s_changed_messages_to_disappear_s_after_s">%1$s has changed messages to disappear %2$s after they have been %3$s</string>