diff --git a/.run/Run Tests.run.xml b/.run/Run Tests.run.xml
new file mode 100644
index 0000000000..42b2e07744
--- /dev/null
+++ b/.run/Run Tests.run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+ false
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 723d50c758..17eaebf5fe 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Session Android
+# Session Android
[Download on the Google Play Store](https://getsession.org/android)
diff --git a/app/build.gradle b/app/build.gradle
index 1e63207bf9..9e231c1bec 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 = 370
+def canonicalVersionName = "1.18.2"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c89fca8a70..79d55b37f8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -41,6 +41,7 @@
+
@@ -106,11 +107,6 @@
android:name="org.thoughtcrime.securesms.onboarding.RegisterActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
-
+ android:theme="@style/Theme.Session.DayNight.NoActionBar"
+ android:windowSoftInputMode="adjustResize" >
+
Unit) {
text ?: return
TextView(context, null, 0, style)
@@ -73,6 +74,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 view(view: View) = contentView.addView(view)
@@ -125,8 +130,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/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt
index 3fd91f7f63..20dc2bbade 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt
@@ -184,16 +184,21 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
override fun deleteMessage(messageID: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
+
messagingDatabase.deleteMessage(messageID)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms)
}
override fun deleteMessages(messageIDs: List, threadId: Long, isSms: Boolean) {
+
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
+ // Perform local delete
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
+
+ // Perform online delete
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms)
}
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 b87eac12c4..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
@@ -334,6 +336,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
if (isEnabled) {
viewModel.localRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
+
+ // Mirror the video preview of the person making the call to prevent disorienting them
+ surfaceView.setMirror(true)
+
binding.localRenderer.addView(surfaceView)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
index 7635ad40e3..9a5eb730de 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
@@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
-import androidx.annotation.DimenRes
import com.bumptech.glide.load.engine.DiskCacheStrategy
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewProfilePictureBinding
@@ -33,13 +32,12 @@ class ProfilePictureView @JvmOverloads constructor(
var additionalDisplayName: String? = null
var isLarge = false
- private val profilePicturesCache = mutableMapOf()
+ private val profilePicturesCache = mutableMapOf()
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
-
// endregion
constructor(context: Context, sender: Recipient): this(context) {
@@ -90,8 +88,8 @@ class ProfilePictureView @JvmOverloads constructor(
val publicKey = publicKey ?: return
val additionalPublicKey = additionalPublicKey
if (additionalPublicKey != null) {
- setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
- setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
+ setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName)
+ setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName)
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
} else {
glide.clear(binding.doubleModeImageView1)
@@ -99,14 +97,14 @@ class ProfilePictureView @JvmOverloads constructor(
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
}
if (additionalPublicKey == null && !isLarge) {
- setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size)
+ setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName)
binding.singleModeImageView.visibility = View.VISIBLE
} else {
glide.clear(binding.singleModeImageView)
binding.singleModeImageView.visibility = View.INVISIBLE
}
if (additionalPublicKey == null && isLarge) {
- setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size)
+ setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName)
binding.largeSingleModeImageView.visibility = View.VISIBLE
} else {
glide.clear(binding.largeSingleModeImageView)
@@ -114,17 +112,19 @@ class ProfilePictureView @JvmOverloads constructor(
}
}
- private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
+ private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) {
if (publicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
- if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
+ if (profilePicturesCache[imageView] == recipient) return
+ profilePicturesCache[imageView] = recipient
val signalProfilePicture = recipient.contactPhoto
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
+ glide.clear(imageView)
+
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
- glide.clear(imageView)
glide.load(signalProfilePicture)
.placeholder(unknownRecipientDrawable)
.centerCrop()
@@ -132,21 +132,19 @@ class ProfilePictureView @JvmOverloads constructor(
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(imageView)
- } else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) {
+ } else if (recipient.isCommunityRecipient && recipient.groupAvatarId == null) {
glide.clear(imageView)
glide.load(unknownOpenGroupDrawable)
.centerCrop()
.circleCrop()
.into(imageView)
} else {
- glide.clear(imageView)
glide.load(placeholder)
.placeholder(unknownRecipientDrawable)
.centerCrop()
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
}
- profilePicturesCache[publicKey] = recipient.profileAvatar
} else {
glide.load(unknownRecipientDrawable)
.centerCrop()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java b/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java
index 8a56acd658..9032b26a2b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components;
-
import android.animation.Animator;
import android.content.Context;
import android.os.Build;
@@ -68,9 +67,7 @@ public class SearchToolbar extends LinearLayout {
}
@Override
- public boolean onQueryTextChange(String newText) {
- return onQueryTextSubmit(newText);
- }
+ public boolean onQueryTextChange(String newText) { return onQueryTextSubmit(newText); }
});
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java
index 6cc39c8d14..0e2de9068c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java
@@ -3,133 +3,95 @@ package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
-import android.os.AsyncTask;
import android.preference.PreferenceManager;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
-import com.fasterxml.jackson.databind.type.CollectionType;
-import com.fasterxml.jackson.databind.type.TypeFactory;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
+import java.util.LinkedList;
import java.util.List;
import network.loki.messenger.R;
public class RecentEmojiPageModel implements EmojiPageModel {
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
- private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji2";
- private static final int EMOJI_LRU_SIZE = 50;
- public static final String KEY = "Recents";
- public static final List DEFAULT_REACTIONS_LIST =
- Arrays.asList("\ud83d\ude02", "\ud83e\udd70", "\ud83d\ude22", "\ud83d\ude21", "\ud83d\ude2e", "\ud83d\ude08");
+ public static final String RECENT_EMOJIS_KEY = "Recents";
- private final SharedPreferences prefs;
- private final LinkedHashSet recentlyUsed;
+ public static final LinkedList DEFAULT_REACTION_EMOJIS_LIST = new LinkedList<>(Arrays.asList(
+ "\ud83d\ude02",
+ "\ud83e\udd70",
+ "\ud83d\ude22",
+ "\ud83d\ude21",
+ "\ud83d\ude2e",
+ "\ud83d\ude08"));
+
+ public static final String DEFAULT_REACTION_EMOJIS_JSON_STRING = JsonUtil.toJson(new LinkedList<>(DEFAULT_REACTION_EMOJIS_LIST));
+ private static SharedPreferences prefs;
+ private static LinkedList recentlyUsed;
public RecentEmojiPageModel(Context context) {
- this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
- this.recentlyUsed = getPersistedCache();
- }
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
- private LinkedHashSet getPersistedCache() {
- String serialized = prefs.getString(EMOJI_LRU_PREFERENCE, "[]");
- try {
- CollectionType collectionType = TypeFactory.defaultInstance()
- .constructCollectionType(LinkedHashSet.class, String.class);
- return JsonUtil.getMapper().readValue(serialized, collectionType);
- } catch (IOException e) {
- Log.w(TAG, e);
- return new LinkedHashSet<>();
- }
+ // Note: Do NOT try to populate or update the persisted recent emojis in the constructor - the
+ // `getEmoji` method ends up getting called half-way through in a race-condition manner.
}
@Override
- public String getKey() {
- return KEY;
- }
+ public String getKey() { return RECENT_EMOJIS_KEY; }
- @Override public int getIconAttr() {
- return R.attr.emoji_category_recent;
- }
+ @Override public int getIconAttr() { return R.attr.emoji_category_recent; }
@Override public List getEmoji() {
- List recent = new ArrayList<>(recentlyUsed);
- List out = new ArrayList<>(DEFAULT_REACTIONS_LIST.size());
-
- for (int i = 0; i < DEFAULT_REACTIONS_LIST.size(); i++) {
- if (recent.size() > i) {
- out.add(recent.get(i));
- } else {
- out.add(DEFAULT_REACTIONS_LIST.get(i));
+ // Populate our recently used list if required (i.e., on first run)
+ if (recentlyUsed == null) {
+ try {
+ String recentlyUsedEmjoiJsonString = prefs.getString(RECENT_EMOJIS_KEY, DEFAULT_REACTION_EMOJIS_JSON_STRING);
+ recentlyUsed = JsonUtil.fromJson(recentlyUsedEmjoiJsonString, LinkedList.class);
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ Log.d(TAG, "Default reaction emoji data was corrupt (likely via key re-use on app upgrade) - rewriting fresh data.");
+ boolean writeSuccess = prefs.edit().putString(RECENT_EMOJIS_KEY, DEFAULT_REACTION_EMOJIS_JSON_STRING).commit();
+ if (!writeSuccess) { Log.w(TAG, "Failed to update recently used emojis in shared prefs."); }
+ recentlyUsed = DEFAULT_REACTION_EMOJIS_LIST;
}
}
-
- return out;
+ return new ArrayList<>(recentlyUsed);
}
@Override public List getDisplayEmoji() {
return Stream.of(getEmoji()).map(Emoji::new).toList();
}
- @Override public boolean hasSpriteMap() {
- return false;
- }
+ @Override public boolean hasSpriteMap() { return false; }
@Nullable
@Override
- public Uri getSpriteUri() {
- return null;
- }
+ public Uri getSpriteUri() { return null; }
- @Override public boolean isDynamic() {
- return true;
- }
+ @Override public boolean isDynamic() { return true; }
- public void onCodePointSelected(String emoji) {
- recentlyUsed.remove(emoji);
- recentlyUsed.add(emoji);
+ public static void onCodePointSelected(String emoji) {
+ // If the emoji is already in the recently used list then remove it..
+ if (recentlyUsed.contains(emoji)) { recentlyUsed.removeFirstOccurrence(emoji); }
- if (recentlyUsed.size() > EMOJI_LRU_SIZE) {
- Iterator iterator = recentlyUsed.iterator();
- iterator.next();
- iterator.remove();
- }
+ // ..and then regardless of whether the emoji used was already in the recently used list or not
+ // it gets placed as the first element in the list..
+ recentlyUsed.addFirst(emoji);
- final LinkedHashSet latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed);
- new AsyncTask() {
+ // Ensure that we only ever store data for a maximum of 6 recently used emojis (this code will
+ // execute if if we did NOT remove any occurrence of a previously used emoji but then added the
+ // new emoji to the front of the list).
+ while (recentlyUsed.size() > 6) { recentlyUsed.removeLast(); }
- @Override
- protected Void doInBackground(Void... params) {
- try {
- String serialized = JsonUtil.toJsonThrows(latestRecentlyUsed);
- prefs.edit()
- .putString(EMOJI_LRU_PREFERENCE, serialized)
- .apply();
- } catch (IOException e) {
- Log.w(TAG, e);
- }
-
- return null;
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- private String[] toReversePrimitiveArray(@NonNull LinkedHashSet emojiSet) {
- String[] emojis = new String[emojiSet.size()];
- int i = emojiSet.size() - 1;
- for (String emoji : emojiSet) {
- emojis[i--] = emoji;
- }
- return emojis;
+ // ..which we then save to shared prefs.
+ String recentlyUsedAsJsonString = JsonUtil.toJson(recentlyUsed);
+ boolean writeSuccess = prefs.edit().putString(RECENT_EMOJIS_KEY, recentlyUsedAsJsonString).commit();
+ if (!writeSuccess) { Log.w(TAG, "Failed to update recently used emojis in shared prefs."); }
}
}
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/contacts/ContactSelectionListLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListLoader.kt
index 3a2b2cbb5c..90e0ce50f2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListLoader.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListLoader.kt
@@ -58,7 +58,7 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
private fun getOpenGroups(contacts: List): List {
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) {
- it.address.isOpenGroup
+ it.address.isCommunity
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt
index 4bbcc3e021..184869b9ad 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt
@@ -116,7 +116,7 @@ class ConversationActionBarView @JvmOverloads constructor(
)
}
if (recipient.isGroupRecipient) {
- val title = if (recipient.isOpenGroupRecipient) {
+ val title = if (recipient.isCommunityRecipient) {
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
context.getString(R.string.ConversationActivity_active_member_count, userCount)
} else {
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 5fb295dcdd..fc6fb03e1e 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
@@ -46,7 +46,6 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
@@ -57,8 +56,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.consumeAsFlow
-import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -106,6 +103,7 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
import org.thoughtcrime.securesms.audio.AudioRecorder
+import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity
@@ -175,12 +173,11 @@ import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.SimpleTextWatcher
import org.thoughtcrime.securesms.util.isScrolledToBottom
+import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.toPx
import java.lang.ref.WeakReference
-import java.time.Instant
-import java.util.Date
import java.util.Locale
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
@@ -191,8 +188,6 @@ import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
private const val TAG = "ConversationActivityV2"
@@ -281,6 +276,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val isScrolledToBottom: Boolean
get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
+ private val isScrolledToWithin30dpOfBottom: Boolean
+ get() = binding?.conversationRecyclerView?.isScrolledToWithin30dpOfBottom ?: true
+
private val layoutManager: LinearLayoutManager?
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
@@ -336,6 +334,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
lifecycleCoroutineScope = lifecycleScope
)
adapter.visibleMessageViewDelegate = this
+
+ // Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView if we're
+ // already near the the bottom and the data changes.
+ adapter.registerAdapterDataObserver(ConversationAdapterDataObserver(binding?.conversationRecyclerView!!, adapter))
+
adapter
}
@@ -352,6 +355,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private lateinit var reactionDelegate: ConversationReactionDelegate
private val reactWithAnyEmojiStartPage = -1
+ // Properties for what message indices are visible previously & now, as well as the scroll state
+ private var previousLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
+ private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
+ private var recyclerScrollState: Int = RecyclerView.SCROLL_STATE_IDLE
+
// region Settings
companion object {
// Extras
@@ -375,12 +383,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
super.onCreate(savedInstanceState, isReady)
binding = ActivityConversationV2Binding.inflate(layoutInflater)
setContentView(binding!!.root)
+
// messageIdToScroll
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
val recipient = viewModel.recipient
val openGroup = recipient.let { viewModel.openGroup }
- if (recipient == null || (recipient.isOpenGroupRecipient && openGroup == null)) {
+ if (recipient == null || (recipient.isCommunityRecipient && openGroup == null)) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish()
}
@@ -390,6 +399,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
setUpLinkPreviewObserver()
restoreDraftIfNeeded()
setUpUiStateObserver()
+
binding!!.scrollToBottomButton.setOnClickListener {
val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
val targetPosition = if (reverseMessageList) 0 else adapter.itemCount
@@ -419,9 +429,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
setUpBlockedBanner()
binding!!.searchBottomBar.setEventListener(this)
updateSendAfterApprovalText()
- showOrHideInputIfNeeded()
setUpMessageRequestsBar()
+ // Note: Do not `showOrHideInputIfNeeded` here - we'll never start this activity w/ the
+ // keyboard visible and have no need to immediately display it.
+
val weakActivity = WeakReference(this)
lifecycleScope.launch(Dispatchers.IO) {
@@ -563,17 +575,45 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ if (recyclerScrollState == RecyclerView.SCROLL_STATE_IDLE) {
+ scrollToMostRecentMessageIfWeShould()
+ }
handleRecyclerViewScrolled()
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
-
+ recyclerScrollState = newState
}
})
+ }
- binding!!.conversationRecyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
- showScrollToBottomButtonIfApplicable()
+ private fun scrollToMostRecentMessageIfWeShould() {
+ // Grab an initial 'previous' last visible message..
+ if (previousLastVisibleRecyclerViewIndex == RecyclerView.NO_POSITION) {
+ previousLastVisibleRecyclerViewIndex = layoutManager?.findLastVisibleItemPosition()!!
}
+
+ // ..and grab the 'current' last visible message.
+ currentLastVisibleRecyclerViewIndex = layoutManager?.findLastVisibleItemPosition()!!
+
+ // If the current last visible message index is less than the previous one (i.e. we've
+ // lost visibility of one or more messages due to showing the IME keyboard) AND we're
+ // at the bottom of the message feed..
+ val atBottomAndTrueLastNoLongerVisible = currentLastVisibleRecyclerViewIndex!! <= previousLastVisibleRecyclerViewIndex!! && !binding?.scrollToBottomButton?.isVisible!!
+
+ // ..OR we're at the last message or have received a new message..
+ val atLastOrReceivedNewMessage = currentLastVisibleRecyclerViewIndex == (adapter.itemCount - 1)
+
+ // ..then scroll the recycler view to the last message on resize. Note: We cannot just call
+ // scroll/smoothScroll - we have to `post` it or nothing happens!
+ if (atBottomAndTrueLastNoLongerVisible || atLastOrReceivedNewMessage) {
+ binding?.conversationRecyclerView?.post {
+ binding?.conversationRecyclerView?.smoothScrollToPosition(adapter.itemCount)
+ }
+ }
+
+ // Update our previous last visible view index to the current one
+ previousLastVisibleRecyclerViewIndex = currentLastVisibleRecyclerViewIndex
}
// called from onCreate
@@ -760,13 +800,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// of the first unread message in the middle of the screen
if (isFirstLoad && !reverseMessageList) {
layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2))
-
if (shouldHighlight) { highlightViewAtPosition(lastSeenItemPosition) }
-
return lastSeenItemPosition
}
if (lastSeenItemPosition <= 3) { return lastSeenItemPosition }
+
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
return lastSeenItemPosition
}
@@ -931,11 +970,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
view.glide = glide
view.onCandidateSelected = { handleMentionSelected(it) }
additionalContentContainer.addView(view)
- val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
+ val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isCommunityRecipient)
this.mentionCandidatesView = view
view.show(candidates, viewModel.threadId)
} else {
- val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
+ val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isCommunityRecipient)
this.mentionCandidatesView!!.setMentionCandidates(candidates)
}
isShowingMentionCandidatesView = true
@@ -1040,8 +1079,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun handleRecyclerViewScrolled() {
val binding = binding ?: return
+
+ // Note: The typing indicate is whether the other person / other people are typing - it has
+ // nothing to do with the IME keyboard state.
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
+
showScrollToBottomButtonIfApplicable()
val maybeTargetVisiblePosition = if (reverseMessageList) layoutManager?.findFirstVisibleItemPosition() else layoutManager?.findLastVisibleItemPosition()
val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION
@@ -1069,6 +1112,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val blindedRecipient = viewModel.blindedRecipient
val binding = binding ?: return
val openGroup = viewModel.openGroup
+
val (textResource, insertParam) = when {
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
@@ -1148,7 +1192,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun copyOpenGroupUrl(thread: Recipient) {
- if (!thread.isOpenGroupRecipient) { return }
+ if (!thread.isCommunityRecipient) { return }
val threadId = threadDb.getThreadIdIfExistsFor(thread) ?: return
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
@@ -1207,6 +1251,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)
}
@@ -1286,6 +1331,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
sendEmojiRemoval(emoji, messageRecord)
} else {
sendEmojiReaction(emoji, messageRecord)
+ RecentEmojiPageModel.onCodePointSelected(emoji) // Save to recently used reaction emojis
}
}
@@ -1312,7 +1358,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} else originalMessage.individualRecipient.address
// Send it
reactionMessage.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, true)
- if (recipient.isOpenGroupRecipient) {
+ if (recipient.isCommunityRecipient) {
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
viewModel.openGroup?.let {
OpenGroupApi.addReaction(it.room, it.server, messageServerId, emoji)
@@ -1336,7 +1382,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} else originalMessage.individualRecipient.address
message.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, false)
- if (recipient.isOpenGroupRecipient) {
+ if (recipient.isCommunityRecipient) {
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
viewModel.openGroup?.let {
OpenGroupApi.deleteReaction(it.room, it.server, messageServerId, emoji)
@@ -1733,7 +1779,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
sendAttachments(slideDeck.asAttachments(), body)
}
INVITE_CONTACTS -> {
- if (viewModel.recipient?.isOpenGroupRecipient != true) { return }
+ if (viewModel.recipient?.isCommunityRecipient != true) { return }
val extras = intent?.extras ?: return
if (!intent.hasExtra(selectedContactsKey)) { return }
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
@@ -1799,19 +1845,61 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
handleLongPress(messages.first(), 0) //TODO: begin selection mode
}
+ // The option to "Delete just for me" or "Delete for everyone"
+ private fun showDeleteOrDeleteForEveryoneInCommunityUI(messages: Set) {
+ val bottomSheet = DeleteOptionsBottomSheet()
+ bottomSheet.recipient = viewModel.recipient!!
+ bottomSheet.onDeleteForMeTapped = {
+ messages.forEach(viewModel::deleteLocally)
+ bottomSheet.dismiss()
+ endActionMode()
+ }
+ bottomSheet.onDeleteForEveryoneTapped = {
+ messages.forEach(viewModel::deleteForEveryone)
+ bottomSheet.dismiss()
+ endActionMode()
+ }
+ bottomSheet.onCancelTapped = {
+ bottomSheet.dismiss()
+ endActionMode()
+ }
+ bottomSheet.show(supportFragmentManager, bottomSheet.tag)
+ }
+
+ private fun showDeleteLocallyUI(messages: Set) {
+ val messageCount = 1
+ showSessionDialog {
+ title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
+ text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
+ button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
+ cancelButton(::endActionMode)
+ }
+ }
+
+ // Note: The messages in the provided set may be a single message, or multiple if there are a
+ // group of selected messages.
override fun deleteMessages(messages: Set) {
- val recipient = viewModel.recipient ?: return
+ val recipient = viewModel.recipient
+ if (recipient == null) {
+ Log.w("ConversationActivityV2", "Asked to delete messages but could not obtain viewModel recipient - aborting.")
+ return
+ }
+
val allSentByCurrentUser = messages.all { it.isOutgoing }
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
- if (recipient.isOpenGroupRecipient) {
- val messageCount = 1
+ // If the recipient is a community OR a Note-to-Self then we delete the message for everyone
+ if (recipient.isCommunityRecipient || recipient.isLocalNumber) {
+ val messageCount = 1 // Only used for plurals string
showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
- button(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
+ button(R.string.delete) {
+ messages.forEach(viewModel::deleteForEveryone); endActionMode()
+ }
cancelButton { endActionMode() }
}
+ // Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
} else if (allSentByCurrentUser && allHasHash) {
val bottomSheet = DeleteOptionsBottomSheet()
bottomSheet.recipient = recipient
@@ -1830,13 +1918,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
endActionMode()
}
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
- } else {
+ }
+ else // Finally, if this is a closed group and you are deleting someone else's message(s) then we can only delete locally.
+ {
val messageCount = 1
-
showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
- button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
+ button(R.string.delete) {
+ messages.forEach(viewModel::deleteLocally); endActionMode()
+ }
cancelButton(::endActionMode)
}
}
@@ -1855,7 +1946,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
showSessionDialog {
title(R.string.ConversationFragment_ban_selected_user)
text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
- button(R.string.ban) { viewModel.banAndDeleteAll(messages.first().individualRecipient); endActionMode() }
+ button(R.string.ban) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
cancelButton(::endActionMode)
}
}
@@ -1937,7 +2028,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val message = messages.first() as MmsMessageRecord
// Do not allow the user to download a file attachment before it has finished downloading
- // TODO: Localise the msg in this toast!
if (message.isMediaPending) {
Toast.makeText(this, resources.getString(R.string.conversation_activity__wait_until_attachment_has_finished_downloading), Toast.LENGTH_LONG).show()
return
@@ -2107,4 +2197,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
+ // AdapterDataObserver implementation to scroll us to the bottom of the ConversationRecyclerView
+ // when we're already near the bottom and we send or receive a message.
+ inner class ConversationAdapterDataObserver(val recyclerView: ConversationRecyclerView, val adapter: ConversationAdapter) : RecyclerView.AdapterDataObserver() {
+ override fun onChanged() {
+ super.onChanged()
+ if (recyclerView.isScrolledToWithin30dpOfBottom) {
+ // Note: The adapter itemCount is zero based - so calling this with the itemCount in
+ // a non-zero based manner scrolls us to the bottom of the last message (including
+ // to the bottom of long messages as required by Jira SES-789 / GitHub 1364).
+ recyclerView.scrollToPosition(adapter.itemCount)
+ }
+ }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
index 6013af5ba4..371df34565 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
@@ -22,10 +22,12 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact
+import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
+import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests
@@ -57,6 +59,7 @@ class ConversationAdapter(
private val contactCache = SparseArray(100)
private val contactLoadedCache = SparseBooleanArray(100)
private val lastSeen = AtomicLong(originalLastSeen)
+ private var lastSentMessageId: Long = -1L
init {
lifecycleCoroutineScope.launch(IO) {
@@ -136,7 +139,8 @@ class ConversationAdapter(
senderId,
lastSeen.get(),
visibleMessageViewDelegate,
- onAttachmentNeedsDownload
+ onAttachmentNeedsDownload,
+ lastSentMessageId
)
if (!message.isDeleted) {
@@ -205,8 +209,23 @@ class ConversationAdapter(
return messageDB.readerFor(cursor).current
}
+ private fun getLastSentMessageId(cursor: Cursor): Long {
+ // If we don't move to first (or at least step backwards) we can step off the end of the
+ // cursor and any query will return an "Index = -1" error.
+ val cursorHasContent = cursor.moveToFirst()
+ if (cursorHasContent) {
+ val thisThreadId = cursor.getLong(4) // Column index 4 is "thread_id"
+ if (thisThreadId != -1L) {
+ val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
+ return messageDB.getLastSentMessageFromSender(thisThreadId, thisUsersSessionId)
+ }
+ }
+ return -1L
+ }
+
override fun changeCursor(cursor: Cursor?) {
super.changeCursor(cursor)
+
val toRemove = mutableSetOf()
val toDeselect = mutableSetOf>()
for (selected in selectedItems) {
@@ -224,6 +243,11 @@ class ConversationAdapter(
toDeselect.iterator().forEach { (pos, record) ->
onDeselect(record, pos)
}
+
+ // This value gets updated here ONLY when the cursor changes, and the value is then passed
+ // through to `VisibleMessageView.bind` each time we bind via `onBindItemViewHolder`, above.
+ // If there are no messages then lastSentMessageId is assigned the value -1L.
+ if (cursor != null) { lastSentMessageId = getLastSentMessageId(cursor) }
}
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
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 405d2a3c0e..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
@@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanBanSelectedUsers
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanDeleteSelectedItems
import org.thoughtcrime.securesms.database.MmsSmsDatabase
-import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
@@ -533,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
@@ -541,7 +540,7 @@ class ConversationReactionOverlay : FrameLayout {
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
}
// Copy Session ID
- if (recipient.isGroupRecipient && !recipient.isOpenGroupRecipient && message.recipient.address.toString() != userPublicKey) {
+ if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_session_id, { handleActionItemClicked(Action.COPY_SESSION_ID) })
}
// Delete message
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
index 677dd22f60..80d6df87fe 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
@@ -1,18 +1,23 @@
package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
+
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
+
import com.goterl.lazysodium.utils.KeyPair
+
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
+
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+
import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
@@ -22,9 +27,12 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.database.MmsSmsDatabase
+
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
+
import java.util.UUID
class ConversationViewModel(
@@ -144,9 +152,14 @@ class ConversationViewModel(
}
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
- val recipient = recipient ?: return@launch
+ val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for delete for everyone - aborting delete operation.")
+
repository.deleteForEveryone(threadId, recipient, message)
+ .onSuccess {
+ Log.d("Loki", "Deleted message ${message.id} ")
+ }
.onFailure {
+ Log.w("Loki", "FAILED TO delete message ${message.id} ")
showMessage("Couldn't delete message due to error: $it")
}
}
@@ -168,10 +181,15 @@ class ConversationViewModel(
}
}
- fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch {
- repository.banAndDeleteAll(threadId, recipient)
+ fun banAndDeleteAll(messageRecord: MessageRecord) = viewModelScope.launch {
+
+ repository.banAndDeleteAll(threadId, messageRecord.individualRecipient)
.onSuccess {
+ // At this point the server side messages have been successfully deleted..
showMessage("Successfully banned user and deleted all their messages")
+
+ // ..so we can now delete all their messages in this thread from local storage & remove the views.
+ repository.deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord)
}
.onFailure {
showMessage("Couldn't execute request due to error: $it")
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 61732827f3..d5e28fb936 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 3746aa52e4..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
@@ -65,7 +65,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
// Copy Session ID
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
- (thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
+ (thread.isGroupRecipient && !thread.isCommunityRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
// Message detail
menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1
// Resend
@@ -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/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
index dadf138ead..11069937a0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
@@ -50,7 +50,7 @@ object ConversationMenuHelper {
) {
// Prepare
menu.clear()
- val isOpenGroup = thread.isOpenGroupRecipient
+ val isOpenGroup = thread.isCommunityRecipient
// Base menu (options that should always be present)
inflater.inflate(R.menu.menu_conversation, menu)
// Expiring messages
@@ -253,7 +253,7 @@ object ConversationMenuHelper {
}
private fun copyOpenGroupUrl(context: Context, thread: Recipient) {
- if (!thread.isOpenGroupRecipient) { return }
+ if (!thread.isCommunityRecipient) { return }
val listener = context as? ConversationMenuListener ?: return
listener.copyOpenGroupUrl(thread)
}
@@ -300,7 +300,7 @@ object ConversationMenuHelper {
}
private fun inviteContacts(context: Context, thread: Recipient) {
- if (!thread.isOpenGroupRecipient) { return }
+ if (!thread.isCommunityRecipient) { return }
val intent = Intent(context, SelectContactsActivity::class.java)
val activity = context as AppCompatActivity
activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS)
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 df49785deb..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
@@ -32,10 +32,12 @@ import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.contacts.Contact.ContactContext
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.utilities.Address
+import org.session.libsession.utilities.TextSecurePreferences
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
@@ -64,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
@@ -131,8 +134,10 @@ class VisibleMessageView : LinearLayout {
senderSessionID: String,
lastSeen: Long,
delegate: VisibleMessageViewDelegate? = null,
- onAttachmentNeedsDownload: (Long, Long) -> Unit
+ onAttachmentNeedsDownload: (Long, Long) -> Unit,
+ lastSentMessageId: Long
) {
+ replyDisabled = message.isOpenGroupInvitation
val threadID = message.threadId
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
val isGroupThread = thread.isGroupRecipient
@@ -164,7 +169,7 @@ class VisibleMessageView : LinearLayout {
binding.profilePictureView.publicKey = senderSessionID
binding.profilePictureView.update(message.individualRecipient)
binding.profilePictureView.setOnClickListener {
- if (thread.isOpenGroupRecipient) {
+ if (thread.isCommunityRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
// TODO: support v2 soon
@@ -177,7 +182,7 @@ class VisibleMessageView : LinearLayout {
maybeShowUserDetails(senderSessionID, threadID)
}
}
- if (thread.isOpenGroupRecipient) {
+ if (thread.isCommunityRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
var standardPublicKey = ""
var blindedPublicKey: String? = null
@@ -193,16 +198,20 @@ class VisibleMessageView : LinearLayout {
}
binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected))
val contactContext =
- if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
+ if (thread.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
+
// Unread marker
binding.unreadMarkerContainer.isVisible = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing
+
// Date break
val showDateBreak = isStartOfMessageCluster || snIsSelected
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
val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
@@ -237,42 +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) {
- val disappearing = message.expiresIn > 0
+ // 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
+
+ // 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 || disappearing) {
- 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)
-
- val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
- val isLastMessage = message.id == lastMessageID
- binding.messageStatusTextView.isVisible =
- textId != null && (!message.isSent || isLastMessage || disappearing)
- val showTimer = disappearing && !message.isPending
- binding.messageStatusImageView.isVisible =
- iconID != null && !showTimer && (!message.isSent || isLastMessage)
+ // 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)
- binding.messageStatusImageView.bringToFront()
+ // Potential options at this point are that the message is:
+ // i.) incoming AND scheduled to disappear.
+ // ii.) outgoing but NOT scheduled to disappear, or
+ // iii.) outgoing AND scheduled to disappear.
+
+ // ----- Case i..) Message is incoming and scheduled to disappear -----
+ if (message.isIncoming && scheduledToDisappear) {
+ // Display the status ('Read') and the show the timer only (no delivery icon)
+ binding.messageStatusTextView.isVisible = true
+ binding.expirationTimerView.isVisible = true
binding.expirationTimerView.bringToFront()
- 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()
+ }
}
}
@@ -294,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
)
@@ -310,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) {
@@ -401,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/Database.java b/app/src/main/java/org/thoughtcrime/securesms/database/Database.java
index b6b224589e..e1879d5230 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/Database.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/Database.java
@@ -26,6 +26,7 @@ import androidx.annotation.NonNull;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.session.libsession.utilities.WindowDebouncer;
+import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@@ -77,11 +78,11 @@ public abstract class Database {
notifyConversationListListeners();
}
- protected void setNotifyConverationListeners(Cursor cursor, long threadId) {
+ protected void setNotifyConversationListeners(Cursor cursor, long threadId) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getUriForThread(threadId));
}
- protected void setNotifyConverationListListeners(Cursor cursor) {
+ protected void setNotifyConversationListListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.ConversationList.CONTENT_URI);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt
index a65c22545b..013bbf5cb5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt
@@ -6,8 +6,8 @@ import android.database.Cursor
import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.messages.ExpirationDatabaseMetadata
import org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX
-import org.session.libsession.utilities.GroupUtil.OPEN_GROUP_INBOX_PREFIX
-import org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX
+import org.session.libsession.utilities.GroupUtil.COMMUNITY_INBOX_PREFIX
+import org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
@@ -38,8 +38,8 @@ class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHel
INSERT INTO $TABLE_NAME ($THREAD_ID) SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ID}
FROM ${ThreadDatabase.TABLE_NAME}, ${RecipientDatabase.TABLE_NAME}
WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$CLOSED_GROUP_PREFIX%'
- AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$OPEN_GROUP_PREFIX%'
- AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$OPEN_GROUP_INBOX_PREFIX%'
+ AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$COMMUNITY_PREFIX%'
+ AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$COMMUNITY_INBOX_PREFIX%'
AND EXISTS (SELECT ${RecipientDatabase.EXPIRE_MESSAGES} FROM ${RecipientDatabase.TABLE_NAME} WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} = ${RecipientDatabase.TABLE_NAME}.${RecipientDatabase.ADDRESS} AND ${RecipientDatabase.EXPIRE_MESSAGES} > 0)
""".trimIndent()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt
index 441c979108..18dd42818d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt
@@ -4,6 +4,7 @@ import android.content.ContentValues
import android.content.Context
import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE
import org.session.libsignal.database.LokiMessageDatabaseProtocol
+import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
@@ -72,7 +73,12 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
"${Companion.messageID} = ? AND $messageType = ?",
arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor ->
cursor.getInt(serverID).toLong()
- } ?: return
+ }
+
+ if (serverID == null) {
+ Log.w(this::class.simpleName, "Could not get server ID to delete message with ID: $messageID")
+ return
+ }
database.beginTransaction()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java
index 1b273de929..63db0c66ba 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java
@@ -68,7 +68,7 @@ public class MediaDatabase extends Database {
public Cursor getGalleryMediaForThread(long threadId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = database.rawQuery(GALLERY_MEDIA_QUERY, new String[]{threadId+""});
- setNotifyConverationListeners(cursor, threadId);
+ setNotifyConversationListeners(cursor, threadId);
return cursor;
}
@@ -83,7 +83,7 @@ public class MediaDatabase extends Database {
public Cursor getDocumentMediaForThread(long threadId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, new String[]{threadId+""});
- setNotifyConverationListeners(cursor, threadId);
+ setNotifyConversationListeners(cursor, threadId);
return cursor;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
index 62db50a1ba..f2fcefd0aa 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
@@ -19,9 +19,9 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
-import android.provider.ContactsContract.CommonDataKinds.BaseTypes
import com.annimon.stream.Stream
import com.google.android.mms.pdu_alt.PduHeaders
+import org.apache.commons.lang3.StringUtils
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
@@ -214,7 +214,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
fun getMessage(messageId: Long): Cursor {
val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString()))
- setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId))
+ setNotifyConversationListeners(cursor, getThreadIdForMessage(messageId))
return cursor
}
@@ -630,6 +630,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
if (retrieved.isExpirationUpdate) deleteExpirationTimerMessages(threadId, true.takeUnless { retrieved.isGroup })
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
if (messageId == -1L) {
+ Log.w(TAG, "insertSecureDecryptedMessageOutbox believes the MmsDatabase insertion failed.")
return Optional.absent()
}
markAsSent(messageId, true)
@@ -859,8 +860,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
*/
private fun deleteMessages(messageIds: Array) {
if (messageIds.isEmpty()) {
+ Log.w(TAG, "No message Ids provided to MmsDatabase.deleteMessages - aborting delete operation!")
return
}
+
// don't need thread IDs
val queryBuilder = StringBuilder()
for (i in messageIds.indices) {
@@ -883,6 +886,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
notifyStickerPackListeners()
}
+ // Caution: The bool returned from `deleteMessage` is NOT "Was the message successfully deleted?"
+ // - it is "Was the thread deleted because removing that message resulted in an empty thread"!
override fun deleteMessage(messageId: Long): Boolean {
val threadId = getThreadIdForMessage(messageId)
val attachmentDatabase = get(context).attachmentDatabase()
@@ -899,14 +904,15 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
}
override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean {
- val attachmentDatabase = get(context).attachmentDatabase()
- val groupReceiptDatabase = get(context).groupReceiptDatabase()
+ val argsArray = messageIds.map { "?" }
+ val argValues = messageIds.map { it.toString() }.toTypedArray()
- queue(Runnable { attachmentDatabase.deleteAttachmentsForMessages(messageIds) })
- groupReceiptDatabase.deleteRowsForMessages(messageIds)
-
- val database = databaseHelper.writableDatabase
- database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
+ val db = databaseHelper.writableDatabase
+ db.delete(
+ TABLE_NAME,
+ ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
+ argValues
+ )
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
notifyConversationListeners(threadId)
@@ -1089,8 +1095,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
}
val whereString = where.substring(0, where.length - 4)
try {
- cursor =
- db!!.query(TABLE_NAME, arrayOf(ID), whereString, null, null, null, null)
+ cursor = db!!.query(TABLE_NAME, arrayOf(ID), whereString, null, null, null, null)
val toDeleteStringMessageIds = mutableListOf()
while (cursor.moveToNext()) {
toDeleteStringMessageIds += cursor.getLong(0).toString()
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 0db4dd00e5..da4f39f0c1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -30,6 +30,7 @@ import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
import org.jetbrains.annotations.NotNull;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.Util;
+import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -115,6 +116,53 @@ public class MmsSmsDatabase extends Database {
return null;
}
+ public @Nullable MessageRecord getSentMessageFor(long timestamp, String serializedAuthor) {
+ // Early exit if the author is not us
+ boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
+ if (!isOwnNumber) {
+ Log.i(TAG, "Asked to find sent messages but provided author is not us - returning null.");
+ return null;
+ }
+
+ try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
+ MmsSmsDatabase.Reader reader = readerFor(cursor);
+
+ MessageRecord messageRecord;
+ while ((messageRecord = reader.getNext()) != null) {
+ if (messageRecord.isOutgoing())
+ {
+ return messageRecord;
+ }
+ }
+ }
+ Log.i(TAG, "Could not find any message sent from us at provided timestamp - returning null.");
+ return null;
+ }
+
+ public MessageRecord getLastSentMessageRecordFromSender(long threadId, String serializedAuthor) {
+ // Early exit if the author is not us
+ boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
+ if (!isOwnNumber) {
+ Log.i(TAG, "Asked to find last sent message but provided author is not us - returning null.");
+ return null;
+ }
+
+ String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
+ String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
+
+ // Try everything with resources so that they auto-close on end of scope
+ try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) {
+ try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
+ MessageRecord messageRecord;
+ while ((messageRecord = reader.getNext()) != null) {
+ if (messageRecord.isOutgoing()) { return messageRecord; }
+ }
+ }
+ }
+ Log.i(TAG, "Could not find last sent message from us in given thread - returning null.");
+ return null;
+ }
+
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) {
return getMessageFor(timestamp, author.serialize());
}
@@ -183,7 +231,7 @@ public class MmsSmsDatabase extends Database {
String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null;
Cursor cursor = queryTables(PROJECTION, selection, order, limitStr);
- setNotifyConverationListeners(cursor, threadId);
+ setNotifyConversationListeners(cursor, threadId);
return cursor;
}
@@ -209,6 +257,82 @@ public class MmsSmsDatabase extends Database {
}
}
+ // Builds up and returns a list of all all the messages sent by this user in the given thread.
+ // Used to do a pass through our local database to remove records when a user has "Ban & Delete"
+ // called on them in a Community.
+ public Set getAllMessageRecordsFromSenderInThread(long threadId, String serializedAuthor) {
+ String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS + " = \"" + serializedAuthor + "\"";
+ Set identifiedMessages = new HashSet();
+
+ // Try everything with resources so that they auto-close on end of scope
+ try (Cursor cursor = queryTables(PROJECTION, selection, null, null)) {
+ try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
+ MessageRecord messageRecord;
+ while ((messageRecord = reader.getNext()) != null) {
+ identifiedMessages.add(messageRecord);
+ }
+ }
+ }
+ return identifiedMessages;
+ }
+
+ // Version of the above `getAllMessageRecordsFromSenderInThread` method that returns the message
+ // Ids rather than the set of MessageRecords - currently unused by potentially useful in the future.
+ public Set getAllMessageIdsFromSenderInThread(long threadId, String serializedAuthor) {
+ String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS + " = \"" + serializedAuthor + "\"";
+
+ Set identifiedMessages = new HashSet();
+
+ // Try everything with resources so that they auto-close on end of scope
+ try (Cursor cursor = queryTables(PROJECTION, selection, null, null)) {
+ try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
+ MessageRecord messageRecord;
+ while ((messageRecord = reader.getNext()) != null) {
+ identifiedMessages.add(messageRecord.id);
+ }
+ }
+ }
+ return identifiedMessages;
+ }
+
+ public long getLastSentMessageFromSender(long threadId, String serializedAuthor) {
+
+ // Early exit
+ boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
+ if (!isOwnNumber) {
+ Log.i(TAG, "Asked to find last sent message but sender isn't us - returning null.");
+ return -1;
+ }
+
+ String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
+ String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
+
+ // Try everything with resources so that they auto-close on end of scope
+ try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) {
+ try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
+ MessageRecord messageRecord;
+ while ((messageRecord = reader.getNext()) != null) {
+ if (messageRecord.isOutgoing()) { return messageRecord.id; }
+ }
+ }
+ }
+ Log.i(TAG, "Could not find last sent message from us - returning -1.");
+ return -1;
+ }
+
+ public long getLastMessageTimestamp(long threadId) {
+ String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
+ String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
+
+ try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) {
+ if (cursor.moveToFirst()) {
+ return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT));
+ }
+ }
+
+ return -1;
+ }
+
public Cursor getUnread() {
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/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
index 69c9fa87f1..8dbef32017 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
@@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.database;
-import static org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX;
+import static org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX;
import android.content.ContentValues;
import android.content.Context;
@@ -123,18 +123,18 @@ public class RecipientDatabase extends Database {
public static String getUpdateApprovedCommand() {
return "UPDATE "+ TABLE_NAME + " " +
"SET " + APPROVED + " = 1, " + APPROVED_ME + " = 1 " +
- "WHERE " + ADDRESS + " NOT LIKE '" + OPEN_GROUP_PREFIX + "%'";
+ "WHERE " + ADDRESS + " NOT LIKE '" + COMMUNITY_PREFIX + "%'";
}
public static String getUpdateResetApprovedCommand() {
return "UPDATE "+ TABLE_NAME + " " +
"SET " + APPROVED + " = 0, " + APPROVED_ME + " = 0 " +
- "WHERE " + ADDRESS + " NOT LIKE '" + OPEN_GROUP_PREFIX + "%'";
+ "WHERE " + ADDRESS + " NOT LIKE '" + COMMUNITY_PREFIX + "%'";
}
public static String getUpdateApprovedSelectConversations() {
return "UPDATE "+ TABLE_NAME + " SET "+APPROVED+" = 1, "+APPROVED_ME+" = 1 "+
- "WHERE "+ADDRESS+ " NOT LIKE '"+OPEN_GROUP_PREFIX+"%' " +
+ "WHERE "+ADDRESS+ " NOT LIKE '"+ COMMUNITY_PREFIX +"%' " +
"AND ("+ADDRESS+" IN (SELECT "+ThreadDatabase.TABLE_NAME+"."+ThreadDatabase.ADDRESS+" FROM "+ThreadDatabase.TABLE_NAME+" WHERE ("+ThreadDatabase.MESSAGE_COUNT+" != 0) "+
"OR "+ADDRESS+" IN (SELECT "+GroupDatabase.TABLE_NAME+"."+GroupDatabase.ADMINS+" FROM "+GroupDatabase.TABLE_NAME+")))";
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java
index eac6a5fbc3..106cc86e17 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java
@@ -10,6 +10,7 @@ import com.annimon.stream.Stream;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.session.libsession.utilities.Util;
+
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import java.util.List;
@@ -115,11 +116,9 @@ public class SearchDatabase extends Database {
public Cursor queryMessages(@NonNull String query) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String prefixQuery = adjustQuery(query);
-
int queryLimit = Math.min(query.length()*50,500);
-
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery, String.valueOf(queryLimit) });
- setNotifyConverationListListeners(cursor);
+ setNotifyConversationListListeners(cursor);
return cursor;
}
@@ -128,7 +127,7 @@ public class SearchDatabase extends Database {
String prefixQuery = adjustQuery(query);
Cursor cursor = db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { prefixQuery, String.valueOf(threadId), prefixQuery, String.valueOf(threadId) });
- setNotifyConverationListListeners(cursor);
+ setNotifyConversationListListeners(cursor);
return cursor;
}
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 2c0c33dda8..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;
@@ -621,17 +616,20 @@ public class SmsDatabase extends MessagingDatabase {
public Cursor getMessageCursor(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId + ""}, null, null, null);
- setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId));
+ setNotifyConversationListeners(cursor, getThreadIdForMessage(messageId));
return cursor;
}
+ // Caution: The bool returned from `deleteMessage` is NOT "Was the message successfully deleted?"
+ // - it is "Was the thread deleted because removing that message resulted in an empty thread"!
@Override
public boolean deleteMessage(long messageId) {
Log.i("MessageDatabase", "Deleting: " + messageId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long threadId = getThreadIdForMessage(messageId);
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
- boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
+ notifyConversationListeners(threadId);
+ boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, false);
return threadDeleted;
}
@@ -645,9 +643,6 @@ public class SmsDatabase extends MessagingDatabase {
argValues[i] = (messageIds[i] + "");
}
- String combinedMessageIdArgss = StringUtils.join(messageIds, ',');
- String combinedMessageIds = StringUtils.join(messageIds, ',');
- Log.i("MessageDatabase", "Deleting: " + combinedMessageIds);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(
TABLE_NAME,
@@ -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 2760ee302f..354ec05c46 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
@@ -92,7 +92,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.SessionMetaProtocol
import java.security.MessageDigest
-import kotlin.time.Duration.Companion.days
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
private const val TAG = "Storage"
@@ -100,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) {
@@ -121,7 +120,7 @@ open class Storage(
)
volatile.set(newVolatileParams)
}
- } else if (address.isOpenGroup) {
+ } else if (address.isCommunity) {
// these should be added on the group join / group info fetch
Log.w("Loki", "Thread created called for open group address, not adding any extra information")
}
@@ -152,7 +151,7 @@ open class Storage(
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
volatile.eraseLegacyClosedGroup(sessionId)
groups.eraseLegacyGroup(sessionId)
- } else if (address.isOpenGroup) {
+ } else if (address.isCommunity) {
// these should be removed in the group leave / handling new configs
Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
}
@@ -182,7 +181,7 @@ open class Storage(
}
override fun getUserProfile(): Profile {
- val displayName = TextSecurePreferences.getProfileName(context)!!
+ val displayName = TextSecurePreferences.getProfileName(context)
val profileKey = ProfileKeyUtil.getProfileKey(context)
val profilePictureUrl = TextSecurePreferences.getProfilePictureURL(context)
return Profile(displayName, profileKey, profilePictureUrl)
@@ -257,7 +256,7 @@ open class Storage(
// recipient closed group
recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize()))
// recipient is open group
- recipient.isOpenGroupRecipient -> {
+ recipient.isCommunityRecipient -> {
val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return
BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
config.getOrConstructCommunity(base, room, pubKey)
@@ -327,7 +326,7 @@ open class Storage(
setRecipientApprovedMe(targetRecipient, true)
}
}
- if (message.threadID == null && !targetRecipient.isOpenGroupRecipient) {
+ if (message.threadID == null && !targetRecipient.isCommunityRecipient) {
// open group recipients should explicitly create threads
message.threadID = getOrCreateThreadIdFor(targetAddress)
}
@@ -767,13 +766,36 @@ open class Storage(
override fun markAsSent(timestamp: Long, author: String) {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
- val messageRecord = database.getMessageFor(timestamp, author) ?: return
+ val messageRecord = database.getSentMessageFor(timestamp, author)
+ if (messageRecord == null) {
+ Log.w(TAG, "Failed to retrieve local message record in Storage.markAsSent - aborting.")
+ return
+ }
+
if (messageRecord.isMms) {
- val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
- mmsDatabase.markAsSent(messageRecord.getId(), true)
+ DatabaseComponent.get(context).mmsDatabase().markAsSent(messageRecord.getId(), true)
} else {
- val smsDatabase = DatabaseComponent.get(context).smsDatabase()
- smsDatabase.markAsSent(messageRecord.getId(), true)
+ DatabaseComponent.get(context).smsDatabase().markAsSent(messageRecord.getId(), true)
+ }
+ }
+
+ // Method that marks a message as sent in Communities (only!) - where the server modifies the
+ // message timestamp and as such we cannot use that to identify the local message.
+ override fun markAsSentToCommunity(threadId: Long, messageID: Long) {
+ val database = DatabaseComponent.get(context).mmsSmsDatabase()
+ val message = database.getLastSentMessageRecordFromSender(threadId, TextSecurePreferences.getLocalNumber(context))
+
+ // Ensure we can find the local message..
+ if (message == null) {
+ Log.w(TAG, "Could not find local message in Storage.markAsSentToCommunity - aborting.")
+ return
+ }
+
+ // ..and mark as sent if found.
+ if (message.isMms) {
+ DatabaseComponent.get(context).mmsDatabase().markAsSent(message.getId(), true)
+ } else {
+ DatabaseComponent.get(context).smsDatabase().markAsSent(message.getId(), true)
}
}
@@ -808,7 +830,11 @@ open class Storage(
override fun markUnidentified(timestamp: Long, author: String) {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
- val messageRecord = database.getMessageFor(timestamp, author) ?: return
+ val messageRecord = database.getMessageFor(timestamp, author)
+ if (messageRecord == null) {
+ Log.w(TAG, "Could not identify message with timestamp: $timestamp from author: $author")
+ return
+ }
if (messageRecord.isMms) {
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
mmsDatabase.markUnidentified(messageRecord.getId(), true)
@@ -818,6 +844,26 @@ open class Storage(
}
}
+ // Method that marks a message as unidentified in Communities (only!) - where the server
+ // modifies the message timestamp and as such we cannot use that to identify the local message.
+ override fun markUnidentifiedInCommunity(threadId: Long, messageId: Long) {
+ val database = DatabaseComponent.get(context).mmsSmsDatabase()
+ val message = database.getLastSentMessageRecordFromSender(threadId, TextSecurePreferences.getLocalNumber(context))
+
+ // Check to ensure the message exists
+ if (message == null) {
+ Log.w(TAG, "Could not find local message in Storage.markUnidentifiedInCommunity - aborting.")
+ return
+ }
+
+ // Mark it as unidentified if we found the message successfully
+ if (message.isMms) {
+ DatabaseComponent.get(context).mmsDatabase().markUnidentified(message.getId(), true)
+ } else {
+ DatabaseComponent.get(context).smsDatabase().markUnidentified(message.getId(), true)
+ }
+ }
+
override fun markAsSentFailed(timestamp: Long, author: String, error: Exception) {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val messageRecord = database.getMessageFor(timestamp, author) ?: return
@@ -971,7 +1017,10 @@ open class Storage(
val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, 0, true, null, listOf(), listOf())
val mmsDB = DatabaseComponent.get(context).mmsDatabase()
val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase()
- if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return
+ if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) {
+ Log.w(TAG, "Bailing from insertOutgoingInfoMessage because we believe the message has already been sent!")
+ return
+ }
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true)
mmsDB.markAsSent(infoMessageID, true)
}
@@ -1289,7 +1338,7 @@ open class Storage(
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
)
groups.set(newGroupInfo)
- } else if (threadRecipient.isOpenGroupRecipient) {
+ } else if (threadRecipient.isCommunityRecipient) {
val openGroup = getOpenGroup(threadID) ?: return
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
@@ -1322,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 fd5042086c..209e7f187d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -18,7 +18,7 @@
package org.thoughtcrime.securesms.database;
import static org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX;
-import static org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX;
+import static org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX;
import static org.thoughtcrime.securesms.database.GroupDatabase.GROUP_ID;
import android.content.ContentValues;
@@ -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;
@@ -427,7 +417,7 @@ public class ThreadDatabase extends Database {
}
Cursor cursor = cursors.size() > 1 ? new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) : cursors.get(0);
- setNotifyConverationListListeners(cursor);
+ setNotifyConversationListListeners(cursor);
return cursor;
}
@@ -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())
@@ -491,7 +481,7 @@ public class ThreadDatabase extends Database {
}
public Cursor getConversationList() {
- String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
+ String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + COMMUNITY_PREFIX + "%') " +
"AND " + ARCHIVED + " = 0 ";
return getConversationList(where);
}
@@ -502,7 +492,7 @@ public class ThreadDatabase extends Database {
}
public Cursor getApprovedConversationList() {
- String where = "((" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%') OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
+ String where = "((" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%') OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + COMMUNITY_PREFIX + "%') " +
"AND " + ARCHIVED + " = 0 ";
return getConversationList(where);
}
@@ -515,18 +505,12 @@ public class ThreadDatabase extends Database {
return getConversationList(where);
}
- public Cursor getArchivedConversationList() {
- String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
- "AND " + ARCHIVED + " = 1 ";
- return getConversationList(where);
- }
-
private Cursor getConversationList(String where) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = createQuery(where, 0);
Cursor cursor = db.rawQuery(query, null);
- setNotifyConverationListListeners(cursor);
+ setNotifyConversationListListeners(cursor);
return cursor;
}
@@ -547,7 +531,7 @@ public class ThreadDatabase extends Database {
// edge case where we set the last seen time for a conversation before it loads messages (joining community for example)
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
Recipient forThreadId = getRecipientForThreadId(threadId);
- if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isOpenGroupRecipient()) return false;
+ if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isCommunityRecipient()) return false;
SQLiteDatabase db = databaseHelper.getWritableDatabase();
@@ -601,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()) {
@@ -742,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);
@@ -750,10 +734,7 @@ public class ThreadDatabase extends Database {
return true;
}
- MmsSmsDatabase.Reader reader = null;
-
- try {
- reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId));
+ try (MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId))) {
MessageRecord record = null;
if (reader != null) {
record = reader.getNext();
@@ -771,11 +752,10 @@ public class ThreadDatabase extends Database {
deleteThread(threadId);
return true;
}
+ // todo: add empty snippet that clears existing data
return false;
}
} finally {
- if (reader != null)
- reader.close();
notifyConversationListListeners();
notifyConversationListeners(threadId);
}
@@ -820,9 +800,9 @@ 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.isOpenGroupRecipient();
+ return threadRecipient != null && !threadRecipient.isCommunityRecipient();
}
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
@@ -865,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;
@@ -910,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;
@@ -926,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));
@@ -944,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 39fba182aa..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
@@ -22,7 +22,10 @@ import android.text.SpannableString;
import androidx.annotation.NonNull;
import org.session.libsession.utilities.recipients.Recipient;
+import org.session.libsignal.utilities.Log;
+import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
+import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
/**
@@ -48,6 +51,9 @@ public abstract class DisplayRecord {
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
long type, int readReceiptCount)
{
+ // TODO: This gets hit very, very often and it likely shouldn't - place a Log.d statement in it to see.
+ //Log.d("[ACL]", "Creating a display record with delivery status of: " + deliveryStatus);
+
this.threadId = threadId;
this.recipient = recipient;
this.dateSent = dateSent;
@@ -72,13 +78,11 @@ 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 !isFailed() && !isPending();
- }
+ public boolean isSent() { return MmsSmsColumns.Types.isSentType(type); }
public boolean isSyncing() {
return MmsSmsColumns.Types.isSyncingType(type);
@@ -99,9 +103,10 @@ public abstract class DisplayRecord {
}
public boolean isPending() {
- return MmsSmsColumns.Types.isPendingMessageType(type)
- && !MmsSmsColumns.Types.isIdentityVerified(type)
- && !MmsSmsColumns.Types.isIdentityDefault(type);
+ boolean isPending = MmsSmsColumns.Types.isPendingMessageType(type) &&
+ !MmsSmsColumns.Types.isIdentityVerified(type) &&
+ !MmsSmsColumns.Types.isIdentityDefault(type);
+ return isPending;
}
public boolean isRead() { return readReceiptCount > 0; }
@@ -109,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/groups/CreateGroupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt
index ecd40938a1..75c7681b1d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt
@@ -78,7 +78,7 @@ class CreateGroupFragment : Fragment() {
if (name.isEmpty()) {
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
}
- if (name.length >= 30) {
+ if (name.length > resources.getInteger(R.integer.max_group_and_community_name_length_chars)) {
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show()
}
val selectedMembers = adapter.selectedMembers
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
index 702bf33929..82b9f16dcd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
@@ -11,7 +11,6 @@ import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
-import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.util.getConversationUnread
import javax.inject.Inject
@@ -75,7 +74,7 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
}
binding.copyConversationId.visibility = if (!recipient.isGroupRecipient && !recipient.isLocalNumber) View.VISIBLE else View.GONE
binding.copyConversationId.setOnClickListener(this)
- binding.copyCommunityUrl.visibility = if (recipient.isOpenGroupRecipient) View.VISIBLE else View.GONE
+ binding.copyCommunityUrl.visibility = if (recipient.isCommunityRecipient) View.VISIBLE else View.GONE
binding.copyCommunityUrl.setOnClickListener(this)
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
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/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
index 850f065728..ccfa16beef 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
@@ -293,7 +293,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
val newData = contactResults + messageResults
-
globalSearchAdapter.setNewData(result.query, newData)
}
}
@@ -496,7 +495,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
manager.setPrimaryClip(clip)
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
- else if (thread.recipient.isOpenGroupRecipient) {
+ else if (thread.recipient.isCommunityRecipient) {
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) ?: return@onCopyConversationId Unit
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt
index 2922044435..1f3f2ff537 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt
@@ -21,6 +21,7 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityPathBinding
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.getColorFromAttr
+import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Snode
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.util.GlowViewUtilities
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
index ad8f2d0421..bd38d0df86 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
@@ -25,7 +25,6 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.IdPrefix
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.ThreadDatabase
-import org.thoughtcrime.securesms.mms.GlideApp
import javax.inject.Inject
@AndroidEntryPoint
@@ -34,6 +33,9 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
@Inject lateinit var threadDb: ThreadDatabase
private lateinit var binding: FragmentUserDetailsBottomSheetBinding
+
+ private var previousContactNickname: String = ""
+
companion object {
const val ARGUMENT_PUBLIC_KEY = "publicKey"
const val ARGUMENT_THREAD_ID = "threadId"
@@ -89,10 +91,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
&& !threadRecipient.isOpenGroupInboxRecipient
&& !threadRecipient.isOpenGroupOutboxRecipient
- publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
+ publicKeyTextView.isVisible = !threadRecipient.isCommunityRecipient
&& !threadRecipient.isOpenGroupInboxRecipient
&& !threadRecipient.isOpenGroupOutboxRecipient
- messageButton.isVisible = !threadRecipient.isOpenGroupRecipient || IdPrefix.fromValue(publicKey)?.isBlinded() == true
+ messageButton.isVisible = !threadRecipient.isCommunityRecipient || IdPrefix.fromValue(publicKey)?.isBlinded() == true
publicKeyTextView.text = publicKey
publicKeyTextView.setOnLongClickListener {
val clipboard =
@@ -130,9 +132,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
nameTextViewContainer.visibility = View.VISIBLE
nameEditTextContainer.visibility = View.INVISIBLE
var newNickName: String? = null
- if (nicknameEditText.text.isNotEmpty()) {
+ if (nicknameEditText.text.isNotEmpty() && nicknameEditText.text.trim().length != 0) {
newNickName = nicknameEditText.text.toString()
}
+ else { newNickName = previousContactNickname }
val publicKey = recipient.address.serialize()
val storage = MessagingModuleConfiguration.shared.storage
val contact = storage.getContactWithSessionID(publicKey) ?: Contact(publicKey)
@@ -145,6 +148,9 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
fun showSoftKeyboard() {
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.showSoftInput(binding.nicknameEditText, 0)
+
+ // Keep track of the original nickname to re-use if an empty / blank nickname is entered
+ previousContactNickname = binding.nameTextView.text.toString()
}
fun hideSoftKeyboard() {
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/notifications/MultipleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java
index a4871f0fc9..cad3b6f6c5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java
@@ -53,7 +53,7 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
public void setMostRecentSender(Recipient recipient, Recipient threadRecipient) {
String displayName = recipient.toShortString();
if (threadRecipient.isGroupRecipient()) {
- displayName = getGroupDisplayName(recipient, threadRecipient.isOpenGroupRecipient());
+ displayName = getGroupDisplayName(recipient, threadRecipient.isCommunityRecipient());
}
if (privacy.isDisplayContact()) {
setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, displayName));
@@ -79,7 +79,7 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
public void addMessageBody(@NonNull Recipient sender, Recipient threadRecipient, @Nullable CharSequence body) {
String displayName = sender.toShortString();
if (threadRecipient.isGroupRecipient()) {
- displayName = getGroupDisplayName(sender, threadRecipient.isOpenGroupRecipient());
+ displayName = getGroupDisplayName(sender, threadRecipient.isCommunityRecipient());
}
if (privacy.isDisplayMessage()) {
SpannableStringBuilder builder = new SpannableStringBuilder();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java
index 891d0bb2df..2aaa593b58 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java
@@ -125,7 +125,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
- String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient());
+ String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isCommunityRecipient());
stringBuilder.append(Util.getBoldedString(displayName + ": "));
}
@@ -215,7 +215,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
- String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient());
+ String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isCommunityRecipient());
stringBuilder.append(Util.getBoldedString(displayName + ": "));
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt
deleted file mode 100644
index 051cd7542e..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package org.thoughtcrime.securesms.onboarding
-
-import android.content.Intent
-import android.graphics.Typeface
-import android.net.Uri
-import android.os.Bundle
-import android.text.Spannable
-import android.text.SpannableStringBuilder
-import android.text.method.LinkMovementMethod
-import android.text.style.ClickableSpan
-import android.text.style.StyleSpan
-import android.view.View
-import android.widget.Toast
-import dagger.hilt.android.AndroidEntryPoint
-import network.loki.messenger.R
-import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
-import org.session.libsession.snode.SnodeModule
-import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsignal.crypto.MnemonicCodec
-import org.session.libsignal.database.LokiAPIDatabaseProtocol
-import org.session.libsignal.utilities.Hex
-import org.session.libsignal.utilities.KeyHelper
-import org.session.libsignal.utilities.hexEncodedPublicKey
-import org.thoughtcrime.securesms.BaseActionBarActivity
-import org.thoughtcrime.securesms.crypto.KeyPairUtilities
-import org.thoughtcrime.securesms.crypto.MnemonicUtilities
-import org.thoughtcrime.securesms.dependencies.ConfigFactory
-import org.thoughtcrime.securesms.util.push
-import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
-
- @Inject
- lateinit var configFactory: ConfigFactory
-
- private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
- internal val database: LokiAPIDatabaseProtocol
- get() = SnodeModule.shared.storage
- // region Lifecycle
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setUpActionBarSessionLogo()
- TextSecurePreferences.apply {
- setHasViewedSeed(this@RecoveryPhraseRestoreActivity, true)
- setConfigurationMessageSynced(this@RecoveryPhraseRestoreActivity, false)
- setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
- setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
- }
- binding = ActivityRecoveryPhraseRestoreBinding.inflate(layoutInflater)
- setContentView(binding.root)
- binding.mnemonicEditText.imeOptions = binding.mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
- binding.restoreButton.setOnClickListener { restore() }
- val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
- termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- termsExplanation.setSpan(object : ClickableSpan() {
-
- override fun onClick(widget: View) {
- openURL("https://getsession.org/terms-of-service/")
- }
- }, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- termsExplanation.setSpan(object : ClickableSpan() {
-
- override fun onClick(widget: View) {
- openURL("https://getsession.org/privacy-policy/")
- }
- }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
- binding.termsTextView.text = termsExplanation
- }
- // endregion
-
- // region Interaction
- private fun restore() {
- val mnemonic = binding.mnemonicEditText.text.toString()
- try {
- // This is here to resolve a case where the app restarts before a user completes onboarding
- // which can result in an invalid database state
- database.clearAllLastMessageHashes()
- database.clearReceivedMessageHashValues()
-
- val loadFileContents: (String) -> String = { fileName ->
- MnemonicUtilities.loadFileContents(this, fileName)
- }
- val hexEncodedSeed = MnemonicCodec(loadFileContents).decode(mnemonic)
- val seed = Hex.fromStringCondensed(hexEncodedSeed)
- val keyPairGenerationResult = KeyPairUtilities.generate(seed)
- val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
- KeyPairUtilities.store(this, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair)
- configFactory.keyPairChanged()
- val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
- val registrationID = KeyHelper.generateRegistrationId(false)
- TextSecurePreferences.setLocalRegistrationId(this, registrationID)
- TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
- val intent = Intent(this, DisplayNameActivity::class.java)
- push(intent)
- } catch (e: Exception) {
- val message = if (e is MnemonicCodec.DecodingError) e.description else MnemonicCodec.DecodingError.Generic.description
- return Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
- }
- }
-
- private fun openURL(url: String) {
- try {
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
- startActivity(intent)
- } catch (e: Exception) {
- Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
- }
- }
- // endregion
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt
index f6efd041cd..e7e5f2d5f6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt
@@ -5,9 +5,14 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
+import android.widget.ProgressBar
+import android.widget.TextView
import android.widget.Toast
+import androidx.core.view.isInvisible
import androidx.preference.Preference
+
import network.loki.messenger.R
+import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.permissions.Permissions
@@ -67,6 +72,19 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() {
}
}
+ private fun updateExportButtonAndProgressBarUI(exportJobRunning: Boolean) {
+ this.activity?.runOnUiThread(Runnable {
+ // Change export logs button text
+ val exportLogsButton = this.activity?.findViewById(R.id.export_logs_button) as TextView?
+ if (exportLogsButton == null) { Log.w("Loki", "Could not find export logs button view.") }
+ exportLogsButton?.text = if (exportJobRunning) getString(R.string.cancel) else getString(R.string.activity_help_settings__export_logs)
+
+ // Show progress bar
+ val exportProgressBar = this.activity?.findViewById(R.id.export_progress_bar) as ProgressBar?
+ exportProgressBar?.isInvisible = !exportJobRunning
+ })
+ }
+
private fun shareLogs() {
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@@ -76,7 +94,7 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() {
Toast.makeText(requireActivity(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()
}
.onAllGranted {
- ShareLogsDialog().show(parentFragmentManager,"Share Logs Dialog")
+ ShareLogsDialog(::updateExportButtonAndProgressBarUI).show(parentFragmentManager,"Share Logs Dialog")
}
.execute()
}
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 062a8d44dd..2a45d596d6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
@@ -17,6 +17,7 @@ import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
+import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.view.isVisible
@@ -203,6 +204,21 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
binding.displayNameEditText.selectAll()
binding.displayNameEditText.requestFocus()
inputMethodManager.showSoftInput(binding.displayNameEditText, 0)
+
+ // Save the updated display name when the user presses enter on the soft keyboard
+ binding.displayNameEditText.setOnEditorActionListener { v, actionId, event ->
+ when (actionId) {
+ // Note: IME_ACTION_DONE is how we've configured the soft keyboard to respond,
+ // while IME_ACTION_UNSPECIFIED is what triggers when we hit enter on a
+ // physical keyboard.
+ EditorInfo.IME_ACTION_DONE, EditorInfo.IME_ACTION_UNSPECIFIED -> {
+ saveDisplayName()
+ displayNameEditActionMode?.finish()
+ true
+ }
+ else -> false
+ }
+ }
} else {
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt
index 2dc5e75d98..9bfc1dabf2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt
@@ -11,55 +11,73 @@ import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.webkit.MimeTypeMap
+import android.widget.ProgressBar
+import android.widget.TextView
import android.widget.Toast
+import androidx.core.view.isInvisible
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
+
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+
import network.loki.messenger.BuildConfig
import network.loki.messenger.R
+
import org.session.libsignal.utilities.ExternalStorageUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.util.FileProviderUtil
import org.thoughtcrime.securesms.util.StreamUtil
+
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.Objects
import java.util.concurrent.TimeUnit
-class ShareLogsDialog : DialogFragment() {
+class ShareLogsDialog(private val updateCallback: (Boolean)->Unit): DialogFragment() {
+
+ private val TAG = "ShareLogsDialog"
private var shareJob: Job? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
title(R.string.dialog_share_logs_title)
text(R.string.dialog_share_logs_explanation)
- button(R.string.share, dismiss = false) { shareLogs() }
- cancelButton { dismiss() }
+ button(R.string.share, dismiss = false) { runShareLogsJob() }
+ cancelButton { updateCallback(false) }
}
- private fun shareLogs() {
+ // If the share logs dialog loses focus the job gets cancelled so we'll update the UI state
+ override fun onPause() {
+ super.onPause()
+ updateCallback(false)
+ }
+
+ private fun runShareLogsJob() {
+ // Cancel any existing share job that might already be running to start anew
shareJob?.cancel()
+
+ updateCallback(true)
+
shareJob = lifecycleScope.launch(Dispatchers.IO) {
val persistentLogger = ApplicationContext.getInstance(context).persistentLogger
try {
+ Log.d(TAG, "Starting share logs job...")
+
val context = requireContext()
val outputUri: Uri = ExternalStorageUtil.getDownloadUri()
- val mediaUri = getExternalFile()
- if (mediaUri == null) {
- // show toast saying media saved
- dismiss()
- return@launch
- }
+ val mediaUri = getExternalFile() ?: return@launch
val inputStream = persistentLogger.logs.get().byteInputStream()
val updateValues = ContentValues()
+
+ // Add details into the output or media files as appropriate
if (outputUri.scheme == ContentResolver.SCHEME_FILE) {
FileOutputStream(mediaUri.path).use { outputStream ->
StreamUtil.copy(inputStream, outputStream)
@@ -73,6 +91,7 @@ class ShareLogsDialog : DialogFragment() {
}
}
}
+
if (Build.VERSION.SDK_INT > 28) {
updateValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
}
@@ -95,13 +114,35 @@ class ShareLogsDialog : DialogFragment() {
}
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
}
-
- dismiss()
} catch (e: Exception) {
withContext(Main) {
Log.e("Loki", "Error saving logs", e)
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
}
+ }
+ }.also { shareJob ->
+ shareJob.invokeOnCompletion { handler ->
+ // Note: Don't show Toasts here directly - use `withContext(Main)` or such if req'd
+ handler?.message.let { msg ->
+ if (shareJob.isCancelled) {
+ if (msg.isNullOrBlank()) {
+ Log.w(TAG, "Share logs job was cancelled.")
+ } else {
+ Log.d(TAG, "Share logs job was cancelled. Reason: $msg")
+ }
+
+ }
+ else if (shareJob.isCompleted) {
+ Log.d(TAG, "Share logs job completed. Msg: $msg")
+ }
+ else {
+ Log.w(TAG, "Share logs job finished while still Active. Msg: $msg")
+ }
+ }
+
+ // Regardless of the job's success it has now completed so update the UI
+ updateCallback(false)
+
dismiss()
}
}
@@ -158,5 +199,4 @@ class ShareLogsDialog : DialogFragment() {
return context.contentResolver.insert(outputUri, contentValues)
}
-
}
\ No newline at end of file
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 f6277d1a4a..8a7a2dfd0f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
@@ -1,11 +1,15 @@
package org.thoughtcrime.securesms.repository
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
@@ -22,6 +26,7 @@ import org.session.libsession.utilities.Address
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
@@ -40,9 +45,6 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import javax.inject.Inject
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlin.coroutines.suspendCoroutine
interface ConversationRepository {
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
@@ -55,37 +57,19 @@ interface ConversationRepository {
fun inviteContacts(threadId: Long, contacts: List)
fun setBlocked(recipient: Recipient, blocked: Boolean)
fun deleteLocally(recipient: Recipient, message: MessageRecord)
+ fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord)
fun setApproved(recipient: Recipient, isApproved: Boolean)
-
- suspend fun deleteForEveryone(
- threadId: Long,
- recipient: Recipient,
- message: MessageRecord
- ): ResultOf
-
+ suspend fun deleteForEveryone(threadId: Long, recipient: Recipient, message: MessageRecord): ResultOf
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
-
- suspend fun deleteMessageWithoutUnsendRequest(
- threadId: Long,
- messages: Set
- ): ResultOf
-
+ suspend fun deleteMessageWithoutUnsendRequest(threadId: Long, messages: Set): ResultOf
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf
-
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf
-
suspend fun deleteThread(threadId: Long): ResultOf
-
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf
-
suspend fun clearAllMessageRequests(block: Boolean): ResultOf
-
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf
-
fun declineMessageRequest(threadId: Long)
-
fun hasReceived(threadId: Long): Boolean
-
}
class DefaultConversationRepository @Inject constructor(
@@ -184,6 +168,15 @@ class DefaultConversationRepository @Inject constructor(
messageDataProvider.deleteMessage(message.id, !message.isMms)
}
+ override fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) {
+ val threadId = messageRecord.threadId
+ val senderId = messageRecord.recipient.address.contactIdentifier()
+ val messageRecordsToRemoveFromLocalStorage = mmsSmsDb.getAllMessageRecordsFromSenderInThread(threadId, senderId)
+ for (message in messageRecordsToRemoveFromLocalStorage) {
+ messageDataProvider.deleteMessage(message.id, !message.isMms)
+ }
+ }
+
override fun setApproved(recipient: Recipient, isApproved: Boolean) {
storage.setRecipientApproved(recipient, isApproved)
}
@@ -196,18 +189,38 @@ class DefaultConversationRepository @Inject constructor(
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
MessageSender.send(unsendRequest, recipient.address)
}
+
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
if (openGroup != null) {
- lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
+ val serverId = lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success {
messageDataProvider.deleteMessage(message.id, !message.isMms)
continuation.resume(ResultOf.Success(Unit))
}.fail { error ->
+ Log.w("TAG", "Call to OpenGroupApi.deleteForEveryone failed - attempting to resume..")
continuation.resumeWithException(error)
}
}
- } else {
+
+ // If the server ID is null then this message is stuck in limbo (it has likely been
+ // deleted remotely but that deletion did not occur locally) - so we'll delete the
+ // message locally to clean up.
+ if (serverId == null) {
+ Log.w("ConversationRepository","Found community message without a server ID - deleting locally.")
+
+ // Caution: The bool returned from `deleteMessage` is NOT "Was the message
+ // successfully deleted?" - it is "Was the thread itself also deleted because
+ // removing that message resulted in an empty thread?".
+ if (message.isMms) {
+ mmsDb.deleteMessage(message.id)
+ } else {
+ smsDb.deleteMessage(message.id)
+ }
+ }
+ }
+ else // If this thread is NOT in a Community
+ {
messageDataProvider.deleteMessage(message.id, !message.isMms)
messageDataProvider.getServerHashForMessage(message.id, message.isMms)?.let { serverHash ->
var publicKey = recipient.address.serialize()
@@ -218,6 +231,7 @@ class DefaultConversationRepository @Inject constructor(
.success {
continuation.resume(ResultOf.Success(Unit))
}.fail { error ->
+ Log.w("ConversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..")
continuation.resumeWithException(error)
}
}
@@ -225,7 +239,7 @@ class DefaultConversationRepository @Inject constructor(
}
override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? {
- if (recipient.isOpenGroupRecipient) return null
+ if (recipient.isCommunityRecipient) return null
messageDataProvider.getServerHashForMessage(message.id, message.isMms) ?: return null
return UnsendRequest(
author = message.takeUnless { it.isOutgoing }?.run { individualRecipient.address.contactIdentifier() } ?: textSecurePreferences.getLocalNumber(),
@@ -279,8 +293,10 @@ class DefaultConversationRepository @Inject constructor(
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf =
suspendCoroutine { continuation ->
+ // Note: This sessionId could be the blinded Id
val sessionID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
+
OpenGroupApi.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
.success {
continuation.resume(ResultOf.Success(Unit))
@@ -306,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)
@@ -335,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 ddfe85515c..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,25 +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);
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);
@@ -113,22 +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(() -> {
- long startTime = System.currentTimeMillis();
- CursorList messages = queryMessages(sanitizeQuery(query), threadId);
- Log.d(TAG, "[ConversationQuery] " + (System.currentTimeMillis() - startTime) + " ms");
-
+ 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<>();
@@ -155,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);
@@ -178,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();
}
@@ -215,7 +194,7 @@ public class SearchRepository {
out.append(' ');
}
}
-
+
return out.toString();
}
@@ -245,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) {
@@ -290,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/ConfigurationMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt
index 297014d86c..9d10cfdab5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt
@@ -193,7 +193,7 @@ object ConfigurationMessageUtilities {
while (current != null) {
val recipient = current.recipient
val contact = when {
- recipient.isOpenGroupRecipient -> {
+ recipient.isCommunityRecipient -> {
val openGroup = storage.getOpenGroup(current.threadId) ?: continue
val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue
convoConfig.getOrConstructCommunity(base, room, pubKey)
@@ -279,7 +279,7 @@ object ConfigurationMessageUtilities {
@JvmField
val DELETE_INACTIVE_ONE_TO_ONES: String = """
- DELETE FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.OPEN_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.OPEN_GROUP_INBOX_PREFIX}%';
+ DELETE FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.COMMUNITY_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.COMMUNITY_INBOX_PREFIX}%';
""".trimIndent()
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt
index a38c93831e..9124765763 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt
@@ -37,3 +37,8 @@ val RecyclerView.isScrolledToBottom: Boolean
get() = computeVerticalScrollOffset().coerceAtLeast(0) +
computeVerticalScrollExtent() +
toPx(50, resources) >= computeVerticalScrollRange()
+
+val RecyclerView.isScrolledToWithin30dpOfBottom: Boolean
+ get() = computeVerticalScrollOffset().coerceAtLeast(0) +
+ computeVerticalScrollExtent() +
+ toPx(30, resources) >= computeVerticalScrollRange()
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt
index b15d82a33e..3984f38b51 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt
@@ -14,7 +14,7 @@ fun ConversationVolatileConfig.getConversationUnread(thread: ThreadRecord): Bool
return getOneToOne(recipient.address.serialize())?.unread == true
} else if (recipient.isClosedGroupRecipient) {
return getLegacyClosedGroup(GroupUtil.doubleDecodeGroupId(recipient.address.toGroupString()))?.unread == true
- } else if (recipient.isOpenGroupRecipient) {
+ } else if (recipient.isCommunityRecipient) {
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(thread.threadId) ?: return false
return getCommunity(openGroup.server, openGroup.room)?.unread == true
}
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/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt
index 272f2c12db..ff5e481895 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt
@@ -408,6 +408,10 @@ class CallManager(
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
localCameraState = newCameraState
+
+ // If the camera we've switched to is the front one then mirror it to match what someone
+ // would see when looking in the mirror rather than the left<-->right flipped version.
+ localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT)
}
fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) {
@@ -639,7 +643,11 @@ class CallManager(
peerConnection?.let { connection ->
connection.flipCamera()
localCameraState = connection.getCameraState()
- localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT)
+
+ // Note: We cannot set the mirrored state of the localRenderer here because
+ // localCameraState.activeDirection is still PENDING (not FRONT or BACK) until the flip
+ // completes and we hit Camera.onCameraSwitchDone (followed by PeerConnectionWrapper.onCameraSwitchCompleted
+ // and CallManager.onCameraSwitchCompleted).
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt
index 4835ab0dc1..f78b93d6b9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt
@@ -326,8 +326,6 @@ class PeerConnectionWrapper(private val context: Context,
}
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
- // mirror rotation offset
- rotationVideoSink.mirrored = newCameraState.activeDirection == CameraState.Direction.FRONT
cameraEventListener.onCameraSwitchCompleted(newCameraState)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/Camera.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/Camera.kt
index 421c144199..b76b168977 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/Camera.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/Camera.kt
@@ -54,7 +54,7 @@ class Camera(context: Context,
Log.w(TAG, "Tried to flip camera without capturer or less than 2 cameras")
return
}
- activeDirection = PENDING
+ activeDirection = PENDING // Note: The activeDirection will be PENDING until `onCameraSwitchDone`
capturer.switchCamera(this)
}
diff --git a/app/src/main/res/drawable/cross.xml b/app/src/main/res/drawable/cross.xml
new file mode 100644
index 0000000000..5b090de2b3
--- /dev/null
+++ b/app/src/main/res/drawable/cross.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
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-sw400dp/activity_display_name.xml b/app/src/main/res/layout-sw400dp/activity_display_name.xml
index 4d4ff30406..d62faca064 100644
--- a/app/src/main/res/layout-sw400dp/activity_display_name.xml
+++ b/app/src/main/res/layout-sw400dp/activity_display_name.xml
@@ -43,6 +43,8 @@
android:paddingBottom="0dp"
android:gravity="center_vertical"
android:inputType="textCapWords"
+ android:maxLength="@integer/max_user_nickname_length_chars"
+ android:maxLines="1"
android:hint="@string/activity_display_name_edit_text_hint" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml
index 000b860841..6fe0c4db60 100644
--- a/app/src/main/res/layout/activity_conversation_v2.xml
+++ b/app/src/main/res/layout/activity_conversation_v2.xml
@@ -23,6 +23,10 @@
+
+
@@ -49,6 +50,7 @@
android:inputType="text"
android:singleLine="true"
android:imeOptions="actionDone"
+ android:maxLength="@integer/max_group_and_community_name_length_chars"
android:contentDescription="@string/AccessibilityId_group_name"
android:hint="@string/activity_edit_closed_group_edit_text_hint" />
@@ -57,6 +59,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
+ android:layout_marginRight="@dimen/medium_spacing"
android:contentDescription="@string/AccessibilityId_accept_name_change"
android:src="@drawable/ic_baseline_done_24"/>
diff --git a/app/src/main/res/layout/activity_recovery_phrase_restore.xml b/app/src/main/res/layout/activity_recovery_phrase_restore.xml
deleted file mode 100644
index 5f2012e9c1..0000000000
--- a/app/src/main/res/layout/activity_recovery_phrase_restore.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 44df7e82ef..d84f183b5c 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -47,7 +47,11 @@
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:visibility="invisible"
- android:hint="@string/activity_settings_display_name_edit_text_hint" />
+ android:hint="@string/activity_settings_display_name_edit_text_hint"
+ android:imeOptions="actionDone"
+ android:inputType="textCapWords"
+ android:maxLength="@integer/max_user_nickname_length_chars"
+ android:maxLines="1" />
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/export_logs_widget.xml b/app/src/main/res/layout/export_logs_widget.xml
index 95c681d397..56f6bc07df 100644
--- a/app/src/main/res/layout/export_logs_widget.xml
+++ b/app/src/main/res/layout/export_logs_widget.xml
@@ -1,9 +1,10 @@
-
+
+ android:layout_height="wrap_content" />
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_create_group.xml b/app/src/main/res/layout/fragment_create_group.xml
index af685fa9f6..1e901c753c 100644
--- a/app/src/main/res/layout/fragment_create_group.xml
+++ b/app/src/main/res/layout/fragment_create_group.xml
@@ -62,10 +62,14 @@
android:layout_marginBottom="@dimen/medium_spacing"
android:contentDescription="@string/AccessibilityId_group_name_input"
android:hint="@string/activity_create_closed_group_edit_text_hint"
- android:maxLength="30"
+ android:imeOptions="actionDone"
+ android:inputType="textCapWords"
+ android:maxLength="@integer/max_group_and_community_name_length_chars"
+ android:maxLines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/titleText" />
+ app:layout_constraintTop_toBottomOf="@id/titleText"
+ tools:ignore="ContentDescription" />
@@ -73,6 +77,7 @@
android:id="@+id/cancelNicknameEditingButton"
android:layout_width="24dp"
android:layout_height="24dp"
+ android:layout_marginLeft="@dimen/large_spacing"
android:contentDescription="@string/AccessibilityId_cancel"
android:src="@drawable/ic_baseline_clear_24" />
@@ -82,12 +87,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:layout_marginHorizontal="@dimen/small_spacing"
android:contentDescription="@string/AccessibilityId_username"
android:textAlignment="center"
android:paddingVertical="12dp"
android:inputType="text"
- android:singleLine="true"
+ android:maxLength="@integer/max_user_nickname_length_chars"
+ android:maxLines="1"
android:imeOptions="actionDone"
android:textColorHint="?android:textColorSecondary"
android:hint="@string/fragment_user_details_bottom_sheet_edit_text_hint" />
@@ -96,6 +101,7 @@
android:id="@+id/saveNicknameButton"
android:layout_width="24dp"
android:layout_height="24dp"
+ android:layout_marginRight="@dimen/large_spacing"
android:contentDescription="@string/AccessibilityId_apply"
android:src="@drawable/ic_baseline_done_24" />
diff --git a/app/src/main/res/layout/preference_widget_progress.xml b/app/src/main/res/layout/preference_widget_progress.xml
index 3cce4b9483..4b44b9a55a 100644
--- a/app/src/main/res/layout/preference_widget_progress.xml
+++ b/app/src/main/res/layout/preference_widget_progress.xml
@@ -1,23 +1,19 @@
-
+
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/reactions_pill.xml b/app/src/main/res/layout/reactions_pill.xml
index c87f29d82c..a88673cba8 100644
--- a/app/src/main/res/layout/reactions_pill.xml
+++ b/app/src/main/res/layout/reactions_pill.xml
@@ -15,7 +15,7 @@
android:layout_width="17dp"
android:layout_height="17dp" />
-
diff --git a/app/src/main/res/layout/view_contact.xml b/app/src/main/res/layout/view_contact.xml
index 3053046d8c..ceb4304cc7 100644
--- a/app/src/main/res/layout/view_contact.xml
+++ b/app/src/main/res/layout/view_contact.xml
@@ -25,6 +25,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/medium_spacing"
+ android:maxLength="@integer/max_user_nickname_length_chars"
android:maxLines="1"
android:textAlignment="viewStart"
android:ellipsize="end"
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" />
+ tools:text="Sorry, gotta go fight crime again - and more text to make it ellipsize" />
50sp
+ 60dp
34dp
38dp
54dp
diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml
index 4d1a6c4077..bb3cd8a804 100644
--- a/app/src/main/res/values/integers.xml
+++ b/app/src/main/res/values/integers.xml
@@ -6,4 +6,7 @@
100
150
10
+
+ 35
+ 35
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 72dcacf284..412eec96db 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