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 caf22f109a..171c1657f8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.4.2'
+ classpath "com.android.tools.build:gradle:$gradlePluginVersion"
classpath files('libs/gradle-witness.jar')
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 90cbd054b3..4ff3d91f65 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -230,11 +230,13 @@
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
android:screenOrientation="portrait"
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
- android:theme="@style/Theme.Session.DayNight.NoActionBar">
+ android:theme="@style/Theme.Session.DayNight.NoActionBar"
+ android:windowSoftInputMode="adjustResize" >
+
Unit) {
text ?: return
TextView(context, null, 0, style)
@@ -75,6 +74,8 @@ class SessionDialogBuilder(val context: Context) {
}.let(topView::addView)
}
+ fun htmlText(@StringRes id: Int, @StyleRes style: Int = 0, modify: TextView.() -> Unit = {}) { text(context.resources.getText(id)) }
+
fun view(view: View) = contentView.addView(view)
fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView)
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..198fc33372 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
@@ -334,6 +334,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/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/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
index f647e2fe3e..87d5ea4ae7 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
@@ -1286,6 +1330,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
sendEmojiRemoval(emoji, messageRecord)
} else {
sendEmojiReaction(emoji, messageRecord)
+ RecentEmojiPageModel.onCodePointSelected(emoji) // Save to recently used reaction emojis
}
}
@@ -1312,7 +1357,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 +1381,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)
@@ -1731,7 +1776,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)!!
@@ -1797,19 +1842,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
@@ -1828,13 +1915,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)
}
}
@@ -1853,7 +1943,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)
}
}
@@ -1935,7 +2025,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
@@ -2105,4 +2194,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..d336c5fcce 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
@@ -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/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
index 3746aa52e4..2788d35dd2 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
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..a59a102d1c 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,6 +32,7 @@ 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
@@ -131,7 +132,8 @@ class VisibleMessageView : LinearLayout {
senderSessionID: String,
lastSeen: Long,
delegate: VisibleMessageViewDelegate? = null,
- onAttachmentNeedsDownload: (Long, Long) -> Unit
+ onAttachmentNeedsDownload: (Long, Long) -> Unit,
+ lastSentMessageId: Long
) {
val threadID = message.threadId
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
@@ -164,7 +166,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 +179,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 +195,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
showStatusMessage(message)
+
// Emoji Reactions
val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
@@ -238,7 +244,8 @@ class VisibleMessageView : LinearLayout {
}
private fun showStatusMessage(message: MessageRecord) {
- val disappearing = message.expiresIn > 0
+
+ val scheduledToDisappear = message.expiresIn > 0
binding.messageInnerLayout.modifyLayoutParams {
gravity = if (message.isOutgoing) Gravity.END else Gravity.START
@@ -250,21 +257,22 @@ class VisibleMessageView : LinearLayout {
binding.expirationTimerView.isGone = true
- if (message.isOutgoing || disappearing) {
+ if (message.isOutgoing || scheduledToDisappear) {
val (iconID, iconColor, textId) = getMessageStatusImage(message)
textId?.let(binding.messageStatusTextView::setText)
iconColor?.let(binding.messageStatusTextView::setTextColor)
iconID?.let { ContextCompat.getDrawable(context, it) }
?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
?.let(binding.messageStatusImageView::setImageDrawable)
-
- 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)
+
+ // Always show the delivery status of the last sent message
+ val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
+ val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
+ val isLastSentMessage = lastSentMessageId == message.id
+
+ binding.messageStatusTextView.isVisible = textId != null && (isLastSentMessage || scheduledToDisappear)
+ val showTimer = scheduledToDisappear && !message.isPending
+ binding.messageStatusImageView.isVisible = iconID != null && !showTimer && (!message.isSent || isLastSentMessage)
binding.messageStatusImageView.bringToFront()
binding.expirationTimerView.bringToFront()
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..1bf1dcdb15 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,69 @@ 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 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..450e7c2d23 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -621,16 +621,19 @@ 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+""});
+ notifyConversationListeners(threadId);
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
return threadDeleted;
}
@@ -645,9 +648,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,
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 584394a86c..73dadda2cb 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"
@@ -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")
}
@@ -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 (
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..507088a0ad 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;
@@ -427,7 +427,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;
}
@@ -491,7 +491,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 +502,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 +515,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 +541,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();
@@ -750,10 +744,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 +762,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);
}
@@ -822,7 +812,7 @@ public class ThreadDatabase extends Database {
private boolean deleteThreadOnEmpty(long threadId) {
Recipient threadRecipient = getRecipientForThreadId(threadId);
- return threadRecipient != null && !threadRecipient.isOpenGroupRecipient();
+ return threadRecipient != null && !threadRecipient.isCommunityRecipient();
}
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
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..605118f3cc 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;
@@ -76,9 +82,7 @@ public abstract class DisplayRecord {
&& 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; }
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/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
index 6a7eaa508a..67400a7a81 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
@@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
@@ -99,11 +98,11 @@ import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.AppTheme
+import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.OutlineButton
import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.SessionShieldIcon
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
-import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.h8
import org.thoughtcrime.securesms.ui.small
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@@ -226,7 +225,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
// Set up empty state view
- binding.emptyStateContainer.setContent { EmptyView() }
+ binding.emptyStateContainer.setContent { EmptyView(ApplicationContext.getInstance(this).newAccount) }
IP2Country.configureIfNeeded(this@HomeActivity)
startObservingUpdates()
@@ -317,7 +316,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
val newData = contactResults + messageResults
-
globalSearchAdapter.setNewData(result.query, newData)
}
}
@@ -365,16 +363,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
) {
Column(Modifier.weight(1f)) {
Row {
- Text("Save your recovery password", style = MaterialTheme.typography.h8)
+ Text(stringResource(R.string.save_your_recovery_password), style = MaterialTheme.typography.h8)
Spacer(Modifier.requiredWidth(8.dp))
SessionShieldIcon()
}
- Text("Save your recovery password to make sure you don't lose access to your account.", style = MaterialTheme.typography.small)
+ Text(stringResource(R.string.save_your_recovery_password_to_make_sure_you_don_t_lose_access_to_your_account), style = MaterialTheme.typography.small)
}
Spacer(Modifier.width(12.dp))
OutlineButton(
stringResource(R.string.continue_2),
- Modifier.align(Alignment.CenterVertically)
+ Modifier.align(Alignment.CenterVertically),
+ contentDescription = GetString(R.string.AccessibilityId_reveal_recovery_phrase_button)
) { startRecoveryPasswordActivity() }
}
}
@@ -382,7 +381,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
@Composable
- private fun EmptyView() {
+ private fun EmptyView(newAccount: Boolean) {
AppTheme {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -392,18 +391,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
) {
Spacer(modifier = Modifier.weight(1f))
Icon(
- painter = painterResource(id = R.drawable.emoji_tada),
+ painter = painterResource(id = if (newAccount) R.drawable.emoji_tada_large else R.drawable.ic_logo_large),
contentDescription = null,
tint = Color.Unspecified
)
- Text("Account Created", style = MaterialTheme.typography.h4, textAlign = TextAlign.Center)
- Text("Welcome to Session", color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
+ if (newAccount) Text(stringResource(R.string.onboardingAccountCreated), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center)
+ if (newAccount) Text(stringResource(R.string.welcome_to_session), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
+
Divider(modifier = Modifier.padding(vertical = 16.dp))
- Text("You don't have any conversations yet",
+ Text(
+ stringResource(R.string.conversationsNone),
style = MaterialTheme.typography.h8,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 12.dp))
- Text("Hit the plus button to start a chat, create a group, or join an official communitiy!", textAlign = TextAlign.Center)
+ Text(stringResource(R.string.onboardingHitThePlusButton), textAlign = TextAlign.Center)
Spacer(modifier = Modifier.weight(2f))
}
}
@@ -585,7 +586,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
@@ -718,7 +719,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
val message = if (recipient.isGroupRecipient) {
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) {
- "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
+ getString(R.string.admin_group_leave_warning)
} else {
resources.getString(R.string.activity_home_leave_group_dialog_message)
}
@@ -774,7 +775,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private fun hideMessageRequests() {
showSessionDialog {
- text("Hide message requests?")
+ text(getString(R.string.hide_message_requests))
button(R.string.yes) {
textSecurePreferences.setHasHiddenMessageRequests()
setupMessageRequestsBanner()
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/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/LandingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt
index dedb0ed85e..8ce485a9b5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import network.loki.messenger.R
@@ -34,7 +35,10 @@ import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.AppTheme
import org.thoughtcrime.securesms.ui.BorderlessButton
import org.thoughtcrime.securesms.ui.FilledButton
+import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.OutlineButton
+import org.thoughtcrime.securesms.ui.PreviewTheme
+import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
import org.thoughtcrime.securesms.ui.classicDarkColors
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.session_accent
@@ -52,7 +56,7 @@ class LandingActivity : BaseActionBarActivity() {
setUpActionBarSessionLogo(true)
ComposeView(this)
- .apply { setContent { LandingScreen() } }
+ .apply { setContent { AppTheme { LandingScreen() } } }
.let(::setContentView)
IdentityKeyUtil.generateIdentityKeyPair(this)
@@ -63,41 +67,55 @@ class LandingActivity : BaseActionBarActivity() {
@Preview
@Composable
- private fun LandingScreen() {
- AppTheme {
- Column(modifier = Modifier.padding(horizontal = 36.dp)) {
- Spacer(modifier = Modifier.weight(1f))
- Text(stringResource(R.string.onboardingBubblePrivacyInYourPocket), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center)
- Spacer(modifier = Modifier.height(24.dp))
- IncomingText(stringResource(R.string.onboardingBubbleWelcomeToSession))
- Spacer(modifier = Modifier.height(14.dp))
- OutgoingText(stringResource(R.string.onboardingBubbleSessionIsEngineered))
- Spacer(modifier = Modifier.height(14.dp))
- IncomingText(stringResource(R.string.onboardingBubbleNoPhoneNumber))
- Spacer(modifier = Modifier.height(14.dp))
- OutgoingText(stringResource(R.string.onboardingBubbleCreatingAnAccountIsEasy))
- Spacer(modifier = Modifier.weight(1f))
+ private fun LandingScreen(
+ @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
+ ) {
+ PreviewTheme(themeResId) {
+ LandingScreen()
+ }
+ }
- OutlineButton(
- text = stringResource(R.string.onboardingAccountCreate),
- modifier = Modifier
- .width(262.dp)
- .align(Alignment.CenterHorizontally)) { startPickDisplayNameActivity() }
- Spacer(modifier = Modifier.height(14.dp))
- FilledButton(text = stringResource(R.string.onboardingAccountExists), modifier = Modifier
+ @Composable
+ private fun LandingScreen() {
+ Column(modifier = Modifier.padding(horizontal = 36.dp)) {
+ Spacer(modifier = Modifier.weight(1f))
+ Text(stringResource(R.string.onboardingBubblePrivacyInYourPocket), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center)
+ Spacer(modifier = Modifier.height(24.dp))
+ IncomingText(stringResource(R.string.onboardingBubbleWelcomeToSession))
+ Spacer(modifier = Modifier.height(14.dp))
+ OutgoingText(stringResource(R.string.onboardingBubbleSessionIsEngineered))
+ Spacer(modifier = Modifier.height(14.dp))
+ IncomingText(stringResource(R.string.onboardingBubbleNoPhoneNumber))
+ Spacer(modifier = Modifier.height(14.dp))
+ OutgoingText(stringResource(R.string.onboardingBubbleCreatingAnAccountIsEasy))
+ Spacer(modifier = Modifier.weight(1f))
+
+ OutlineButton(
+ text = stringResource(R.string.onboardingAccountCreate),
+ modifier = Modifier
.width(262.dp)
- .align(Alignment.CenterHorizontally)) { startLinkDeviceActivity() }
- Spacer(modifier = Modifier.height(8.dp))
- BorderlessButton(
- text = stringResource(R.string.onboardingTosPrivacy),
- modifier = Modifier
- .width(262.dp)
- .align(Alignment.CenterHorizontally),
- fontSize = 11.sp,
- lineHeight = 13.sp
- ) { openDialog() }
- Spacer(modifier = Modifier.height(8.dp))
- }
+ .align(Alignment.CenterHorizontally),
+ contentDescription = GetString(R.string.AccessibilityId_create_account_button)
+ ) { startPickDisplayNameActivity() }
+ Spacer(modifier = Modifier.height(14.dp))
+ FilledButton(
+ text = stringResource(R.string.onboardingAccountExists),
+ modifier = Modifier
+ .width(262.dp)
+ .align(Alignment.CenterHorizontally),
+ contentDescription = GetString(R.string.AccessibilityId_restore_account_button)
+ ) { startLinkDeviceActivity() }
+ Spacer(modifier = Modifier.height(8.dp))
+ BorderlessButton(
+ text = stringResource(R.string.onboardingTosPrivacy),
+ modifier = Modifier
+ .width(262.dp)
+ .align(Alignment.CenterHorizontally),
+ contentDescription = GetString(R.string.AccessibilityId_privacy_policy_link),
+ fontSize = 11.sp,
+ lineHeight = 13.sp
+ ) { openDialog() }
+ Spacer(modifier = Modifier.height(8.dp))
}
}
@@ -105,8 +123,14 @@ class LandingActivity : BaseActionBarActivity() {
showSessionDialog {
title(R.string.urlOpen)
text(R.string.urlOpenBrowser)
- button(R.string.activity_landing_terms_of_service) { open("https://getsession.org/terms-of-service") }
- button(R.string.activity_landing_privacy_policy) { open("https://getsession.org/privacy-policy") }
+ button(
+ R.string.activity_landing_terms_of_service,
+ contentDescriptionRes = R.string.AccessibilityId_terms_of_service_link
+ ) { open("https://getsession.org/terms-of-service") }
+ button(
+ R.string.activity_landing_privacy_policy,
+ contentDescriptionRes = R.string.AccessibilityId_privacy_policy_link
+ ) { open("https://getsession.org/privacy-policy") }
}
}
@@ -136,8 +160,8 @@ class LandingActivity : BaseActionBarActivity() {
private fun ChatText(
text: String,
color: Color,
- textColor: Color = Color.Unspecified,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ textColor: Color = Color.Unspecified
) {
Text(
text,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt
index 3ae0ff3253..fc0421edf4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt
@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.ui.baseBold
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
import org.thoughtcrime.securesms.ui.components.SessionTabRow
+import org.thoughtcrime.securesms.ui.contentDescription
import javax.inject.Inject
private const val TAG = "LinkDeviceActivity"
@@ -130,15 +131,22 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on
Spacer(Modifier.size(24.dp))
SessionOutlinedTextField(
text = state.recoveryPhrase,
+ modifier = Modifier
+ .contentDescription(R.string.AccessibilityId_recovery_phrase_input)
+ .padding(horizontal = 64.dp),
placeholder = stringResource(R.string.recoveryPasswordEnter),
onChange = onChange,
onContinue = onContinue,
- error = state.error,
- modifier = Modifier.padding(horizontal = 64.dp)
+ error = state.error
)
Spacer(Modifier.size(12.dp))
state.error?.let {
- Text(it, style = MaterialTheme.typography.baseBold, color = MaterialTheme.colors.error)
+ Text(
+ it,
+ modifier = Modifier.contentDescription(R.string.AccessibilityId_error_message),
+ style = MaterialTheme.typography.baseBold,
+ color = MaterialTheme.colors.error
+ )
}
Spacer(Modifier.weight(2f))
OutlineButton(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt
index e68fcb541a..b37cdc16d7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt
@@ -18,16 +18,21 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.stringResource
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
+import network.loki.messenger.R
+import org.session.libsession.utilities.AppTextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences
+import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.onboarding.messagenotifications.startPNModeActivity
import org.thoughtcrime.securesms.onboarding.pickname.startPickDisplayNameActivity
import org.thoughtcrime.securesms.ui.AppTheme
import org.thoughtcrime.securesms.ui.ProgressArc
+import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
import javax.inject.Inject
@@ -63,6 +68,8 @@ class LoadingActivity: BaseActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ ApplicationContext.getInstance(this).newAccount = false
+
ComposeView(this)
.apply { setContent { LoadingScreen() } }
.let(::setContentView)
@@ -98,9 +105,9 @@ class LoadingActivity: BaseActionBarActivity() {
AppTheme {
Column {
Spacer(modifier = Modifier.weight(1f))
- ProgressArc(animatable.value, modifier = Modifier.align(Alignment.CenterHorizontally))
- Text("One moment please..", modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h6)
- Text("Loading your account", modifier = Modifier.align(Alignment.CenterHorizontally))
+ ProgressArc(animatable.value, modifier = Modifier.align(Alignment.CenterHorizontally).contentDescription(R.string.AccessibilityId_loading_animation))
+ Text(stringResource(R.string.waitOneMoment), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h6)
+ Text(stringResource(R.string.loadAccountProgressMessage), modifier = Modifier.align(Alignment.CenterHorizontally))
Spacer(modifier = Modifier.weight(2f))
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt
index 42fc83f05e..71c1786e4c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt
@@ -38,9 +38,11 @@ import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.home.HomeActivity
import org.thoughtcrime.securesms.notifications.PushRegistry
import org.thoughtcrime.securesms.ui.AppTheme
+import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.OutlineButton
import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
+import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.h8
import org.thoughtcrime.securesms.ui.h9
import org.thoughtcrime.securesms.ui.session_accent
@@ -61,16 +63,16 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
ComposeView(this)
- .apply { setContent { MessageNotifications() } }
+ .apply { setContent { MessageNotificationsScreen() } }
.let(::setContentView)
}
@Composable
- private fun MessageNotifications() {
+ private fun MessageNotificationsScreen() {
val state by viewModel.stateFlow.collectAsState()
AppTheme {
- MessageNotifications(state, viewModel::setEnabled, ::register)
+ MessageNotificationsScreen(state, viewModel::setEnabled, ::register)
}
}
@@ -87,30 +89,31 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
@Preview
@Composable
-fun MessageNotificationsPreview(
+fun MessageNotificationsScreenPreview(
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
) {
PreviewTheme(themeResId) {
- MessageNotifications()
+ MessageNotificationsScreen()
}
}
@Composable
-fun MessageNotifications(
+fun MessageNotificationsScreen(
state: MessageNotificationsState = MessageNotificationsState(),
setEnabled: (Boolean) -> Unit = {},
onContinue: () -> Unit = {}
) {
Column(Modifier.padding(horizontal = 32.dp)) {
Spacer(Modifier.weight(1f))
- Text("Message notifications", style = MaterialTheme.typography.h4)
+ Text(stringResource(R.string.notificationsMessage), style = MaterialTheme.typography.h4)
Spacer(Modifier.height(16.dp))
- Text("There are two ways Session can notify you of new messages.")
+ Text(stringResource(R.string.onboardingMessageNotificationExplaination))
Spacer(Modifier.height(16.dp))
NotificationRadioButton(
R.string.activity_pn_mode_fast_mode,
R.string.activity_pn_mode_fast_mode_explanation,
R.string.activity_pn_mode_recommended_option_tag,
+ contentDescription = R.string.AccessibilityId_fast_mode_notifications_button,
selected = state.pushEnabled,
onClick = { setEnabled(true) }
)
@@ -118,6 +121,7 @@ fun MessageNotifications(
NotificationRadioButton(
R.string.activity_pn_mode_slow_mode,
R.string.activity_pn_mode_slow_mode_explanation,
+ contentDescription = R.string.AccessibilityId_slow_mode_notifications_button,
selected = state.pushDisabled,
onClick = { setEnabled(false) }
)
@@ -138,13 +142,14 @@ fun NotificationRadioButton(
@StringRes title: Int,
@StringRes explanation: Int,
@StringRes tag: Int? = null,
+ @StringRes contentDescription: Int? = null,
selected: Boolean = false,
onClick: () -> Unit = {}
) {
Row {
OutlinedButton(
onClick = onClick,
- modifier = Modifier.weight(1f),
+ modifier = Modifier.weight(1f).contentDescription(contentDescription),
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.background, contentColor = Color.White),
border = if (selected) BorderStroke(ButtonDefaults.OutlinedBorderSize, session_accent) else ButtonDefaults.outlinedBorder,
shape = RoundedCornerShape(8.dp)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt
index 07ed9f7d22..e6a0993222 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt
@@ -31,6 +31,8 @@ import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import network.loki.messenger.R
+import org.session.libsession.utilities.AppTextSecurePreferences
+import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.onboarding.messagenotifications.startPNModeActivity
import org.thoughtcrime.securesms.ui.AppTheme
@@ -105,7 +107,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
OutlinedTextField(
value = state.displayName,
- modifier = Modifier.contentDescription(R.string.displayNameEnter),
+ modifier = Modifier.contentDescription(R.string.AccessibilityId_enter_display_name),
onValueChange = { onChange(it) },
placeholder = { Text(stringResource(R.string.displayNameEnter)) },
colors = outlinedTextFieldColors(state.error != null),
@@ -137,6 +139,8 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
}
fun Context.startPickDisplayNameActivity(failedToLoad: Boolean = false, flags: Int = 0) {
+ ApplicationContext.getInstance(this).newAccount = !failedToLoad
+
Intent(this, PickDisplayNameActivity::class.java)
.apply { putExtra(EXTRA_PICK_NEW_NAME, failedToLoad) }
.also { it.flags = flags }
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt
index 6f15f86ce8..bc9f7efbbf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt
@@ -6,7 +6,6 @@ import android.graphics.Bitmap
import android.os.Bundle
import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -15,7 +14,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
@@ -27,9 +25,13 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
@@ -42,11 +44,16 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.AppTheme
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
+import org.thoughtcrime.securesms.ui.GetString
+import org.thoughtcrime.securesms.ui.LaunchedEffectAsync
import org.thoughtcrime.securesms.ui.LocalExtraColors
import org.thoughtcrime.securesms.ui.OutlineButton
import org.thoughtcrime.securesms.ui.PreviewTheme
@@ -54,8 +61,10 @@ import org.thoughtcrime.securesms.ui.SessionShieldIcon
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
import org.thoughtcrime.securesms.ui.classicDarkColors
import org.thoughtcrime.securesms.ui.colorDestructive
+import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.h8
import org.thoughtcrime.securesms.ui.small
+import kotlin.time.Duration.Companion.seconds
class RecoveryPasswordActivity : BaseActionBarActivity() {
@@ -74,21 +83,22 @@ class RecoveryPasswordActivity : BaseActionBarActivity() {
private fun onHide() {
showSessionDialog {
- title("Hide Recovery Password Permanently")
- text("Without your recovery password, you cannot load your account on new devices.\n" +
- "\n" +
- "We strongly recommend you save your recovery password in a safe and secure place before continuing.")
+ title(R.string.recoveryPasswordHidePermanently)
+ htmlText(R.string.recoveryPasswordHidePermanentlyDescription1)
destructiveButton(R.string.continue_2) { onHideConfirm() }
- button(R.string.cancel) {}
+ cancelButton()
}
}
private fun onHideConfirm() {
showSessionDialog {
- title("Hide Recovery Password Permanently")
- text("Are you sure you want to permanently hide your recovery password on this device? This cannot be undone.")
- button(R.string.cancel) {}
- destructiveButton(R.string.yes) {
+ title(R.string.recoveryPasswordHidePermanently)
+ text(R.string.recoveryPasswordHidePermanentlyDescription2)
+ cancelButton()
+ destructiveButton(
+ R.string.yes,
+ contentDescription = R.string.AccessibilityId_confirm_button
+ ) {
viewModel.permanentlyHidePassword()
finish()
}
@@ -98,7 +108,7 @@ class RecoveryPasswordActivity : BaseActionBarActivity() {
@Preview
@Composable
-fun PreviewMessageDetails(
+fun PreviewRecoveryPassword(
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
) {
PreviewTheme(themeResId) {
@@ -132,31 +142,28 @@ fun RecoveryPasswordCell(seed: String = "", qrBitmap: Bitmap? = null, copySeed:(
mutableStateOf(false)
}
- val copied = remember {
- mutableStateOf(false)
- }
-
CellWithPaddingAndMargin {
Column {
Row {
- Text("Recovery Password")
+ Text(stringResource(R.string.sessionRecoveryPassword))
Spacer(Modifier.width(8.dp))
SessionShieldIcon()
}
- Text("Use your recovery password to load your account on new devices.\n\nYour account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure β and don't share it with anyone.")
+ Text(stringResource(R.string.recoveryPasswordDescription))
AnimatedVisibility(!showQr.value) {
Text(
seed,
modifier = Modifier
- .padding(vertical = 24.dp)
- .border(
- width = 1.dp,
- color = classicDarkColors[3],
- shape = RoundedCornerShape(11.dp)
- )
- .padding(24.dp),
+ .contentDescription(R.string.AccessibilityId_hide_recovery_password_button)
+ .padding(vertical = 24.dp)
+ .border(
+ width = 1.dp,
+ color = classicDarkColors[3],
+ shape = RoundedCornerShape(11.dp)
+ )
+ .padding(24.dp),
style = MaterialTheme.typography.small.copy(fontFamily = FontFamily.Monospace),
color = LocalExtraColors.current.prominentButtonColor,
)
@@ -181,16 +188,21 @@ fun RecoveryPasswordCell(seed: String = "", qrBitmap: Bitmap? = null, copySeed:(
AnimatedVisibility(!showQr.value) {
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
- Crossfade(targetState = if (copied.value) R.string.copied else R.string.copy, modifier = Modifier.weight(1f), label = "Copy to Copied CrossFade") {
- OutlineButton(text = stringResource(it), modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.onPrimary) { copySeed(); copied.value = true }
+ OutlineButton(
+ modifier = Modifier.weight(1f),
+ color = MaterialTheme.colors.onPrimary,
+ onClick = copySeed,
+ temporaryContent = { Text(stringResource(R.string.copied)) }
+ ) {
+ Text(stringResource(R.string.copy))
}
- OutlineButton(text = "View QR", modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) { showQr.toggle() }
+ OutlineButton(text = stringResource(R.string.qrView), modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) { showQr.toggle() }
}
}
AnimatedVisibility(showQr.value, modifier = Modifier.align(Alignment.CenterHorizontally)) {
OutlineButton(
- text = "View Password",
+ text = stringResource(R.string.recoveryPasswordView),
color = MaterialTheme.colors.onPrimary,
modifier = Modifier.align(Alignment.CenterHorizontally)
) { showQr.toggle() }
@@ -229,11 +241,12 @@ fun HideRecoveryPasswordCell(onHide: () -> Unit = {}) {
CellWithPaddingAndMargin {
Row {
Column(Modifier.weight(1f)) {
- Text(text = "Hide Recovery Password", style = MaterialTheme.typography.h8)
- Text(text = "Permanently hide your recovery password on this device.")
+ Text(text = stringResource(R.string.recoveryPasswordHideRecoveryPassword), style = MaterialTheme.typography.h8)
+ Text(text = stringResource(R.string.recoveryPasswordHideRecoveryPasswordDescription))
}
OutlineButton(
- "Hide",
+ stringResource(R.string.hide),
+ contentDescription = GetString(R.string.AccessibilityId_hide_recovery_password_button),
modifier = Modifier.align(Alignment.CenterVertically),
color = colorDestructive
) { onHide() }
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 1535770a1e..32b7e83cbb 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.isGone
@@ -215,6 +216,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..384f47d4d6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
@@ -1,13 +1,22 @@
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
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.MessageRequestResponse
@@ -22,7 +31,10 @@ 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
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
@@ -39,10 +51,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+
import javax.inject.Inject
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlin.coroutines.suspendCoroutine
interface ConversationRepository {
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
@@ -55,37 +65,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 +176,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 +197,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 +239,7 @@ class DefaultConversationRepository @Inject constructor(
.success {
continuation.resume(ResultOf.Success(Unit))
}.fail { error ->
+ Log.w("[onversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..")
continuation.resumeWithException(error)
}
}
@@ -225,7 +247,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 +301,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))
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..a5669aa351 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java
@@ -95,6 +95,16 @@ public class SearchRepository {
Stopwatch timer = new Stopwatch("FtsQuery");
String cleanQuery = sanitizeQuery(query);
+
+ // If the search is for a single character and it was stripped by `sanitizeQuery` then abort
+ // the search for an empty string to avoid SQLite error.
+ if (cleanQuery.length() == 0)
+ {
+ Log.d(TAG, "Aborting empty search query.");
+ timer.stop(TAG);
+ return;
+ }
+
timer.split("clean");
Pair, List> contacts = queryContacts(cleanQuery);
@@ -119,10 +129,11 @@ public class SearchRepository {
}
executor.execute(() -> {
- long startTime = System.currentTimeMillis();
- CursorList messages = queryMessages(sanitizeQuery(query), threadId);
- Log.d(TAG, "[ConversationQuery] " + (System.currentTimeMillis() - startTime) + " ms");
-
+ // If the sanitized search query is empty then abort the search to prevent SQLite errors.
+ String cleanQuery = sanitizeQuery(query).trim();
+ if (cleanQuery.isEmpty()) { return; }
+
+ CursorList messages = queryMessages(cleanQuery, threadId);
callback.onResult(messages);
});
}
@@ -215,7 +226,7 @@ public class SearchRepository {
out.append(' ');
}
}
-
+
return out.toString();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
index 618c874d5e..19f0b15b9e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
@@ -36,6 +36,12 @@ import androidx.compose.material.RadioButton
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -58,6 +64,9 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.runIf
@@ -65,17 +74,19 @@ import org.thoughtcrime.securesms.components.ProfilePictureView
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCard
import kotlin.math.min
import kotlin.math.roundToInt
+import kotlin.time.Duration.Companion.seconds
@Composable
fun OutlineButton(
text: String,
modifier: Modifier = Modifier,
+ contentDescription: GetString = GetString(text),
color: Color = LocalExtraColors.current.prominentButtonColor,
loading: Boolean = false,
onClick: () -> Unit
) {
OutlinedButton(
- modifier = modifier.contentDescription(GetString(text)),
+ modifier = modifier.contentDescription(contentDescription),
onClick = onClick,
border = BorderStroke(1.dp, color),
shape = RoundedCornerShape(50), // = 50% percent
@@ -96,7 +107,68 @@ fun OutlineButton(
}
@Composable
-fun FilledButton(text: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
+fun OutlineButton(
+ modifier: Modifier = Modifier,
+ color: Color = LocalExtraColors.current.prominentButtonColor,
+ onClick: () -> Unit = {},
+ content: @Composable () -> Unit = {}
+) {
+ OutlinedButton(
+ modifier = modifier,
+ onClick = onClick,
+ border = BorderStroke(1.dp, color),
+ shape = RoundedCornerShape(percent = 50),
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = color,
+ backgroundColor = Color.Unspecified
+ )
+ ) {
+ content()
+ }
+}
+
+@Composable
+fun OutlineButton(
+ temporaryContent: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ color: Color = LocalExtraColors.current.prominentButtonColor,
+ onClick: () -> Unit = {},
+ content: @Composable () -> Unit = {}
+) {
+ var clicked by remember { mutableStateOf(false) }
+ if (clicked) LaunchedEffectAsync {
+ delay(2.seconds)
+ clicked = false
+ }
+
+ OutlinedButton(
+ modifier = modifier,
+ onClick = {
+ onClick()
+ clicked = true
+ },
+ border = BorderStroke(1.dp, color),
+ shape = RoundedCornerShape(percent = 50),
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = color,
+ backgroundColor = Color.Unspecified
+ )
+ ) {
+ AnimatedVisibility(clicked) {
+ temporaryContent()
+ }
+ AnimatedVisibility(!clicked) {
+ content()
+ }
+ }
+}
+
+@Composable
+fun FilledButton(
+ text: String,
+ modifier: Modifier = Modifier,
+ contentDescription: GetString? = GetString(text),
+ onClick: () -> Unit) {
OutlinedButton(
modifier = modifier.size(108.dp, 34.dp),
onClick = onClick,
@@ -126,6 +198,7 @@ fun BorderlessButtonSecondary(
fun BorderlessButton(
text: String,
modifier: Modifier = Modifier,
+ contentDescription: GetString = GetString(text),
fontSize: TextUnit = TextUnit.Unspecified,
lineHeight: TextUnit = TextUnit.Unspecified,
contentColor: Color = MaterialTheme.colors.onBackground,
@@ -134,7 +207,7 @@ fun BorderlessButton(
) {
TextButton(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.contentDescription(contentDescription),
shape = RoundedCornerShape(percent = 50),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = contentColor,
@@ -284,8 +357,10 @@ fun TitledRadioButton(option: RadioOption, onClick: () -> Unit) {
@Composable
fun Modifier.contentDescription(text: GetString?): Modifier {
- val context = LocalContext.current
- return text?.let { semantics { contentDescription = it(context) } } ?: this
+ return text?.let {
+ val context = LocalContext.current
+ semantics { contentDescription = it(context) }
+ } ?: this
}
@Composable
@@ -435,3 +510,8 @@ fun RowScope.SessionShieldIcon() {
.wrapContentSize(unbounded = true)
)
}
+
+@Composable
+fun LaunchedEffectAsync(block: suspend CoroutineScope.() -> Unit) {
+ rememberCoroutineScope().apply { LaunchedEffect(Unit) { launch { block() } } }
+}
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/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/emoji_tada.xml b/app/src/main/res/drawable/emoji_tada.xml
deleted file mode 100644
index ce0f30067d..0000000000
--- a/app/src/main/res/drawable/emoji_tada.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/emoji_tada_large.xml b/app/src/main/res/drawable/emoji_tada_large.xml
new file mode 100644
index 0000000000..ed802646ff
--- /dev/null
+++ b/app/src/main/res/drawable/emoji_tada_large.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_logo_large.xml b/app/src/main/res/drawable/ic_logo_large.xml
new file mode 100644
index 0000000000..b494b17663
--- /dev/null
+++ b/app/src/main/res/drawable/ic_logo_large.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
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_settings.xml b/app/src/main/res/layout/activity_settings.xml
index fdd6d0f5a6..a5bac6b069 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" />
@@ -80,7 +86,7 @@
android:textColor="?android:textColorPrimary"
android:fontFamily="@font/space_mono_regular"
android:textAlignment="center"
- android:contentDescription="@string/AccessibilityId_session_id"
+ android:contentDescription="@string/AccessibilityId_account_id"
tools:text="05987d601943c267879be41830888066c6a024cbdc9a548d06813924bf3372ea78" />
+ android:contentDescription="@string/AccessibilityId_recovery_password_menu_item">
-
+
+ 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_conversation.xml b/app/src/main/res/layout/view_conversation.xml
index 12a7a8ac8c..ed3cc66969 100644
--- a/app/src/main/res/layout/view_conversation.xml
+++ b/app/src/main/res/layout/view_conversation.xml
@@ -165,7 +165,7 @@
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
- tools:text="Sorry, gotta go fight crime again" />
+ tools:text="Sorry, gotta go fight crime again - and more text to make it ellipsize" />
100
150
10
+
+ 35
+ 35
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f3be517011..a6333792c6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -20,6 +20,7 @@
Link Device
Session ID
+ Account ID
Continue
@@ -132,7 +133,7 @@
User settings
Username
Privacy
- Show recovery password
+ Recovery password menu item
Edit user nickname
Apply
Cancel
@@ -1059,6 +1060,7 @@
Privacy in your pocket.
Welcome to Session π
+ Welcome to Session
Session is engineered to protect your privacy.
"You donβt even need a phone number to sign up. "
Creating an account is \ninstant, free, and \nanonymous π
@@ -1081,6 +1083,38 @@
Load Account
Camera Permission permanently denied. Configure in settings.
Enter your recovery password to load your account. If you haven\'t saved it, you can find it in your app settings.
+ One moment please..
+ Loading your account
+ Message notifications
+ There are two ways Session can notify you of new messages.
+ Hide Recovery Password Permanently
+
We strongly recommend you save your recovery password in a safe and secure place before continuing.]]>
+ Are you sure you want to permanently hide your recovery password on this device? This cannot be undone.
+ Use your recovery password to load your account on new devices. Your account cannot be recovered without your recovery password. Make sure it\'s stored somewhere safe and secure β and don\'t share it with anyone.
+ Hide
+ Hide Recovery Password
+ View QR
+ View Password
+ Permanently hide your recovery password on this device.
+ You don\'t have any conversations yet
+ Hit the plus button to start a chat, create a group, or join an official communitiy!
+ Because you are the creator of this group it will be deleted for everyone. This cannot be undone.
+ Hide message requests?
+ Save your recovery password
+ Save your recovery password to make sure you don\'t lose access to your account.
+ Account Created
+ Fast mode notifications button
+ Slow mode notifications button
+ Reveal recovery phrase button
+ Create account button
+ Restore your session button
+ Privacy policy link
+ Terms of service link
+ Loading animation
+ Recovery phrase input
+ Error message
+ Hide recovery password button
+ Confirm button
This Account ID is invalid. Please check and try again.
Enter Account ID
diff --git a/app/src/main/res/xml/preferences_app_protection.xml b/app/src/main/res/xml/preferences_app_protection.xml
index 6e0cd98c2f..425edaf9ed 100644
--- a/app/src/main/res/xml/preferences_app_protection.xml
+++ b/app/src/main/res/xml/preferences_app_protection.xml
@@ -45,7 +45,7 @@
diff --git a/app/src/main/res/xml/preferences_help.xml b/app/src/main/res/xml/preferences_help.xml
index 0372619ea3..9bc9bd13e1 100644
--- a/app/src/main/res/xml/preferences_help.xml
+++ b/app/src/main/res/xml/preferences_help.xml
@@ -6,39 +6,38 @@
android:key="export_logs"
android:title="@string/activity_help_settings__report_bug_title"
android:summary="@string/activity_help_settings__report_bug_summary"
- android:widgetLayout="@layout/export_logs_widget"/>
+ android:widgetLayout="@layout/export_logs_widget" />
+
+
+
+ android:widgetLayout="@layout/preference_external_link" />
+ android:widgetLayout="@layout/preference_external_link" />
+ android:widgetLayout="@layout/preference_external_link" />
+ android:widgetLayout="@layout/preference_external_link" />
\ No newline at end of file
diff --git a/app/src/sharedTest/java/org/thoughtcrime/securesms/NoOpLogger.kt b/app/src/sharedTest/java/org/thoughtcrime/securesms/NoOpLogger.kt
new file mode 100644
index 0000000000..998c3b179d
--- /dev/null
+++ b/app/src/sharedTest/java/org/thoughtcrime/securesms/NoOpLogger.kt
@@ -0,0 +1,19 @@
+package org.thoughtcrime.securesms
+
+import org.session.libsignal.utilities.Log.Logger
+
+object NoOpLogger: Logger() {
+ override fun v(tag: String?, message: String?, t: Throwable?) {}
+
+ override fun d(tag: String?, message: String?, t: Throwable?) {}
+
+ override fun i(tag: String?, message: String?, t: Throwable?) {}
+
+ override fun w(tag: String?, message: String?, t: Throwable?) {}
+
+ override fun e(tag: String?, message: String?, t: Throwable?) {}
+
+ override fun wtf(tag: String?, message: String?, t: Throwable?) {}
+
+ override fun blockUntilAllWritesFinished() {}
+}
\ No newline at end of file
diff --git a/app/src/test/java/org/thoughtcrime/securesms/BaseViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/BaseViewModelTest.kt
index d73bc91a6d..a0f67fa699 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/BaseViewModelTest.kt
+++ b/app/src/test/java/org/thoughtcrime/securesms/BaseViewModelTest.kt
@@ -1,10 +1,20 @@
package org.thoughtcrime.securesms
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import org.junit.BeforeClass
import org.junit.Rule
+import org.session.libsignal.utilities.Log
open class BaseViewModelTest: BaseCoroutineTest() {
+ companion object {
+ @BeforeClass
+ @JvmStatic
+ fun setupLogger() {
+ Log.initialize(NoOpLogger)
+ }
+ }
+
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt
index 22679f311e..a9df5d946e 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt
+++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt
@@ -39,7 +39,7 @@ import kotlin.time.Duration.Companion.minutes
private const val THREAD_ID = 1L
private const val LOCAL_NUMBER = "05---local---address"
private val LOCAL_ADDRESS = Address.fromSerialized(LOCAL_NUMBER)
-private const val GROUP_NUMBER = "${GroupUtil.OPEN_GROUP_PREFIX}4133"
+private const val GROUP_NUMBER = "${GroupUtil.COMMUNITY_PREFIX}4133"
private val GROUP_ADDRESS = Address.fromSerialized(GROUP_NUMBER)
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt
index b89c62b6d5..37303e29d5 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt
+++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt
@@ -3,12 +3,14 @@ package org.thoughtcrime.securesms.conversation.v2
import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
+import org.junit.BeforeClass
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.anyLong
@@ -18,7 +20,9 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.session.libsession.utilities.recipients.Recipient
+import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.BaseViewModelTest
+import org.thoughtcrime.securesms.NoOpLogger
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
@@ -32,6 +36,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
private val threadId = 123L
private val edKeyPair = mock()
private lateinit var recipient: Recipient
+ private lateinit var messageRecord: MessageRecord
private val viewModel: ConversationViewModel by lazy {
ConversationViewModel(threadId, edKeyPair, repository, storage)
@@ -40,6 +45,9 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Before
fun setUp() {
recipient = mock()
+ messageRecord = mock { record ->
+ whenever(record.individualRecipient).thenReturn(recipient)
+ }
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow())
}
@@ -144,7 +152,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
val error = Throwable()
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Failure(error))
- viewModel.banAndDeleteAll(recipient)
+ viewModel.banAndDeleteAll(messageRecord)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
}
@@ -153,7 +161,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
fun `should emit a message on ban user and delete all success`() = runBlockingTest {
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Success(Unit))
- viewModel.banAndDeleteAll(recipient)
+ viewModel.banAndDeleteAll(messageRecord)
assertThat(
viewModel.uiState.first().uiMessages.first().message,
@@ -189,7 +197,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test
fun `open group recipient should have no blinded recipient`() {
- whenever(recipient.isOpenGroupRecipient).thenReturn(true)
+ whenever(recipient.isCommunityRecipient).thenReturn(true)
whenever(recipient.isOpenGroupOutboxRecipient).thenReturn(false)
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false)
assertThat(viewModel.blindedRecipient, nullValue())
diff --git a/gradle.properties b/gradle.properties
index 6f3a80a7fb..0948d30e31 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -16,7 +16,7 @@ android.enableJetifier=true
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
org.gradle.unsafe.configuration-cache=true
-gradlePluginVersion=7.3.1
+gradlePluginVersion=7.4.2
googleServicesVersion=4.3.12
kotlinVersion=1.8.21
android.useAndroidX=true
diff --git a/libsession/src/main/java/org/session/libsession/avatars/AvatarHelper.java b/libsession/src/main/java/org/session/libsession/avatars/AvatarHelper.java
index 1588289017..cc0909a592 100644
--- a/libsession/src/main/java/org/session/libsession/avatars/AvatarHelper.java
+++ b/libsession/src/main/java/org/session/libsession/avatars/AvatarHelper.java
@@ -8,9 +8,11 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.session.libsession.utilities.Address;
+import org.session.libsignal.utilities.Log;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -22,9 +24,9 @@ public class AvatarHelper {
private static final String AVATAR_DIRECTORY = "avatars";
public static InputStream getInputStreamFor(@NonNull Context context, @NonNull Address address)
- throws IOException
+ throws FileNotFoundException
{
- return new FileInputStream(getAvatarFile(context, address));
+ return new FileInputStream(getAvatarFile(context, address));
}
public static List getAvatarFiles(@NonNull Context context) {
diff --git a/libsession/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java b/libsession/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java
index f8675b031f..76a3449625 100644
--- a/libsession/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java
+++ b/libsession/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java
@@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
import org.session.libsession.utilities.Address;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
@@ -24,7 +25,7 @@ public class ProfileContactPhoto implements ContactPhoto {
}
@Override
- public InputStream openInputStream(Context context) throws IOException {
+ public InputStream openInputStream(Context context) throws FileNotFoundException {
return AvatarHelper.getInputStreamFor(context, address);
}
diff --git a/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java b/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java
index f78089e25e..2920b4b1ce 100644
--- a/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java
+++ b/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java
@@ -8,6 +8,7 @@ import android.graphics.drawable.LayerDrawable;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
+import androidx.appcompat.content.res.AppCompatResources;
import com.amulyakhare.textdrawable.TextDrawable;
import com.makeramen.roundedimageview.RoundedDrawable;
@@ -31,7 +32,7 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
@Override
public Drawable asDrawable(Context context, int color, boolean inverted) {
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
- RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId));
+ RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
foreground.setScaleType(ImageView.ScaleType.CENTER_CROP);
@@ -39,8 +40,10 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
}
- Drawable gradient = context.getResources().getDrawable(ThemeUtil.isDarkTheme(context) ? R.drawable.avatar_gradient_dark
- : R.drawable.avatar_gradient_light);
+ Drawable gradient = AppCompatResources.getDrawable(
+ context,
+ ThemeUtil.isDarkTheme(context) ? R.drawable.avatar_gradient_dark : R.drawable.avatar_gradient_light
+ );
return new ExpandingLayerDrawable(new Drawable[] {background, foreground, gradient});
}
diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
index 9d6ae18d0c..260e254fe7 100644
--- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
+++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
@@ -120,7 +120,9 @@ interface StorageProtocol {
fun markAsSyncing(timestamp: Long, author: String)
fun markAsSending(timestamp: Long, author: String)
fun markAsSent(timestamp: Long, author: String)
+ fun markAsSentToCommunity(threadID: Long, messageID: Long)
fun markUnidentified(timestamp: Long, author: String)
+ fun markUnidentifiedInCommunity(threadID: Long, messageID: Long)
fun markAsSyncFailed(timestamp: Long, author: String, error: Exception)
fun markAsSentFailed(timestamp: Long, author: String, error: Exception)
fun clearErrorMessage(messageID: Long)
diff --git a/libsession/src/main/java/org/session/libsession/messaging/contacts/Contact.kt b/libsession/src/main/java/org/session/libsession/messaging/contacts/Contact.kt
index 92ff9190c5..7161d070aa 100644
--- a/libsession/src/main/java/org/session/libsession/messaging/contacts/Contact.kt
+++ b/libsession/src/main/java/org/session/libsession/messaging/contacts/Contact.kt
@@ -77,7 +77,7 @@ class Contact(val sessionID: String) {
companion object {
fun contextForRecipient(recipient: Recipient): ContactContext {
- return if (recipient.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
+ return if (recipient.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
}
}
}
\ No newline at end of file
diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt
index 333c87ba78..271549c410 100644
--- a/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt
+++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt
@@ -22,7 +22,7 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
override suspend fun execute(dispatcherName: String) {
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val numberToDelete = messageServerIds.size
- Log.d(TAG, "Deleting $numberToDelete messages")
+ Log.d(TAG, "About to attempt to delete $numberToDelete messages")
// FIXME: This entire process should probably run in a transaction (with the attachment deletion happening only if it succeeded)
try {
@@ -42,6 +42,7 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
delegate?.handleJobSucceeded(this, dispatcherName)
}
catch (e: Exception) {
+ Log.w(TAG, "OpenGroupDeleteJob failed: $e")
delegate?.handleJobFailed(this, dispatcherName, e)
}
}
diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt
index f30c1b9168..faad7aeebf 100644
--- a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt
+++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt
@@ -43,14 +43,14 @@ sealed class Destination {
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
ClosedGroup(groupPublicKey)
}
- address.isOpenGroup -> {
+ address.isCommunity -> {
val storage = MessagingModuleConfiguration.shared.storage
val threadID = storage.getThreadId(address)!!
storage.getOpenGroup(threadID)?.let {
OpenGroup(roomToken = it.room, server = it.server, fileIds = fileIds)
} ?: throw Exception("Missing open group for thread with ID: $threadID.")
}
- address.isOpenGroupInbox -> {
+ address.isCommunityInbox -> {
val groupInboxId = GroupUtil.getDecodedGroupID(address.serialize()).split("!")
OpenGroupInbox(
groupInboxId.dropLast(2).joinToString("!"),
diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt
index a7b5051f51..1f23a1cc87 100644
--- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt
+++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt
@@ -602,8 +602,7 @@ object OpenGroupApi {
// region Message Deletion
@JvmStatic
fun deleteMessage(serverID: Long, room: String, server: String): Promise {
- val request =
- Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID))
+ val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID))
return send(request).map {
Log.d("Loki", "Message deletion successful.")
}
@@ -659,7 +658,9 @@ object OpenGroupApi {
}
fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise {
+
val requests = mutableListOf>(
+ // Ban request
BatchRequestInfo(
request = BatchRequest(
method = POST,
@@ -669,6 +670,7 @@ object OpenGroupApi {
endpoint = Endpoint.UserBan(publicKey),
responseType = object: TypeReference(){}
),
+ // Delete request
BatchRequestInfo(
request = BatchRequest(DELETE, "/room/$room/all/$publicKey"),
endpoint = Endpoint.RoomDeleteMessages(room, publicKey),
diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
index 21df7fcb5d..b0459de1d6 100644
--- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
+++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
@@ -39,6 +39,7 @@ import org.session.libsignal.crypto.PushTransportDetails
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.IdPrefix
+import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.defaultRequiresAuth
import org.session.libsignal.utilities.hasNamespaces
@@ -370,7 +371,7 @@ object MessageSender {
}
// Result Handling
- fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
+ private fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
val timestamp = message.sentTimestamp!!
@@ -392,8 +393,10 @@ object MessageSender {
// in case any errors from previous sends
storage.clearErrorMessage(messageID)
+
// Track the open group server message ID
- if (message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)) {
+ val messageIsAddressedToCommunity = message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)
+ if (messageIsAddressedToCommunity) {
val server: String
val room: String
when (destination) {
@@ -415,9 +418,26 @@ object MessageSender {
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
}
}
- // Mark the message as sent
- storage.markAsSent(timestamp, userPublicKey)
- storage.markUnidentified(timestamp, userPublicKey)
+
+ // Mark the message as sent.
+ // Note: When sending a message to a community the server modifies the message timestamp
+ // so when we go to look up the message in the local database by timestamp it fails and
+ // we're left with the message delivery status as "Sending" forever! As such, we use a
+ // pair of modified "markAsSentToCommunity" and "markUnidentifiedInCommunity" methods
+ // to retrieve the local message by thread & message ID rather than timestamp when
+ // handling community messages only so we can tick the delivery status over to 'Sent'.
+ // Fixed in: https://optf.atlassian.net/browse/SES-1567
+ if (messageIsAddressedToCommunity)
+ {
+ storage.markAsSentToCommunity(message.threadID!!, message.id!!)
+ storage.markUnidentifiedInCommunity(message.threadID!!, message.id!!)
+ }
+ else
+ {
+ storage.markAsSent(timestamp, userPublicKey)
+ storage.markUnidentified(timestamp, userPublicKey)
+ }
+
// Start the disappearing messages timer if needed
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(message, startDisappearAfterRead = true)
} ?: run {
diff --git a/libsession/src/main/java/org/session/libsession/utilities/Address.kt b/libsession/src/main/java/org/session/libsession/utilities/Address.kt
index c8cd11d4b6..920b466a8b 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/Address.kt
+++ b/libsession/src/main/java/org/session/libsession/utilities/Address.kt
@@ -8,7 +8,6 @@ import androidx.annotation.VisibleForTesting
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Util
import org.session.libsignal.utilities.guava.Optional
-import java.util.Collections
import java.util.LinkedList
import java.util.concurrent.atomic.AtomicReference
import java.util.regex.Matcher
@@ -23,17 +22,17 @@ class Address private constructor(address: String) : Parcelable, Comparable {
val escapedAddresses = DelimiterUtil.split(serialized, delimiter)
+ val set = escapedAddresses.toSet().sorted()
val addresses: MutableList = LinkedList()
- for (escapedAddress in escapedAddresses) {
+ for (escapedAddress in set) {
addresses.add(fromSerialized(DelimiterUtil.unescape(escapedAddress, delimiter)))
}
return addresses
@@ -177,9 +177,9 @@ class Address private constructor(address: String) : Parcelable, Comparable, delimiter: Char): String {
- Collections.sort(addresses)
+ val set = addresses.toSet().sorted()
val escapedAddresses: MutableList = LinkedList()
- for (address in addresses) {
+ for (address in set) {
escapedAddresses.add(DelimiterUtil.escape(address.serialize(), delimiter))
}
return Util.join(escapedAddresses, delimiter.toString() + "")
diff --git a/libsession/src/main/java/org/session/libsession/utilities/GroupRecord.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupRecord.kt
index a8cf6fd796..34b17f16b9 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/GroupRecord.kt
+++ b/libsession/src/main/java/org/session/libsession/utilities/GroupRecord.kt
@@ -22,7 +22,7 @@ class GroupRecord(
}
val isOpenGroup: Boolean
- get() = Address.fromSerialized(encodedId).isOpenGroup
+ get() = Address.fromSerialized(encodedId).isCommunity
val isClosedGroup: Boolean
get() = Address.fromSerialized(encodedId).isClosedGroup
diff --git a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt
index 630db9e991..9c30aeb249 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt
+++ b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt
@@ -8,12 +8,12 @@ import java.io.IOException
object GroupUtil {
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
- const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!"
- const val OPEN_GROUP_INBOX_PREFIX = "__open_group_inbox__!"
+ const val COMMUNITY_PREFIX = "__loki_public_chat_group__!"
+ const val COMMUNITY_INBOX_PREFIX = "__open_group_inbox__!"
@JvmStatic
fun getEncodedOpenGroupID(groupID: ByteArray): String {
- return OPEN_GROUP_PREFIX + Hex.toStringCondensed(groupID)
+ return COMMUNITY_PREFIX + Hex.toStringCondensed(groupID)
}
@JvmStatic
@@ -25,7 +25,7 @@ object GroupUtil {
@JvmStatic
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): Address {
- return Address.fromSerialized(OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID))
+ return Address.fromSerialized(COMMUNITY_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID))
}
@JvmStatic
@@ -69,17 +69,17 @@ object GroupUtil {
}
fun isEncodedGroup(groupId: String): Boolean {
- return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(OPEN_GROUP_PREFIX)
+ return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(COMMUNITY_PREFIX)
}
@JvmStatic
- fun isOpenGroup(groupId: String): Boolean {
- return groupId.startsWith(OPEN_GROUP_PREFIX)
+ fun isCommunity(groupId: String): Boolean {
+ return groupId.startsWith(COMMUNITY_PREFIX)
}
@JvmStatic
- fun isOpenGroupInbox(groupId: String): Boolean {
- return groupId.startsWith(OPEN_GROUP_INBOX_PREFIX)
+ fun isCommunityInbox(groupId: String): Boolean {
+ return groupId.startsWith(COMMUNITY_INBOX_PREFIX)
}
@JvmStatic
diff --git a/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java b/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java
index 720806ef89..094a9fc349 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java
+++ b/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java
@@ -459,16 +459,16 @@ public class Recipient implements RecipientModifiedListener {
}
public boolean is1on1() { return address.isContact() && !isLocalNumber; }
- public boolean isOpenGroupRecipient() {
- return address.isOpenGroup();
+ public boolean isCommunityRecipient() {
+ return address.isCommunity();
}
public boolean isOpenGroupOutboxRecipient() {
- return address.isOpenGroupOutbox();
+ return address.isCommunityOutbox();
}
public boolean isOpenGroupInboxRecipient() {
- return address.isOpenGroupInbox();
+ return address.isCommunityInbox();
}
public boolean isClosedGroupRecipient() {