diff --git a/app/build.gradle b/app/build.gradle
index 85a3206e6a..0d338ec9e2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,8 +31,8 @@ configurations.all {
exclude module: "commons-logging"
}
-def canonicalVersionCode = 369
-def canonicalVersionName = "1.18.1"
+def canonicalVersionCode = 371
+def canonicalVersionName = "1.18.2"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
@@ -41,6 +41,17 @@ def abiPostFix = ['armeabi-v7a' : 1,
'x86_64' : 4,
'universal' : 5]
+// Function to get the current git commit hash so we can embed it along w/ the build version.
+// Note: This is visible in the SettingsActivity, right at the bottom (R.id.versionTextView).
+def getGitHash = { ->
+ def stdout = new ByteArrayOutputStream()
+ exec {
+ commandLine "git", "rev-parse", "--short", "HEAD"
+ standardOutput = stdout
+ }
+ return stdout.toString().trim()
+}
+
android {
compileSdkVersion androidCompileSdkVersion
namespace 'network.loki.messenger'
@@ -94,6 +105,7 @@ android {
project.ext.set("archivesBaseName", "session")
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
+ buildConfigField "String", "GIT_HASH", "\"$getGitHash\""
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "USER_AGENT", "\"OWA\""
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4ff3d91f65..9cbede54f6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -41,6 +41,7 @@
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java
index 51f66ec323..c43d406575 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java
@@ -39,15 +39,20 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
public int getDesiredTheme() {
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
int userSelectedTheme = themeState.getTheme();
+
+ // If the user has configured Session to follow the system light/dark theme mode then do so..
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);
if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) {
return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark;
} else {
return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark;
}
- } else {
+ }
+ else // ..otherwise just return their selected theme.
+ {
return userSelectedTheme;
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt
index d5db6ae041..f6cbc7fddc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt
@@ -8,6 +8,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.Button
import android.widget.LinearLayout
import android.widget.LinearLayout.VERTICAL
+import android.widget.Space
import android.widget.TextView
import androidx.annotation.AttrRes
import androidx.annotation.LayoutRes
@@ -16,7 +17,6 @@ import androidx.annotation.StyleRes
import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat
import androidx.core.view.setMargins
-import androidx.core.view.setPadding
import androidx.core.view.updateMargins
import androidx.fragment.app.Fragment
import network.loki.messenger.R
@@ -31,13 +31,16 @@ class SessionDialogBuilder(val context: Context) {
private val dp20 = toPx(20, 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 var dialog: AlertDialog? = null
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)
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
private val buttonLayout = LinearLayout(context)
@@ -53,14 +56,14 @@ class SessionDialogBuilder(val context: Context) {
fun title(text: CharSequence?) = title(text?.toString())
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(text: CharSequence?, @StyleRes style: Int = 0) {
text(text, style) {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
- .apply { updateMargins(dp40, 0, dp40, dp20) }
+ .apply { updateMargins(dp40, 0, dp40, 0) }
}
}
@@ -72,6 +75,10 @@ class SessionDialogBuilder(val context: Context) {
textAlignment = View.TEXT_ALIGNMENT_CENTER
modify()
}.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)) }
@@ -126,8 +133,7 @@ class SessionDialogBuilder(val context: Context) {
) = Button(context, null, 0, style).apply {
setText(text)
contentDescription = resources.getString(contentDescriptionRes)
- layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1f)
- .apply { setMargins(toPx(20, resources)) }
+ layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
setOnClickListener {
listener.invoke()
if (dismiss) dismiss()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
index 198fc33372..afa6944645 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
@@ -93,6 +93,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
super.onNewIntent(intent)
if (intent?.action == ACTION_ANSWER) {
val answerIntent = WebRtcCallService.acceptCallIntent(this)
+ answerIntent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
ContextCompat.startForegroundService(this, answerIntent)
}
}
@@ -106,6 +107,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
setShowWhenLocked(true)
setTurnScreenOn(true)
}
+
window.addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt b/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt
index 31870e9b7a..69dec0cdd6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt
@@ -17,6 +17,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
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.MappingAdapter
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)
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 }
title.setText(item.title)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt
index cc968df398..ced4cc0035 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt
@@ -68,7 +68,7 @@ enum class ExpiryType(
AFTER_SEND(
ExpiryMode::AfterSend,
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
);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt
index 68e2f975c9..df2bc1c371 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt
@@ -35,11 +35,28 @@ class ContactListAdapter(
binding.profilePictureView.update(contact.recipient)
binding.nameTextView.text = contact.displayName
binding.root.setOnClickListener { listener(contact.recipient) }
+
+ // TODO: When we implement deleting contacts (hide might be safest for now) 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() {
- binding.profilePictureView.recycle()
- }
+ fun unbind() { binding.profilePictureView.recycle() }
}
class HeaderViewHolder(
@@ -52,15 +69,11 @@ class ContactListAdapter(
}
}
- override fun getItemCount(): Int {
- return items.size
- }
+ override fun getItemCount(): Int { return items.size }
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
- if (holder is ContactViewHolder) {
- holder.unbind()
- }
+ if (holder is ContactViewHolder) { holder.unbind() }
}
override fun getItemViewType(position: Int): Int {
@@ -72,13 +85,9 @@ class ContactListAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == ViewType.Contact) {
- ContactViewHolder(
- ViewContactBinding.inflate(LayoutInflater.from(context), parent, false)
- )
+ ContactViewHolder(ViewContactBinding.inflate(LayoutInflater.from(context), parent, false))
} else {
- HeaderViewHolder(
- ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false)
- )
+ HeaderViewHolder(ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false))
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
index 87d5ea4ae7..20a3884846 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
@@ -252,7 +252,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair())
}
private var actionMode: ActionMode? = null
- private var unreadCount = 0
+ private var unreadCount = Int.MAX_VALUE
// Attachments
private val audioRecorder = AudioRecorder(this)
private val stopAudioHandler = Handler(Looper.getMainLooper())
@@ -572,10 +572,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding!!.conversationRecyclerView.layoutManager = layoutManager
// Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will)
LoaderManager.getInstance(this).restartLoader(0, null, this)
- binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
- if (recyclerScrollState == RecyclerView.SCROLL_STATE_IDLE) {
+ // The unreadCount check is to prevent us scrolling to the bottom when we first enter a conversation
+ if (recyclerScrollState == RecyclerView.SCROLL_STATE_IDLE && unreadCount != Int.MAX_VALUE) {
scrollToMostRecentMessageIfWeShould()
}
handleRecyclerViewScrolled()
@@ -1251,6 +1252,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// `position` is the adapter position; not the visual position
private fun handleSwipeToReply(message: MessageRecord) {
+ if (message.isOpenGroupInvitation) return
val recipient = viewModel.recipient ?: return
binding?.inputBar?.draftQuote(recipient, message, glide)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
index d336c5fcce..af754a300a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
@@ -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)
// Reply
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)
}
// Copy message text
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
index 4f2d29edb1..00550c8d52 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
@@ -123,7 +123,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
AppTheme {
MessageDetails(
state = state,
- onReply = { setResultAndFinish(ON_REPLY) },
+ onReply = if (state.canReply) { { setResultAndFinish(ON_REPLY) } } else null,
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
onDelete = { setResultAndFinish(ON_DELETE) },
onClickImage = { viewModel.onClickImage(it) },
@@ -145,7 +145,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
@Composable
fun MessageDetails(
state: MessageDetailsState,
- onReply: () -> Unit = {},
+ onReply: (() -> Unit)? = null,
onResend: (() -> Unit)? = null,
onDelete: () -> Unit = {},
onClickImage: (Int) -> Unit = {},
@@ -214,18 +214,20 @@ fun CellMetadata(
@Composable
fun CellButtons(
- onReply: () -> Unit = {},
+ onReply: (() -> Unit)? = null,
onResend: (() -> Unit)? = null,
onDelete: () -> Unit = {},
) {
Cell {
Column {
- ItemButton(
- stringResource(R.string.reply),
- R.drawable.ic_message_details__reply,
- onClick = onReply
- )
- Divider()
+ onReply?.let {
+ ItemButton(
+ stringResource(R.string.reply),
+ R.drawable.ic_message_details__reply,
+ onClick = it
+ )
+ Divider()
+ }
onResend?.let {
ItemButton(
stringResource(R.string.resend),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt
index 4ebc1f27b3..ba153a6b36 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt
@@ -117,7 +117,7 @@ class MessageDetailsViewModel @Inject constructor(
Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide)
fun onClickImage(index: Int) {
- val state = state.value ?: return
+ val state = state.value
val mmsRecord = state.mmsRecord ?: return
val slide = mmsRecord.slideDeck.slides[index] ?: return
// only open to downloaded images
@@ -158,6 +158,7 @@ data class MessageDetailsState(
val thread: Recipient? = null,
) {
val fromTitle = GetString(R.string.message_details_header__from)
+ val canReply = record?.isOpenGroupInvitation != true
}
data class Attachment(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt
index 5959c41d16..3544f11b10 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt
@@ -140,6 +140,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
}
fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) {
+ quoteView?.let(binding.inputBarAdditionalContentContainer::removeView)
+
quote = message
// 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
// `updateLinkPreview` get called to update the existing View.
if (linkPreview != null && linkPreviewDraftView != null) return
-
+ linkPreviewDraftView?.let(binding.inputBarAdditionalContentContainer::removeView)
linkPreviewDraftView = LinkPreviewDraftView(context).also { it.delegate = this }
// Add the link preview View. Note: If there's already a quote View in the 'additional
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
index 2788d35dd2..21398c71aa 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
@@ -77,7 +77,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
// Reply
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 {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
index a59a102d1c..4e8a079024 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
@@ -37,6 +37,7 @@ import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.modifyLayoutParams
import org.session.libsignal.utilities.IdPrefix
+import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.database.LokiThreadDatabase
@@ -65,6 +66,7 @@ private const val TAG = "VisibleMessageView"
@AndroidEntryPoint
class VisibleMessageView : LinearLayout {
+ private var replyDisabled: Boolean = false
@Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
@Inject lateinit var lokiApiDb: LokiAPIDatabase
@@ -135,6 +137,7 @@ class VisibleMessageView : LinearLayout {
onAttachmentNeedsDownload: (Long, Long) -> Unit,
lastSentMessageId: Long
) {
+ replyDisabled = message.isOpenGroupInvitation
val threadID = message.threadId
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
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.isVisible = showDateBreak
- // Message status indicator
+ // Update message status indicator
showStatusMessage(message)
// Emoji Reactions
@@ -243,44 +246,101 @@ class VisibleMessageView : LinearLayout {
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) {
+ // 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` 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 {
gravity = if (message.isOutgoing) Gravity.END else Gravity.START
}
-
binding.statusContainer.modifyLayoutParams {
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) {
- val (iconID, iconColor, textId) = getMessageStatusImage(message)
- textId?.let(binding.messageStatusTextView::setText)
- iconColor?.let(binding.messageStatusTextView::setTextColor)
- iconID?.let { ContextCompat.getDrawable(context, it) }
- ?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
- ?.let(binding.messageStatusImageView::setImageDrawable)
+ // Set text & icons as appropriate for the message state. Note: Possible message states we care
+ // about are: isFailed, isSyncFailed, isPending, isSyncing, isResyncing, isRead, and isSent.
+ textId.let(binding.messageStatusTextView::setText)
+ iconColor?.let(binding.messageStatusTextView::setTextColor)
+ iconID?.let { ContextCompat.getDrawable(context, it) }
+ ?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
+ ?.let(binding.messageStatusImageView::setImageDrawable)
- // Always show the delivery status of the last sent message
- val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
- val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
- val isLastSentMessage = lastSentMessageId == message.id
+ // 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.
- binding.messageStatusTextView.isVisible = textId != null && (isLastSentMessage || scheduledToDisappear)
- val showTimer = scheduledToDisappear && !message.isPending
- binding.messageStatusImageView.isVisible = iconID != null && !showTimer && (!message.isSent || isLastSentMessage)
-
- binding.messageStatusImageView.bringToFront()
+ // ----- 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()
- binding.expirationTimerView.isVisible = showTimer
- if (showTimer) updateExpirationTimer(message)
- } else {
- binding.messageStatusTextView.isVisible = false
- binding.messageStatusImageView.isVisible = false
+ updateExpirationTimer(message)
+ return
+ }
+
+ // --- If we got here then we know the message is outgoing ---
+
+ val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
+ val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
+ val isLastSentMessage = lastSentMessageId == message.id
+
+ // ----- Case ii.) Message is outgoing but NOT scheduled to disappear -----
+ if (!scheduledToDisappear) {
+ // If this isn't a disappearing message then we never show the timer
+
+ // If the message has NOT been successfully sent then always show the delivery status text and icon..
+ val neitherSentNorRead = !(message.isSent || message.isRead)
+ if (neitherSentNorRead) {
+ binding.messageStatusTextView.isVisible = true
+ binding.messageStatusImageView.isVisible = true
+ } else {
+ // ..but if the message HAS been successfully sent or read then only display the delivery status
+ // text and image if this is the last sent message.
+ binding.messageStatusTextView.isVisible = isLastSentMessage
+ binding.messageStatusImageView.isVisible = isLastSentMessage
+ if (isLastSentMessage) { binding.messageStatusImageView.bringToFront() }
+ }
+ }
+ else // ----- Case iii.) Message is outgoing AND scheduled to disappear -----
+ {
+ // Always display the delivery status text on all outgoing disappearing messages
+ binding.messageStatusTextView.isVisible = true
+
+ // If the message is sent or has been read..
+ val sentOrRead = message.isSent || message.isRead
+ if (sentOrRead) {
+ // ..then display the timer icon for this disappearing message (but keep the message status icon hidden)
+ binding.expirationTimerView.isVisible = true
+ binding.expirationTimerView.bringToFront()
+ updateExpirationTimer(message)
+ } else {
+ // If the message has NOT been sent or read (or it has failed) then show the delivery status icon rather than the timer icon
+ binding.messageStatusImageView.isVisible = true
+ binding.messageStatusImageView.bringToFront()
+ }
}
}
@@ -302,10 +362,9 @@ class VisibleMessageView : LinearLayout {
@ColorInt val iconTint: Int?,
@StringRes val messageText: Int?)
- private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo = when {
+ private fun getMessageStatusInfo(message: MessageRecord): MessageStatusInfo = when {
message.isFailed ->
- MessageStatusInfo(
- R.drawable.ic_delivery_status_failed,
+ MessageStatusInfo(R.drawable.ic_delivery_status_failed,
resources.getColor(R.color.destructive, context.theme),
R.string.delivery_status_failed
)
@@ -318,24 +377,32 @@ class VisibleMessageView : LinearLayout {
message.isPending ->
MessageStatusInfo(
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(
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(
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(
R.drawable.ic_delivery_status_sent,
context.getColorFromAttr(R.attr.message_status_color),
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` suite of methods for options).
+ MessageStatusInfo(null, null, null)
+ }
}
private fun updateExpirationTimer(message: MessageRecord) {
@@ -409,6 +476,7 @@ class VisibleMessageView : LinearLayout {
} else {
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
}
+ if (replyDisabled) return
if (translationX > 0) { return } // Only allow swipes to the left
// The idea here is to asymptotically approach a maximum drag distance
val damping = 50.0f
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java
index 088685241c..76b95d7b17 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java
@@ -241,7 +241,21 @@ public class AttachmentManager {
}
public static void selectDocument(Activity activity, int requestCode) {
- selectMediaType(activity, "*/*", null, requestCode);
+ Permissions.PermissionsBuilder builder = Permissions.with(activity);
+
+ // The READ_EXTERNAL_STORAGE permission is deprecated (and will AUTO-FAIL if requested!) on
+ // Android 13 and above (API 33 - 'Tiramisu') we must ask for READ_MEDIA_VIDEO/IMAGES/AUDIO instead.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
+ .request(Manifest.permission.READ_MEDIA_IMAGES)
+ .request(Manifest.permission.READ_MEDIA_AUDIO);
+ } else {
+ builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
+ }
+ builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
+ .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
+ .onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this.
+ .execute();
}
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt
index 1ba4a0c3e5..cb9a19ffc1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt
@@ -1,21 +1,28 @@
package org.thoughtcrime.securesms.conversation.v2.utilities
+import android.app.Application
import android.content.Context
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableString
+import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.Range
+import androidx.appcompat.widget.ThemeUtils
import androidx.core.content.res.ResourcesCompat
import network.loki.messenger.R
import nl.komponents.kovenant.combine.Tuple2
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.utilities.SodiumUtilities
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.util.UiModeUtilities
import org.thoughtcrime.securesms.util.getAccentColor
+import org.thoughtcrime.securesms.util.getColorResourceIdFromAttr
+import org.thoughtcrime.securesms.util.getMessageTextColourAttr
import java.util.regex.Pattern
object MentionUtilities {
@@ -58,15 +65,37 @@ object MentionUtilities {
}
}
val result = SpannableString(text)
- val isLightMode = UiModeUtilities.isDayUiMode(context)
- val color = if (isOutgoingMessage) {
- ResourcesCompat.getColor(context.resources, if (isLightMode) R.color.white else R.color.black, context.theme)
- } else {
- context.getAccentColor()
+
+ var mentionTextColour: Int? = null
+ // In dark themes..
+ if (ThemeUtil.isDarkTheme(context)) {
+ // ..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) {
- 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)
+
+ // 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
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index 1bf1dcdb15..da4f39f0c1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -320,6 +320,19 @@ public class MmsSmsDatabase extends Database {
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() {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC";
String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0";
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
index 450e7c2d23..84b9441834 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -22,15 +22,11 @@ import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Pair;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
import com.annimon.stream.Stream;
-
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteStatement;
-
import org.apache.commons.lang3.StringUtils;
import org.session.libsession.messaging.calls.CallMessageType;
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.SmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
-
import java.io.Closeable;
import java.io.IOException;
import java.security.SecureRandom;
@@ -634,7 +629,7 @@ public class SmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(messageId);
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
notifyConversationListeners(threadId);
- boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
+ boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, false);
return threadDeleted;
}
@@ -701,12 +696,7 @@ public class SmsDatabase extends MessagingDatabase {
}
}
- /*package */void deleteThread(long threadId) {
- SQLiteDatabase db = databaseHelper.getWritableDatabase();
- db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
- }
-
- /*package*/void deleteMessagesInThreadBeforeDate(long threadId, long date) {
+ void deleteMessagesInThreadBeforeDate(long threadId, long date) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = THREAD_ID + " = ? AND (CASE " + TYPE;
@@ -719,7 +709,12 @@ public class SmsDatabase extends MessagingDatabase {
db.delete(TABLE_NAME, where, new String[] {threadId + ""});
}
- /*package*/ void deleteThreads(Set threadIds) {
+ void deleteThread(long threadId) {
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
+ }
+
+ void deleteThreads(Set threadIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = "";
@@ -727,23 +722,23 @@ public class SmsDatabase extends MessagingDatabase {
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);
}
- /*package */ void deleteAllThreads() {
+ void deleteAllThreads() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
}
- /*package*/ SQLiteDatabase beginTransaction() {
+ SQLiteDatabase beginTransaction() {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.beginTransaction();
return database;
}
- /*package*/ void endTransaction(SQLiteDatabase database) {
+ void endTransaction(SQLiteDatabase database) {
database.setTransactionSuccessful();
database.endTransaction();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
index 73dadda2cb..354ec05c46 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
@@ -99,7 +99,7 @@ private const val TAG = "Storage"
open class Storage(
context: Context,
helper: SQLCipherOpenHelper,
- private val configFactory: ConfigFactory
+ val configFactory: ConfigFactory
) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener {
override fun threadCreated(address: Address, threadId: Long) {
@@ -1371,29 +1371,29 @@ open class Storage(
val threadDB = DatabaseComponent.get(context).threadDatabase()
val groupDB = DatabaseComponent.get(context).groupDatabase()
threadDB.deleteConversation(threadID)
- val recipient = getRecipientForThread(threadID) ?: return
- when {
- recipient.isContactRecipient -> {
- if (recipient.isLocalNumber) return
- val contacts = configFactory.contacts ?: return
- contacts.upsertContact(recipient.address.serialize()) { priority = PRIORITY_HIDDEN }
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
- }
- recipient.isClosedGroupRecipient -> {
- // TODO: handle closed group
- val volatile = configFactory.convoVolatile ?: return
- val groups = configFactory.userGroups ?: return
- val groupID = recipient.address.toGroupString()
- val closedGroup = getGroup(groupID)
- val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
- if (closedGroup != null) {
- groupDB.delete(groupID) // TODO: Should we delete the group? (seems odd to leave it)
- volatile.eraseLegacyClosedGroup(groupPublicKey)
- groups.eraseLegacyGroup(groupPublicKey)
- } else {
- Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
- }
- }
+
+ val recipient = getRecipientForThread(threadID)
+ if (recipient == null) {
+ Log.w(TAG, "Got null recipient when deleting conversation - aborting.");
+ return
+ }
+
+ // 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 groups = configFactory.userGroups ?: return
+ val groupID = recipient.address.toGroupString()
+ val closedGroup = getGroup(groupID)
+ val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
+ if (closedGroup != null) {
+ groupDB.delete(groupID)
+ volatile.eraseLegacyClosedGroup(groupPublicKey)
+ groups.eraseLegacyGroup(groupPublicKey)
+ } else {
+ Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
index 507088a0ad..209e7f187d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -26,14 +26,10 @@ import android.content.Context;
import android.database.Cursor;
import android.database.MergeCursor;
import android.net.Uri;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
import com.annimon.stream.Stream;
-
import net.zetetic.database.sqlcipher.SQLiteDatabase;
-
import org.jetbrains.annotations.NotNull;
import org.session.libsession.snode.SnodeAPI;
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.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
-
import java.io.Closeable;
import java.util.Collections;
import java.util.HashMap;
@@ -83,7 +78,7 @@ public class ThreadDatabase extends Database {
public static final String TABLE_NAME = "thread";
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 ADDRESS = "recipient_ids";
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 UNREAD_COUNT = "unread_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";
public static final String SNIPPET_TYPE = "snippet_type";
public static final String SNIPPET_URI = "snippet_uri";
@@ -101,27 +96,27 @@ public class ThreadDatabase extends Database {
public static final String READ_RECEIPT_COUNT = "read_receipt_count";
public static final String EXPIRES_IN = "expires_in";
public static final String LAST_SEEN = "last_seen";
- public static final String HAS_SENT = "has_sent";
+ public static final String HAS_SENT = "has_sent";
public static final String IS_PINNED = "is_pinned";
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, " +
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, " +
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " 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 archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");",
};
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
};
@@ -131,8 +126,8 @@ public class ThreadDatabase extends Database {
private static final List COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION = Stream.concat(Stream.concat(Stream.of(TYPED_THREAD_PROJECTION),
Stream.of(RecipientDatabase.TYPED_RECIPIENT_PROJECTION)),
- Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
- .toList();
+ Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
+ .toList();
public static String getCreatePinnedCommand() {
return "ALTER TABLE "+ TABLE_NAME + " " +
@@ -158,11 +153,10 @@ public class ThreadDatabase extends Database {
ContentValues contentValues = new ContentValues(4);
long date = SnodeAPI.getNowWithOffset();
- contentValues.put(DATE, date - date % 1000);
+ contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
contentValues.put(ADDRESS, address.serialize());
- if (group)
- contentValues.put(TYPE, distributionType);
+ if (group) contentValues.put(DISTRIBUTION_TYPE, distributionType);
contentValues.put(MESSAGE_COUNT, 0);
@@ -175,7 +169,7 @@ public class ThreadDatabase extends Database {
long expiresIn, int readReceiptCount)
{
ContentValues contentValues = new ContentValues(7);
- contentValues.put(DATE, date - date % 1000);
+ contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
contentValues.put(MESSAGE_COUNT, count);
if (!body.isEmpty()) {
contentValues.put(SNIPPET, body);
@@ -187,9 +181,7 @@ public class ThreadDatabase extends Database {
contentValues.put(READ_RECEIPT_COUNT, readReceiptCount);
contentValues.put(EXPIRES_IN, expiresIn);
- if (unarchive) {
- contentValues.put(ARCHIVED, 0);
- }
+ if (unarchive) { contentValues.put(ARCHIVED, 0); }
SQLiteDatabase db = databaseHelper.getWritableDatabase();
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) {
ContentValues contentValues = new ContentValues(4);
- contentValues.put(DATE, date - date % 1000);
+ contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
if (!snippet.isEmpty()) {
contentValues.put(SNIPPET, snippet);
}
@@ -230,9 +222,7 @@ public class ThreadDatabase extends Database {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = "";
- for (long threadId : threadIds) {
- where += ID + " = '" + threadId + "' OR ";
- }
+ for (long threadId : threadIds) { where += ID + " = '" + threadId + "' OR "; }
where = where.substring(0, where.length() - 4);
@@ -358,7 +348,7 @@ public class ThreadDatabase extends Database {
public void setDistributionType(long threadId, int distributionType) {
ContentValues contentValues = new ContentValues(1);
- contentValues.put(TYPE, distributionType);
+ contentValues.put(DISTRIBUTION_TYPE, distributionType);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
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) {
ContentValues contentValues = new ContentValues(1);
- contentValues.put(DATE, date);
+ contentValues.put(THREAD_CREATION_DATE, date);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
int updated = db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
if (updated > 0) notifyConversationListListeners();
@@ -375,11 +365,11 @@ public class ThreadDatabase extends Database {
public int getDistributionType(long threadId) {
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 {
if (cursor != null && cursor.moveToNext()) {
- return cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
+ return cursor.getInt(cursor.getColumnIndexOrThrow(DISTRIBUTION_TYPE));
}
return DistributionTypes.DEFAULT;
@@ -469,7 +459,7 @@ public class ThreadDatabase extends Database {
Cursor cursor = null;
try {
- String where = "SELECT " + DATE + " FROM " + TABLE_NAME +
+ String where = "SELECT " + THREAD_CREATION_DATE + " FROM " + TABLE_NAME +
" LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME +
" ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS +
" 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 " +
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 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);
if (cursor != null && cursor.moveToFirst())
@@ -595,7 +585,7 @@ public class ThreadDatabase extends Database {
public Long getLastUpdated(long threadId) {
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 {
if (cursor != null && cursor.moveToFirst()) {
@@ -736,7 +726,7 @@ public class ThreadDatabase extends Database {
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
long count = mmsSmsDatabase.getConversationCount(threadId);
- boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && deleteThreadOnEmpty(threadId);
+ boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && possibleToDeleteThreadOnEmpty(threadId);
if (count == 0 && shouldDeleteEmptyThread) {
deleteThread(threadId);
@@ -810,7 +800,7 @@ public class ThreadDatabase extends Database {
return setLastSeen(threadId, lastSeenTime);
}
- private boolean deleteThreadOnEmpty(long threadId) {
+ private boolean possibleToDeleteThreadOnEmpty(long threadId) {
Recipient threadRecipient = getRecipientForThreadId(threadId);
return threadRecipient != null && !threadRecipient.isCommunityRecipient();
}
@@ -855,7 +845,7 @@ public class ThreadDatabase extends Database {
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
" ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID +
" 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) {
query += " LIMIT " + limit;
@@ -900,7 +890,7 @@ public class ThreadDatabase extends Database {
public ThreadRecord getCurrent() {
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)));
Optional settings;
@@ -916,7 +906,7 @@ public class ThreadDatabase extends Database {
Recipient recipient = Recipient.from(context, address, settings, groupRecord, true);
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));
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT));
int unreadMentionCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_MENTION_COUNT));
@@ -934,7 +924,17 @@ public class ThreadDatabase extends Database {
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,
distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index 7713043c2c..cd1988e83f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -357,7 +357,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
executeStatements(db, AttachmentDatabase.CREATE_INDEXS);
- executeStatements(db, ThreadDatabase.CREATE_INDEXS);
+ executeStatements(db, ThreadDatabase.CREATE_INDEXES);
executeStatements(db, DraftDatabase.CREATE_INDEXS);
executeStatements(db, GroupDatabase.CREATE_INDEXS);
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java
index 605118f3cc..639ea0db09 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java
@@ -78,8 +78,8 @@ public abstract class DisplayRecord {
public int getReadReceiptCount() { return readReceiptCount; }
public boolean isDelivered() {
- return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE
- && deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
+ return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE &&
+ deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
}
public boolean isSent() { return MmsSmsColumns.Types.isSentType(type); }
@@ -114,6 +114,11 @@ public abstract class DisplayRecord {
public boolean isOutgoing() {
return MmsSmsColumns.Types.isOutgoingMessageType(type);
}
+
+ public boolean isIncoming() {
+ return !MmsSmsColumns.Types.isOutgoingMessageType(type);
+ }
+
public boolean isGroupUpdateMessage() {
return SmsDatabase.Types.isGroupUpdateMessage(type);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
index b5b0aea20c..a61b78b4b6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
@@ -31,6 +31,7 @@ import org.session.libsession.messaging.utilities.UpdateMessageData;
import org.session.libsession.utilities.IdentityKeyMismatch;
import org.session.libsession.utilities.NetworkFailure;
import org.session.libsession.utilities.recipients.Recipient;
+import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import java.util.List;
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()));
} else if (isExpirationTimerUpdate()) {
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()) {
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())));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java
index f3e72a8747..0c023a8f29 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java
@@ -43,6 +43,7 @@ import network.loki.messenger.R;
public class ThreadRecord extends DisplayRecord {
private @Nullable final Uri snippetUri;
+ public @Nullable final MessageRecord lastMessage;
private final long count;
private final int unreadCount;
private final int unreadMentionCount;
@@ -54,13 +55,14 @@ public class ThreadRecord extends DisplayRecord {
private final int initialRecipientHash;
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,
long snippetType, int distributionType, boolean archived, long expiresIn,
long lastSeen, int readReceiptCount, boolean pinned)
{
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
this.snippetUri = snippetUri;
+ this.lastMessage = lastMessage;
this.count = count;
this.unreadCount = unreadCount;
this.unreadMentionCount = unreadMentionCount;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
index c876edb822..c9896a5b8e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
@@ -4,6 +4,8 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
+import android.text.SpannableString
+import android.text.TextUtils
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
@@ -89,7 +91,7 @@ class ConversationView : LinearLayout {
|| (configFactory.convoVolatile?.getConversationUnread(thread) == true)
binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup)
- val senderDisplayName = getUserDisplayName(thread.recipient)
+ val senderDisplayName = getTitle(thread.recipient)
?: thread.recipient.address.toString()
binding.conversationViewDisplayNameTextView.text = senderDisplayName
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
}
binding.muteIndicatorImageView.setImageResource(drawableRes)
- val rawSnippet = thread.getDisplayBody(context)
- val snippet = highlightMentions(rawSnippet, thread.threadId, context)
- binding.snippetTextView.text = snippet
+ binding.snippetTextView.text = highlightMentions(thread.getSnippet(), thread.threadId, context)
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
if (isTyping) {
@@ -131,12 +131,21 @@ class ConversationView : LinearLayout {
binding.profilePictureView.recycle()
}
- private fun getUserDisplayName(recipient: Recipient): String? {
- return if (recipient.isLocalNumber) {
- context.getString(R.string.note_to_self)
- } else {
- recipient.toShortString() // Internally uses the Contact API
- }
+ private fun getTitle(recipient: Recipient): String? = when {
+ recipient.isLocalNumber -> context.getString(R.string.note_to_self)
+ 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
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt
index 1537769cdc..c22ccde1f1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt
@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.home.search
import android.content.Context
import android.text.Editable
+import android.text.InputFilter
+import android.text.InputFilter.LengthFilter
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.KeyEvent
@@ -34,6 +36,7 @@ class GlobalSearchInputLayout @JvmOverloads constructor(
binding.searchInput.onFocusChangeListener = this
binding.searchInput.addTextChangedListener(this)
binding.searchInput.setOnEditorActionListener(this)
+ binding.searchInput.setFilters( arrayOf(LengthFilter(100)) ) // 100 char search limit
binding.searchCancel.setOnClickListener(this)
binding.searchClear.setOnClickListener(this)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt
index 8908554b03..1ff0a395fe 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt
@@ -24,8 +24,7 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
private val executor = viewModelScope + SupervisorJob()
- private val _result: MutableStateFlow =
- MutableStateFlow(GlobalSearchResult.EMPTY)
+ private val _result: MutableStateFlow = MutableStateFlow(GlobalSearchResult.EMPTY)
val result: StateFlow = _result
@@ -41,13 +40,14 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
_queryText
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
.mapLatest { query ->
- if (query.trim().length < 2) {
+ // Early exit on empty search query
+ if (query.trim().isEmpty()) {
SearchResult.EMPTY
} else {
- // user input delay here in case we get a new query within a few hundred ms
- // this coroutine will be cancelled and expensive query will not be run if typing quickly
- // first query of 2 characters will be instant however
+ // User input delay in case we get a new query within a few hundred ms this
+ // coroutine will be cancelled and the expensive query will not be run.
delay(300)
+
val settableFuture = SettableFuture()
searchRepository.query(query.toString(), settableFuture::set)
try {
@@ -64,6 +64,4 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
}
.launchIn(executor)
}
-
-
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
index 2c70bff637..b281e0798b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
@@ -349,11 +349,17 @@ public class DefaultMessageNotifier implements MessageNotifier {
builder.setThread(notifications.get(0).getRecipient());
builder.setMessageCount(notificationState.getMessageCount());
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(notifications.get(0).getThreadId(),context);
+
+ // TODO: Removing highlighting mentions in the notification because this context is the libsession one which
+ // TODO: doesn't have access to the `R.attr.message_sent_text_color` and `R.attr.message_received_text_color`
+ // TODO: attributes to perform the colour lookup. Also, it makes little sense to highlight the mentions using
+ // TODO: the app theme as it may result in insufficient contrast with the notification background which will
+ // TODO: be using the SYSTEM theme.
builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(),
- MentionUtilities.highlightMentions(text == null ? "" : text,
- notifications.get(0).getThreadId(),
- context),
+ //MentionUtilities.highlightMentions(text == null ? "" : text, notifications.get(0).getThreadId(), context), // Removing hightlighting mentions -ACL
+ text == null ? "" : text,
notifications.get(0).getSlideDeck());
+
builder.setContentIntent(notifications.get(0).getPendingIntent(context));
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
builder.setOnlyAlertOnce(!signal);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt
index 9f83726f46..59681c1f8a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt
@@ -61,11 +61,15 @@ class MarkReadReceiver : BroadcastReceiver() {
val mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
+ val threadDb = DatabaseComponent.get(context).threadDatabase()
+
// start disappear after read messages except TimerUpdates in groups.
markedReadMessages
.filter { it.expiryType == ExpiryType.AFTER_READ }
.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()) }
hashToDisappearAfterReadMessage(context, markedReadMessages)?.let {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
index 32b7e83cbb..91d90a5ef5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
@@ -38,6 +38,7 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.*
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.recipients.Recipient
+import org.session.libsignal.utilities.getProperty
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection
import org.thoughtcrime.securesms.components.ProfilePictureView
@@ -119,7 +120,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
passwordButton.isGone = prefs.getHidePassword()
passwordButton.setOnClickListener { showPassword() }
clearAllDataButton.setOnClickListener { clearAllData() }
- versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
+
+ val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6)
+ versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars)")
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
index 384f47d4d6..8a7a2dfd0f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
@@ -4,19 +4,14 @@ import network.loki.messenger.libsession_util.util.ExpiryMode
import android.content.ContentResolver
import android.content.Context
-
import app.cash.copper.Query
import app.cash.copper.flow.observeQuery
-
import dagger.hilt.android.qualifiers.ApplicationContext
-
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
-
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
-
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.Destination
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.recipients.Recipient
import org.session.libsignal.utilities.Log
-
import org.session.libsignal.utilities.toHexString
-
import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.DraftDatabase
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.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
-
import javax.inject.Inject
interface ConversationRepository {
@@ -239,7 +231,7 @@ class DefaultConversationRepository @Inject constructor(
.success {
continuation.resume(ResultOf.Success(Unit))
}.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)
}
}
@@ -330,9 +322,7 @@ class DefaultConversationRepository @Inject constructor(
while (reader.next != null) {
deleteMessageRequest(reader.current)
val recipient = reader.current.recipient
- if (block) {
- setBlocked(recipient, true)
- }
+ if (block) { setBlocked(recipient, true) }
}
}
return ResultOf.Success(Unit)
@@ -359,9 +349,7 @@ class DefaultConversationRepository @Inject constructor(
val cursor = mmsSmsDb.getConversation(threadId, true)
mmsSmsDb.readerFor(cursor).use { reader ->
while (reader.next != null) {
- if (!reader.current.isOutgoing) {
- return true
- }
+ if (!reader.current.isOutgoing) { return true }
}
}
return false
diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java
index a5669aa351..f2adbf2349 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java
@@ -4,12 +4,8 @@ import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MergeCursor;
-import android.text.TextUtils;
-
import androidx.annotation.NonNull;
-
import com.annimon.stream.Stream;
-
import org.session.libsession.messaging.contacts.Contact;
import org.session.libsession.utilities.Address;
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.SearchResult;
import org.thoughtcrime.securesms.util.Stopwatch;
-
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
-
import kotlin.Pair;
-/**
- * Manages data retrieval for search.
- */
+// Class to manage data retrieval for search
public class SearchRepository {
-
private static final String TAG = SearchRepository.class.getSimpleName();
private static final Set BANNED_CHARACTERS = new HashSet<>();
static {
- // Several ranges of invalid ASCII characters
- 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 = 91; i <= 96; i++) {
- BANNED_CHARACTERS.add((char) i);
- }
- for (int i = 123; i <= 126; i++) {
- BANNED_CHARACTERS.add((char) i);
- }
+ // Construct a list containing several ranges of invalid ASCII characters
+ // See: https://www.ascii-code.com/
+ 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 = 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;
@@ -86,35 +70,25 @@ public class SearchRepository {
}
public void query(@NonNull String query, @NonNull Callback 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);
return;
}
executor.execute(() -> {
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");
Pair, List> contacts = queryContacts(cleanQuery);
- timer.split("contacts");
+ timer.split("Contacts");
CursorList conversations = queryConversations(cleanQuery, contacts.getSecond());
- timer.split("conversations");
+ timer.split("Conversations");
CursorList messages = queryMessages(cleanQuery);
- timer.split("messages");
+ timer.split("Messages");
timer.stop(TAG);
@@ -123,23 +97,20 @@ public class SearchRepository {
}
public void query(@NonNull String query, long threadId, @NonNull Callback> 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());
return;
}
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 messages = queryMessages(cleanQuery, threadId);
callback.onResult(messages);
});
}
private Pair, List> queryContacts(String query) {
-
Cursor contacts = contactDatabase.queryContactsByName(query);
List contactList = new ArrayList<>();
List contactStrings = new ArrayList<>();
@@ -166,11 +137,10 @@ public class SearchRepository {
MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients});
return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings);
-
}
private CursorList queryConversations(@NonNull String query, List matchingAddresses) {
- List numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
+ List numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
String localUserNumber = TextSecurePreferences.getLocalNumber(context);
if (localUserNumber != null) {
matchingAddresses.remove(localUserNumber);
@@ -189,9 +159,7 @@ public class SearchRepository {
membersGroupList.close();
}
-
Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses));
-
return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase))
: CursorList.emptyList();
}
@@ -256,9 +224,7 @@ public class SearchRepository {
private final Context context;
- RecipientModelBuilder(@NonNull Context context) {
- this.context = context;
- }
+ RecipientModelBuilder(@NonNull Context context) { this.context = context; }
@Override
public Recipient build(@NonNull Cursor cursor) {
@@ -301,9 +267,7 @@ public class SearchRepository {
private final Context context;
- MessageModelBuilder(@NonNull Context context) {
- this.context = context;
- }
+ MessageModelBuilder(@NonNull Context context) { this.context = context; }
@Override
public MessageResult build(@NonNull Cursor cursor) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt
index c1dff74333..2f6ad7fd8b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.kt
@@ -151,8 +151,8 @@ class ExpiringMessageManager(context: Context) : MessageExpirationManagerProtoco
val userPublicKey = getLocalNumber(context)
val senderPublicKey = message.sender
- val sentTimestamp = if (message.sentTimestamp == null) 0 else message.sentTimestamp!!
- val expireStartedAt = if (expiryMode is AfterSend || message.isSenderSelf) sentTimestamp else 0
+ val sentTimestamp = message.sentTimestamp ?: 0
+ val expireStartedAt = if ((expiryMode is AfterSend || message.isSenderSelf) && !message.isGroup) sentTimestamp else 0
// Notify the user
if (senderPublicKey == null || userPublicKey == senderPublicKey) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt
index 3ae3d30f01..cfe1f38f58 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt
@@ -182,9 +182,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
}
fun broadcastWantsToAnswer(context: Context, wantsToAnswer: Boolean) {
- val intent = Intent(ACTION_WANTS_TO_ANSWER)
- .putExtra(EXTRA_WANTS_TO_ANSWER, wantsToAnswer)
-
+ val intent = Intent(ACTION_WANTS_TO_ANSWER).putExtra(EXTRA_WANTS_TO_ANSWER, wantsToAnswer)
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
}
@@ -506,9 +504,9 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
}
private fun handleAnswerCall(intent: Intent) {
- val recipient = callManager.recipient ?: return
- val pending = callManager.pendingOffer ?: return
- val callId = callManager.callId ?: return
+ val recipient = callManager.recipient ?: return Log.e(TAG, "No recipient to answer in handleAnswerCall")
+ val pending = callManager.pendingOffer ?: return Log.e(TAG, "No pending offer in handleAnswerCall")
+ val callId = callManager.callId ?: return Log.e(TAG, "No callId in handleAnswerCall")
val timestamp = callManager.pendingOfferTime
if (callManager.currentConnectionState != CallState.RemoteRing) {
@@ -526,9 +524,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
insertMissedCall(recipient, true)
terminate()
}
- if (didHangup) {
- return
- }
+ if (didHangup) { return }
}
callManager.postConnectionEvent(Event.SendAnswer) {
@@ -686,7 +682,6 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
private fun registerPowerButtonReceiver() {
if (powerButtonReceiver == null) {
powerButtonReceiver = PowerButtonReceiver()
-
registerReceiver(powerButtonReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF))
}
}
@@ -719,7 +714,6 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
}
}
-
private fun handleCheckTimeout(intent: Intent) {
val callId = callManager.callId ?: return
val callState = callManager.currentConnectionState
@@ -746,9 +740,9 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
}
if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) {
- // start an intent for the fullscreen
+ // Start an intent for the fullscreen call activity
val foregroundIntent = Intent(this, WebRtcCallActivity::class.java)
- .setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_BROUGHT_TO_FRONT)
+ .setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_BROUGHT_TO_FRONT or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
.setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
startActivity(foregroundIntent)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java b/app/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java
index cac53899fb..d92fc7546d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java
@@ -37,12 +37,10 @@ public class Stopwatch {
for (int i = 1; i < splits.size(); i++) {
out.append(splits.get(i).label).append(": ");
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);
+ out.append("total: ").append(splits.get(splits.size() - 1).time - startTime).append("ms.");
}
-
Log.d(tag, out.toString());
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt
index dfd4ffe419..c0477825fd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt
@@ -9,13 +9,17 @@ import android.graphics.Bitmap
import android.graphics.PointF
import android.graphics.Rect
import android.util.Size
+import android.util.TypedValue
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.DimenRes
import network.loki.messenger.R
import org.session.libsession.utilities.getColorFromAttr
import android.view.inputmethod.InputMethodManager
+import androidx.annotation.AttrRes
+import androidx.annotation.ColorRes
import androidx.core.graphics.applyCanvas
+import org.session.libsignal.utilities.Log
import kotlin.math.roundToInt
fun View.contains(point: PointF): Boolean {
@@ -32,6 +36,24 @@ val View.hitRect: Rect
@ColorInt
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 {
+ return if (messageIsOutgoing) R.attr.message_sent_text_color else R.attr.message_received_text_color
+}
+
+// Method to get an actual R.id. resource Id from an attribute such as R.attr.message_sent_text_color etc.
+@ColorRes
+fun getColorResourceIdFromAttr(context: Context, attr: Int): Int {
+ val outTypedValue = TypedValue()
+ val successfullyFoundAttribute = context.theme.resolveAttribute(attr, outTypedValue, true)
+ if (successfullyFoundAttribute) { return outTypedValue.resourceId }
+
+ Log.w("ViewUtils", "Could not find colour attribute $attr in theme - using grey as a safe fallback")
+ return R.color.gray50
+}
+
fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) {
val startSize = resources.getDimension(startSizeID)
val endSize = resources.getDimension(endSizeID)
@@ -70,7 +92,6 @@ fun View.hideKeyboard() {
imm.hideSoftInputFromWindow(this.windowToken, 0)
}
-
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888, longestWidth: Int = 2000): Bitmap {
val size = Size(measuredWidth, measuredHeight).coerceAtMost(longestWidth)
val scale = size.width / measuredWidth.toFloat()
diff --git a/app/src/main/res/drawable/destructive_dialog_text_button_background.xml b/app/src/main/res/drawable/destructive_dialog_text_button_background.xml
index f3e13c8000..3ba98c4992 100644
--- a/app/src/main/res/drawable/destructive_dialog_text_button_background.xml
+++ b/app/src/main/res/drawable/destructive_dialog_text_button_background.xml
@@ -4,7 +4,6 @@
-
-
diff --git a/app/src/main/res/drawable/unimportant_dialog_text_button_background.xml b/app/src/main/res/drawable/unimportant_dialog_text_button_background.xml
index f3e13c8000..3ba98c4992 100644
--- a/app/src/main/res/drawable/unimportant_dialog_text_button_background.xml
+++ b/app/src/main/res/drawable/unimportant_dialog_text_button_background.xml
@@ -4,7 +4,6 @@
-
-
diff --git a/app/src/main/res/layout/dialog_clear_all_data.xml b/app/src/main/res/layout/dialog_clear_all_data.xml
index db95647dad..4ef5c40e8e 100644
--- a/app/src/main/res/layout/dialog_clear_all_data.xml
+++ b/app/src/main/res/layout/dialog_clear_all_data.xml
@@ -6,8 +6,7 @@
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
- android:elevation="4dp"
- android:padding="@dimen/medium_spacing">
+ android:elevation="4dp">
diff --git a/app/src/main/res/layout/dialog_send_seed.xml b/app/src/main/res/layout/dialog_send_seed.xml
index 1aed6f0428..725c9c4d83 100644
--- a/app/src/main/res/layout/dialog_send_seed.xml
+++ b/app/src/main/res/layout/dialog_send_seed.xml
@@ -38,7 +38,7 @@
style="@style/Widget.Session.Button.Dialog.UnimportantText"
android:id="@+id/cancelButton"
android:layout_width="0dp"
- android:layout_height="@dimen/small_button_height"
+ android:layout_height="@dimen/dialog_button_height"
android:layout_weight="1"
android:text="@string/cancel" />
@@ -46,7 +46,7 @@
style="@style/Widget.Session.Button.Dialog.DestructiveText"
android:id="@+id/sendSeedButton"
android:layout_width="0dp"
- android:layout_height="@dimen/small_button_height"
+ android:layout_height="@dimen/dialog_button_height"
android:layout_weight="1"
android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/dialog_send_seed_send_button_title" />
diff --git a/app/src/main/res/layout/view_control_message.xml b/app/src/main/res/layout/view_control_message.xml
index 3cbe26048e..56b0c91867 100644
--- a/app/src/main/res/layout/view_control_message.xml
+++ b/app/src/main/res/layout/view_control_message.xml
@@ -48,7 +48,7 @@
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/very_small_font_size"
android:textStyle="bold"
- tools:text="@string/MessageRecord_you_disabled_disappearing_messages" />
+ tools:text="You disabled disappearing messages" />
مسدود
ذخیره
یادداشت به خود
- نسخه
+ %s نسخه
پیام جدید
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 4eb1d37fd1..6471f73e52 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -11,6 +11,7 @@
50sp
+ 60dp
34dp
38dp
54dp
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index e3e552f496..5b27ef5eac 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -119,6 +119,7 @@
- false
- @dimen/small_font_size
- ?android:textColorPrimary
+ - bold