mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
Merge branch 'dev' into on
This commit is contained in:
commit
3ae2dc5bc5
24
.run/Run Tests.run.xml
Normal file
24
.run/Run Tests.run.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Tests" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="testPlayDebugUnitTestCoverageReport" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
@ -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" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
|
||||
android:screenOrientation="portrait"
|
||||
|
@ -478,9 +478,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
Log.d("Loki-Avatar", "Uploading Avatar Finished");
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
} catch (Exception exception) {
|
||||
// Do nothing
|
||||
Log.e("Loki-Avatar", "Uploading avatar failed", exception);
|
||||
} catch (Exception e) {
|
||||
Log.e("Loki-Avatar", "Uploading avatar failed.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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<Long>, 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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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<String, String?>()
|
||||
private val profilePicturesCache = mutableMapOf<View, Recipient>()
|
||||
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()
|
||||
|
@ -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() {
|
||||
|
@ -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<String> 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<String> recentlyUsed;
|
||||
public static final LinkedList<String> 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<String> recentlyUsed;
|
||||
|
||||
public RecentEmojiPageModel(Context context) {
|
||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.recentlyUsed = getPersistedCache();
|
||||
}
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
private LinkedHashSet<String> 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<String> getEmoji() {
|
||||
List<String> recent = new ArrayList<>(recentlyUsed);
|
||||
List<String> 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<Emoji> 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<String> 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<String> latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed);
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
// 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<String> 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."); }
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
|
||||
|
||||
private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
||||
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) {
|
||||
it.address.isOpenGroup
|
||||
it.address.isCommunity
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -106,6 +106,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,6 +176,7 @@ 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
|
||||
@ -281,6 +283,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 +341,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 +362,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 +390,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 +406,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 +436,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 +582,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 +807,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 +977,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 +1086,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
|
||||
@ -1148,7 +1198,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 +1336,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
sendEmojiRemoval(emoji, messageRecord)
|
||||
} else {
|
||||
sendEmojiReaction(emoji, messageRecord)
|
||||
RecentEmojiPageModel.onCodePointSelected(emoji) // Save to recently used reaction emojis
|
||||
}
|
||||
}
|
||||
|
||||
@ -1312,7 +1363,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 +1387,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 +1782,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 +1848,62 @@ 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<MessageRecord>) {
|
||||
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<MessageRecord>) {
|
||||
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<MessageRecord>) {
|
||||
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 then we delete the message for everyone
|
||||
if (recipient.isCommunityRecipient) {
|
||||
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 +1922,17 @@ 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 +1951,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)
|
||||
}
|
||||
}
|
||||
@ -2105,4 +2203,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<Contact>(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<MessageRecord>()
|
||||
val toDeselect = mutableSetOf<Pair<Int, MessageRecord>>()
|
||||
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? {
|
||||
|
@ -541,7 +541,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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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<FrameLayout.LayoutParams> {
|
||||
gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
||||
@ -250,7 +257,7 @@ 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)
|
||||
@ -258,13 +265,14 @@ class VisibleMessageView : LinearLayout {
|
||||
?.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()
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
@ -859,8 +859,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
*/
|
||||
private fun deleteMessages(messageIds: Array<String?>) {
|
||||
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 +885,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 +903,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)
|
||||
|
@ -183,7 +183,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 +209,62 @@ 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<MessageRecord> getAllMessageRecordsFromSenderInThread(long threadId, String serializedAuthor) {
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS + " = \"" + serializedAuthor + "\"";
|
||||
Set<MessageRecord> identifiedMessages = new HashSet<MessageRecord>();
|
||||
|
||||
// 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<Long> getAllMessageIdsFromSenderInThread(long threadId, String serializedAuthor) {
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS + " = \"" + serializedAuthor + "\"";
|
||||
|
||||
Set<Long> identifiedMessages = new HashSet<Long>();
|
||||
|
||||
// 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) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
|
||||
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
|
||||
|
||||
// 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 (isOwnNumber && messageRecord.isOutgoing()) { return messageRecord.id; }
|
||||
}
|
||||
}
|
||||
}
|
||||
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";
|
||||
|
@ -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+")))";
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -621,10 +621,12 @@ 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);
|
||||
@ -645,9 +647,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,
|
||||
|
@ -121,7 +121,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 +152,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 +257,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 +327,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)
|
||||
}
|
||||
@ -1289,7 +1289,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 (
|
||||
|
@ -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);
|
||||
}
|
||||
@ -516,7 +516,7 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
|
||||
public Cursor getArchivedConversationList() {
|
||||
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 + " = 1 ";
|
||||
return getConversationList(where);
|
||||
}
|
||||
@ -526,7 +526,7 @@ public class ThreadDatabase extends Database {
|
||||
String query = createQuery(where, 0);
|
||||
Cursor cursor = db.rawQuery(query, null);
|
||||
|
||||
setNotifyConverationListListeners(cursor);
|
||||
setNotifyConversationListListeners(cursor);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
@ -547,7 +547,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();
|
||||
|
||||
@ -822,7 +822,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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -317,7 +317,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
|
||||
val newData = contactResults + messageResults
|
||||
|
||||
globalSearchAdapter.setNewData(result.query, newData)
|
||||
}
|
||||
}
|
||||
@ -585,7 +584,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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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 + ": "));
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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<Recipient>)
|
||||
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<Unit>
|
||||
|
||||
suspend fun deleteForEveryone(threadId: Long, recipient: Recipient, message: MessageRecord): ResultOf<Unit>
|
||||
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
|
||||
|
||||
suspend fun deleteMessageWithoutUnsendRequest(
|
||||
threadId: Long,
|
||||
messages: Set<MessageRecord>
|
||||
): ResultOf<Unit>
|
||||
|
||||
suspend fun deleteMessageWithoutUnsendRequest(threadId: Long, messages: Set<MessageRecord>): ResultOf<Unit>
|
||||
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||
|
||||
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||
|
||||
suspend fun deleteThread(threadId: Long): ResultOf<Unit>
|
||||
|
||||
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit>
|
||||
|
||||
suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit>
|
||||
|
||||
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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<Unit> =
|
||||
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))
|
||||
|
@ -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<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
|
||||
@ -119,10 +129,11 @@ public class SearchRepository {
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
CursorList<MessageResult> 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<MessageResult> messages = queryMessages(cleanQuery, threadId);
|
||||
callback.onResult(messages);
|
||||
});
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
@ -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
|
||||
}
|
||||
|
@ -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).
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
12
app/src/main/res/drawable/cross.xml
Normal file
12
app/src/main/res/drawable/cross.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="100dp"
|
||||
android:height="100dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
|
||||
<path
|
||||
android:pathData="M0,0 L100,100 M0,100 L100,0"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="@android:color/white" />
|
||||
</vector>
|
@ -23,6 +23,10 @@
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<!--
|
||||
Add this to the below recycler view if you need to debug activity `adjustResize` issues:
|
||||
android:background="@drawable/cross"
|
||||
-->
|
||||
<org.thoughtcrime.securesms.conversation.v2.ConversationRecyclerView
|
||||
android:focusable="false"
|
||||
android:id="@+id/conversationRecyclerView"
|
||||
@ -31,6 +35,7 @@
|
||||
android:layout_above="@+id/typingIndicatorViewContainer"
|
||||
android:layout_below="@id/toolbar" />
|
||||
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer
|
||||
android:focusable="false"
|
||||
android:id="@+id/typingIndicatorViewContainer"
|
||||
|
@ -32,6 +32,7 @@
|
||||
android:id="@+id/btnCancelGroupNameEdit"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginLeft="@dimen/medium_spacing"
|
||||
android:contentDescription="@string/AccessibilityId_cancel_name_change"
|
||||
android:src="@drawable/ic_baseline_clear_24"/>
|
||||
|
||||
@ -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"/>
|
||||
|
||||
|
@ -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" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnGroupNameDisplay"
|
||||
@ -57,6 +61,7 @@
|
||||
android:contentDescription="@string/AccessibilityId_username"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="@dimen/very_large_font_size"
|
||||
android:maxLength="@integer/max_user_nickname_length_chars"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/export_logs_button"
|
||||
android:layout_gravity="center"
|
||||
style="@style/Widget.Session.Button.Common.Filled"
|
||||
android:textStyle="bold"
|
||||
@ -11,5 +12,6 @@
|
||||
android:paddingHorizontal="@dimen/medium_spacing"
|
||||
android:paddingVertical="12dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</FrameLayout>
|
@ -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" />
|
||||
|
||||
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||
android:id="@+id/contactSearch"
|
||||
|
@ -29,6 +29,8 @@
|
||||
android:id="@+id/nameTextViewContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/medium_spacing"
|
||||
android:paddingEnd="@dimen/medium_spacing"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:layout_centerInParent="true"
|
||||
@ -42,6 +44,7 @@
|
||||
android:id="@+id/nameTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="@dimen/small_spacing"
|
||||
android:layout_marginEnd="@dimen/small_spacing"
|
||||
@ -57,6 +60,7 @@
|
||||
android:layout_height="22dp"
|
||||
android:contentDescription="@string/AccessibilityId_edit_user_nickname"
|
||||
android:paddingTop="2dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:src="@drawable/ic_baseline_edit_24" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -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" />
|
||||
|
||||
|
@ -1,23 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/container"
|
||||
android:id="@+id/export_progress_container"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="16dp"
|
||||
android:gravity="bottom">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom" >
|
||||
|
||||
<ProgressBar android:id="@+id/progress_bar"
|
||||
<ProgressBar
|
||||
android:id="@+id/export_progress_bar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"/>
|
||||
|
||||
<TextView android:id="@+id/progress_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
tools:text="1345 messages so far"/>
|
||||
android:indeterminate="true"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</LinearLayout>
|
@ -15,7 +15,7 @@
|
||||
android:layout_width="17dp"
|
||||
android:layout_height="17dp" />
|
||||
|
||||
<Space
|
||||
<View
|
||||
android:id="@+id/reactions_pill_spacer"
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="wrap_content" />
|
||||
|
@ -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"
|
||||
|
@ -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" />
|
||||
|
||||
<include layout="@layout/view_typing_indicator"
|
||||
android:id="@+id/typingIndicatorView"
|
||||
|
@ -6,4 +6,7 @@
|
||||
<integer name="reaction_scrubber_reveal_offset">100</integer>
|
||||
<integer name="reaction_scrubber_hide_duration">150</integer>
|
||||
<integer name="reaction_scrubber_emoji_reveal_duration_start_delay_factor">10</integer>
|
||||
|
||||
<integer name="max_user_nickname_length_chars">35</integer>
|
||||
<integer name="max_group_and_community_name_length_chars">35</integer>
|
||||
</resources>
|
@ -45,7 +45,7 @@
|
||||
<PreferenceCategory android:title="@string/preferences__link_previews">
|
||||
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:defaultValue="false"
|
||||
android:key="pref_link_previews"
|
||||
android:summary="@string/preferences__link_previews_summary"
|
||||
android:title="@string/preferences__send_link_previews"/>
|
||||
|
@ -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" />
|
||||
|
||||
<!-- Note: Having this as `android:layout` rather than `android:layoutWidget` allows it to fit the screen width -->
|
||||
<Preference android:layout="@layout/preference_widget_progress" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory>
|
||||
<Preference
|
||||
android:key="translate_session"
|
||||
android:title="@string/activity_help_settings__translate_session"
|
||||
android:widgetLayout="@layout/preference_external_link"
|
||||
/>
|
||||
android:widgetLayout="@layout/preference_external_link" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory>
|
||||
<Preference
|
||||
android:key="feedback"
|
||||
android:title="@string/activity_help_settings__feedback"
|
||||
android:widgetLayout="@layout/preference_external_link"
|
||||
/>
|
||||
android:widgetLayout="@layout/preference_external_link" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory>
|
||||
<Preference
|
||||
android:key="faq"
|
||||
android:title="@string/activity_help_settings__faq"
|
||||
android:widgetLayout="@layout/preference_external_link"
|
||||
/>
|
||||
android:widgetLayout="@layout/preference_external_link" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory>
|
||||
<Preference
|
||||
android:key="support"
|
||||
android:title="@string/activity_help_settings__support"
|
||||
android:widgetLayout="@layout/preference_external_link"
|
||||
/>
|
||||
android:widgetLayout="@layout/preference_external_link" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
@ -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() {}
|
||||
}
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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<KeyPair>()
|
||||
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())
|
||||
|
@ -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,7 +24,7 @@ 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));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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});
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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("!"),
|
||||
|
@ -602,8 +602,7 @@ object OpenGroupApi {
|
||||
// region Message Deletion
|
||||
@JvmStatic
|
||||
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
||||
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<Unit, Exception> {
|
||||
|
||||
val requests = mutableListOf<BatchRequestInfo<*>>(
|
||||
// Ban request
|
||||
BatchRequestInfo(
|
||||
request = BatchRequest(
|
||||
method = POST,
|
||||
@ -669,6 +670,7 @@ object OpenGroupApi {
|
||||
endpoint = Endpoint.UserBan(publicKey),
|
||||
responseType = object: TypeReference<Any>(){}
|
||||
),
|
||||
// Delete request
|
||||
BatchRequestInfo(
|
||||
request = BatchRequest(DELETE, "/room/$room/all/$publicKey"),
|
||||
endpoint = Endpoint.RoomDeleteMessages(room, publicKey),
|
||||
|
@ -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<Addr
|
||||
get() = GroupUtil.isEncodedGroup(address)
|
||||
val isClosedGroup: Boolean
|
||||
get() = GroupUtil.isClosedGroup(address)
|
||||
val isOpenGroup: Boolean
|
||||
get() = GroupUtil.isOpenGroup(address)
|
||||
val isOpenGroupInbox: Boolean
|
||||
get() = GroupUtil.isOpenGroupInbox(address)
|
||||
val isOpenGroupOutbox: Boolean
|
||||
val isCommunity: Boolean
|
||||
get() = GroupUtil.isCommunity(address)
|
||||
val isCommunityInbox: Boolean
|
||||
get() = GroupUtil.isCommunityInbox(address)
|
||||
val isCommunityOutbox: Boolean
|
||||
get() = address.startsWith(IdPrefix.BLINDED.value) || address.startsWith(IdPrefix.BLINDEDV2.value)
|
||||
val isContact: Boolean
|
||||
get() = !(isGroup || isOpenGroupInbox)
|
||||
get() = !(isGroup || isCommunityInbox)
|
||||
|
||||
fun contactIdentifier(): String {
|
||||
if (!isContact && !isOpenGroup) {
|
||||
if (!isContact && !isCommunity) {
|
||||
if (isGroup) throw AssertionError("Not e164, is group")
|
||||
throw AssertionError("Not e164, unknown")
|
||||
}
|
||||
@ -168,8 +167,9 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
|
||||
@JvmStatic
|
||||
fun fromSerializedList(serialized: String, delimiter: Char): List<Address> {
|
||||
val escapedAddresses = DelimiterUtil.split(serialized, delimiter)
|
||||
val set = escapedAddresses.toSet().sorted()
|
||||
val addresses: MutableList<Address> = 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<Addr
|
||||
|
||||
@JvmStatic
|
||||
fun toSerializedList(addresses: List<Address>, delimiter: Char): String {
|
||||
Collections.sort(addresses)
|
||||
val set = addresses.toSet().sorted()
|
||||
val escapedAddresses: MutableList<String> = LinkedList()
|
||||
for (address in addresses) {
|
||||
for (address in set) {
|
||||
escapedAddresses.add(DelimiterUtil.escape(address.serialize(), delimiter))
|
||||
}
|
||||
return Util.join(escapedAddresses, delimiter.toString() + "")
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user