mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Merge branch 'on' into on-2
This commit is contained in:
commit
eb958bac4e
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>
|
@ -5,7 +5,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
|
||||||
classpath files('libs/gradle-witness.jar')
|
classpath files('libs/gradle-witness.jar')
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||||
|
@ -230,11 +230,13 @@
|
|||||||
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
|
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
|
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
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
|
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
|
@ -154,6 +154,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
|
|
||||||
private volatile boolean isAppVisible;
|
private volatile boolean isAppVisible;
|
||||||
|
|
||||||
|
public boolean newAccount = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getSystemService(String name) {
|
public Object getSystemService(String name) {
|
||||||
if (MessagingModuleConfiguration.MESSAGING_MODULE_SERVICE.equals(name)) {
|
if (MessagingModuleConfiguration.MESSAGING_MODULE_SERVICE.equals(name)) {
|
||||||
@ -212,6 +214,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
DatabaseModule.init(this);
|
DatabaseModule.init(this);
|
||||||
MessagingModuleConfiguration.configure(this);
|
MessagingModuleConfiguration.configure(this);
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
messagingModuleConfiguration = new MessagingModuleConfiguration(
|
messagingModuleConfiguration = new MessagingModuleConfiguration(
|
||||||
this,
|
this,
|
||||||
storage,
|
storage,
|
||||||
@ -478,9 +481,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
Log.d("Loki-Avatar", "Uploading Avatar Finished");
|
Log.d("Loki-Avatar", "Uploading Avatar Finished");
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
} catch (Exception exception) {
|
} catch (Exception e) {
|
||||||
// Do nothing
|
Log.e("Loki-Avatar", "Uploading avatar failed.");
|
||||||
Log.e("Loki-Avatar", "Uploading avatar failed", exception);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import androidx.annotation.LayoutRes
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.core.view.setMargins
|
import androidx.core.view.setMargins
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import androidx.core.view.updateMargins
|
import androidx.core.view.updateMargins
|
||||||
@ -21,7 +22,6 @@ import androidx.fragment.app.Fragment
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
|
|
||||||
|
|
||||||
@DslMarker
|
@DslMarker
|
||||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
|
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
|
||||||
annotation class DialogDsl
|
annotation class DialogDsl
|
||||||
@ -64,7 +64,6 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
|
private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
|
||||||
text ?: return
|
text ?: return
|
||||||
TextView(context, null, 0, style)
|
TextView(context, null, 0, style)
|
||||||
@ -75,6 +74,8 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
}.let(topView::addView)
|
}.let(topView::addView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun htmlText(@StringRes id: Int, @StyleRes style: Int = 0, modify: TextView.() -> Unit = {}) { text(context.resources.getText(id)) }
|
||||||
|
|
||||||
fun view(view: View) = contentView.addView(view)
|
fun view(view: View) = contentView.addView(view)
|
||||||
|
|
||||||
fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView)
|
fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView)
|
||||||
|
@ -184,16 +184,21 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||||
else DatabaseComponent.get(context).mmsDatabase()
|
else DatabaseComponent.get(context).mmsDatabase()
|
||||||
|
|
||||||
messagingDatabase.deleteMessage(messageID)
|
messagingDatabase.deleteMessage(messageID)
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms)
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
|
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
|
||||||
|
|
||||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||||
else DatabaseComponent.get(context).mmsDatabase()
|
else DatabaseComponent.get(context).mmsDatabase()
|
||||||
|
|
||||||
|
// Perform local delete
|
||||||
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
|
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
|
||||||
|
|
||||||
|
// Perform online delete
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms)
|
||||||
}
|
}
|
||||||
|
@ -334,6 +334,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
viewModel.localRenderer?.let { surfaceView ->
|
viewModel.localRenderer?.let { surfaceView ->
|
||||||
surfaceView.setZOrderOnTop(true)
|
surfaceView.setZOrderOnTop(true)
|
||||||
|
|
||||||
|
// Mirror the video preview of the person making the call to prevent disorienting them
|
||||||
|
surfaceView.setMirror(true)
|
||||||
|
|
||||||
binding.localRenderer.addView(surfaceView)
|
binding.localRenderer.addView(surfaceView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.DimenRes
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
||||||
@ -33,13 +32,12 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
var additionalDisplayName: String? = null
|
var additionalDisplayName: String? = null
|
||||||
var isLarge = false
|
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)
|
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
||||||
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
|
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
|
||||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
||||||
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
constructor(context: Context, sender: Recipient): this(context) {
|
constructor(context: Context, sender: Recipient): this(context) {
|
||||||
@ -90,8 +88,8 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
val publicKey = publicKey ?: return
|
val publicKey = publicKey ?: return
|
||||||
val additionalPublicKey = additionalPublicKey
|
val additionalPublicKey = additionalPublicKey
|
||||||
if (additionalPublicKey != null) {
|
if (additionalPublicKey != null) {
|
||||||
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
|
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName)
|
||||||
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
|
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName)
|
||||||
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
|
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
glide.clear(binding.doubleModeImageView1)
|
glide.clear(binding.doubleModeImageView1)
|
||||||
@ -99,14 +97,14 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
|
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (additionalPublicKey == null && !isLarge) {
|
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
|
binding.singleModeImageView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
glide.clear(binding.singleModeImageView)
|
glide.clear(binding.singleModeImageView)
|
||||||
binding.singleModeImageView.visibility = View.INVISIBLE
|
binding.singleModeImageView.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (additionalPublicKey == null && isLarge) {
|
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
|
binding.largeSingleModeImageView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
glide.clear(binding.largeSingleModeImageView)
|
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()) {
|
if (publicKey.isNotEmpty()) {
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
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 signalProfilePicture = recipient.contactPhoto
|
||||||
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
||||||
|
|
||||||
|
glide.clear(imageView)
|
||||||
|
|
||||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||||
|
|
||||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||||
glide.clear(imageView)
|
|
||||||
glide.load(signalProfilePicture)
|
glide.load(signalProfilePicture)
|
||||||
.placeholder(unknownRecipientDrawable)
|
.placeholder(unknownRecipientDrawable)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
@ -132,21 +132,19 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
} else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) {
|
} else if (recipient.isCommunityRecipient && recipient.groupAvatarId == null) {
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
glide.load(unknownOpenGroupDrawable)
|
glide.load(unknownOpenGroupDrawable)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
} else {
|
} else {
|
||||||
glide.clear(imageView)
|
|
||||||
glide.load(placeholder)
|
glide.load(placeholder)
|
||||||
.placeholder(unknownRecipientDrawable)
|
.placeholder(unknownRecipientDrawable)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
|
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
|
||||||
}
|
}
|
||||||
profilePicturesCache[publicKey] = recipient.profileAvatar
|
|
||||||
} else {
|
} else {
|
||||||
glide.load(unknownRecipientDrawable)
|
glide.load(unknownRecipientDrawable)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
|
|
||||||
import android.animation.Animator;
|
import android.animation.Animator;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@ -68,9 +67,7 @@ public class SearchToolbar extends LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onQueryTextChange(String newText) {
|
public boolean onQueryTextChange(String newText) { return onQueryTextSubmit(newText); }
|
||||||
return onQueryTextSubmit(newText);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
||||||
|
@ -3,133 +3,95 @@ package org.thoughtcrime.securesms.components.emoji;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
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.JsonUtil;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Iterator;
|
import java.util.LinkedList;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class RecentEmojiPageModel implements EmojiPageModel {
|
public class RecentEmojiPageModel implements EmojiPageModel {
|
||||||
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
|
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
|
||||||
private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji2";
|
public static final String RECENT_EMOJIS_KEY = "Recents";
|
||||||
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");
|
|
||||||
|
|
||||||
private final SharedPreferences prefs;
|
public static final LinkedList<String> DEFAULT_REACTION_EMOJIS_LIST = new LinkedList<>(Arrays.asList(
|
||||||
private final LinkedHashSet<String> recentlyUsed;
|
"\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) {
|
public RecentEmojiPageModel(Context context) {
|
||||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
this.recentlyUsed = getPersistedCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
private LinkedHashSet<String> getPersistedCache() {
|
// Note: Do NOT try to populate or update the persisted recent emojis in the constructor - the
|
||||||
String serialized = prefs.getString(EMOJI_LRU_PREFERENCE, "[]");
|
// `getEmoji` method ends up getting called half-way through in a race-condition manner.
|
||||||
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<>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getKey() {
|
public String getKey() { return RECENT_EMOJIS_KEY; }
|
||||||
return KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public int getIconAttr() {
|
@Override public int getIconAttr() { return R.attr.emoji_category_recent; }
|
||||||
return R.attr.emoji_category_recent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public List<String> getEmoji() {
|
@Override public List<String> getEmoji() {
|
||||||
List<String> recent = new ArrayList<>(recentlyUsed);
|
// Populate our recently used list if required (i.e., on first run)
|
||||||
List<String> out = new ArrayList<>(DEFAULT_REACTIONS_LIST.size());
|
if (recentlyUsed == null) {
|
||||||
|
try {
|
||||||
for (int i = 0; i < DEFAULT_REACTIONS_LIST.size(); i++) {
|
String recentlyUsedEmjoiJsonString = prefs.getString(RECENT_EMOJIS_KEY, DEFAULT_REACTION_EMOJIS_JSON_STRING);
|
||||||
if (recent.size() > i) {
|
recentlyUsed = JsonUtil.fromJson(recentlyUsedEmjoiJsonString, LinkedList.class);
|
||||||
out.add(recent.get(i));
|
} catch (Exception e) {
|
||||||
} else {
|
Log.w(TAG, e);
|
||||||
out.add(DEFAULT_REACTIONS_LIST.get(i));
|
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 new ArrayList<>(recentlyUsed);
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public List<Emoji> getDisplayEmoji() {
|
@Override public List<Emoji> getDisplayEmoji() {
|
||||||
return Stream.of(getEmoji()).map(Emoji::new).toList();
|
return Stream.of(getEmoji()).map(Emoji::new).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public boolean hasSpriteMap() {
|
@Override public boolean hasSpriteMap() { return false; }
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Uri getSpriteUri() {
|
public Uri getSpriteUri() { return null; }
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public boolean isDynamic() {
|
@Override public boolean isDynamic() { return true; }
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCodePointSelected(String emoji) {
|
public static void onCodePointSelected(String emoji) {
|
||||||
recentlyUsed.remove(emoji);
|
// If the emoji is already in the recently used list then remove it..
|
||||||
recentlyUsed.add(emoji);
|
if (recentlyUsed.contains(emoji)) { recentlyUsed.removeFirstOccurrence(emoji); }
|
||||||
|
|
||||||
if (recentlyUsed.size() > EMOJI_LRU_SIZE) {
|
// ..and then regardless of whether the emoji used was already in the recently used list or not
|
||||||
Iterator<String> iterator = recentlyUsed.iterator();
|
// it gets placed as the first element in the list..
|
||||||
iterator.next();
|
recentlyUsed.addFirst(emoji);
|
||||||
iterator.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
final LinkedHashSet<String> latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed);
|
// Ensure that we only ever store data for a maximum of 6 recently used emojis (this code will
|
||||||
new AsyncTask<Void, Void, Void>() {
|
// 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
|
// ..which we then save to shared prefs.
|
||||||
protected Void doInBackground(Void... params) {
|
String recentlyUsedAsJsonString = JsonUtil.toJson(recentlyUsed);
|
||||||
try {
|
boolean writeSuccess = prefs.edit().putString(RECENT_EMOJIS_KEY, recentlyUsedAsJsonString).commit();
|
||||||
String serialized = JsonUtil.toJsonThrows(latestRecentlyUsed);
|
if (!writeSuccess) { Log.w(TAG, "Failed to update recently used emojis in shared prefs."); }
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
|
|||||||
|
|
||||||
private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
||||||
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) {
|
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) {
|
if (recipient.isGroupRecipient) {
|
||||||
val title = if (recipient.isOpenGroupRecipient) {
|
val title = if (recipient.isCommunityRecipient) {
|
||||||
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
|
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
|
||||||
context.getString(R.string.ConversationActivity_active_member_count, userCount)
|
context.getString(R.string.ConversationActivity_active_member_count, userCount)
|
||||||
} else {
|
} else {
|
||||||
|
@ -46,7 +46,6 @@ import androidx.lifecycle.Observer
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
import androidx.lifecycle.flowWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.loader.app.LoaderManager
|
import androidx.loader.app.LoaderManager
|
||||||
import androidx.loader.content.Loader
|
import androidx.loader.content.Loader
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@ -57,8 +56,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.consumeAsFlow
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -106,6 +103,7 @@ import org.thoughtcrime.securesms.ApplicationContext
|
|||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
||||||
import org.thoughtcrime.securesms.audio.AudioRecorder
|
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.contacts.SelectContactsActivity.Companion.selectedContactsKey
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate
|
import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity
|
||||||
@ -175,12 +173,11 @@ import org.thoughtcrime.securesms.util.MediaUtil
|
|||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||||
import org.thoughtcrime.securesms.util.SimpleTextWatcher
|
import org.thoughtcrime.securesms.util.SimpleTextWatcher
|
||||||
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
||||||
|
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.time.Instant
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@ -191,8 +188,6 @@ import kotlin.math.abs
|
|||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
private const val TAG = "ConversationActivityV2"
|
private const val TAG = "ConversationActivityV2"
|
||||||
|
|
||||||
@ -281,6 +276,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
private val isScrolledToBottom: Boolean
|
private val isScrolledToBottom: Boolean
|
||||||
get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
|
get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
|
||||||
|
|
||||||
|
private val isScrolledToWithin30dpOfBottom: Boolean
|
||||||
|
get() = binding?.conversationRecyclerView?.isScrolledToWithin30dpOfBottom ?: true
|
||||||
|
|
||||||
private val layoutManager: LinearLayoutManager?
|
private val layoutManager: LinearLayoutManager?
|
||||||
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
|
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
|
||||||
|
|
||||||
@ -336,6 +334,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
lifecycleCoroutineScope = lifecycleScope
|
lifecycleCoroutineScope = lifecycleScope
|
||||||
)
|
)
|
||||||
adapter.visibleMessageViewDelegate = this
|
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
|
adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +355,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
private lateinit var reactionDelegate: ConversationReactionDelegate
|
private lateinit var reactionDelegate: ConversationReactionDelegate
|
||||||
private val reactWithAnyEmojiStartPage = -1
|
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
|
// region Settings
|
||||||
companion object {
|
companion object {
|
||||||
// Extras
|
// Extras
|
||||||
@ -375,12 +383,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
super.onCreate(savedInstanceState, isReady)
|
super.onCreate(savedInstanceState, isReady)
|
||||||
binding = ActivityConversationV2Binding.inflate(layoutInflater)
|
binding = ActivityConversationV2Binding.inflate(layoutInflater)
|
||||||
setContentView(binding!!.root)
|
setContentView(binding!!.root)
|
||||||
|
|
||||||
// messageIdToScroll
|
// messageIdToScroll
|
||||||
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
|
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
|
||||||
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
|
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
|
||||||
val recipient = viewModel.recipient
|
val recipient = viewModel.recipient
|
||||||
val openGroup = recipient.let { viewModel.openGroup }
|
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()
|
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
|
||||||
return finish()
|
return finish()
|
||||||
}
|
}
|
||||||
@ -390,6 +399,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
setUpLinkPreviewObserver()
|
setUpLinkPreviewObserver()
|
||||||
restoreDraftIfNeeded()
|
restoreDraftIfNeeded()
|
||||||
setUpUiStateObserver()
|
setUpUiStateObserver()
|
||||||
|
|
||||||
binding!!.scrollToBottomButton.setOnClickListener {
|
binding!!.scrollToBottomButton.setOnClickListener {
|
||||||
val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
|
val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
|
||||||
val targetPosition = if (reverseMessageList) 0 else adapter.itemCount
|
val targetPosition = if (reverseMessageList) 0 else adapter.itemCount
|
||||||
@ -419,9 +429,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
setUpBlockedBanner()
|
setUpBlockedBanner()
|
||||||
binding!!.searchBottomBar.setEventListener(this)
|
binding!!.searchBottomBar.setEventListener(this)
|
||||||
updateSendAfterApprovalText()
|
updateSendAfterApprovalText()
|
||||||
showOrHideInputIfNeeded()
|
|
||||||
setUpMessageRequestsBar()
|
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)
|
val weakActivity = WeakReference(this)
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@ -563,17 +575,45 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
if (recyclerScrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||||
|
scrollToMostRecentMessageIfWeShould()
|
||||||
|
}
|
||||||
handleRecyclerViewScrolled()
|
handleRecyclerViewScrolled()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
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
|
// called from onCreate
|
||||||
@ -760,13 +800,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
// of the first unread message in the middle of the screen
|
// of the first unread message in the middle of the screen
|
||||||
if (isFirstLoad && !reverseMessageList) {
|
if (isFirstLoad && !reverseMessageList) {
|
||||||
layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2))
|
layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2))
|
||||||
|
|
||||||
if (shouldHighlight) { highlightViewAtPosition(lastSeenItemPosition) }
|
if (shouldHighlight) { highlightViewAtPosition(lastSeenItemPosition) }
|
||||||
|
|
||||||
return lastSeenItemPosition
|
return lastSeenItemPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastSeenItemPosition <= 3) { return lastSeenItemPosition }
|
if (lastSeenItemPosition <= 3) { return lastSeenItemPosition }
|
||||||
|
|
||||||
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
|
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
|
||||||
return lastSeenItemPosition
|
return lastSeenItemPosition
|
||||||
}
|
}
|
||||||
@ -931,11 +970,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
view.glide = glide
|
view.glide = glide
|
||||||
view.onCandidateSelected = { handleMentionSelected(it) }
|
view.onCandidateSelected = { handleMentionSelected(it) }
|
||||||
additionalContentContainer.addView(view)
|
additionalContentContainer.addView(view)
|
||||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
|
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isCommunityRecipient)
|
||||||
this.mentionCandidatesView = view
|
this.mentionCandidatesView = view
|
||||||
view.show(candidates, viewModel.threadId)
|
view.show(candidates, viewModel.threadId)
|
||||||
} else {
|
} else {
|
||||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
|
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isCommunityRecipient)
|
||||||
this.mentionCandidatesView!!.setMentionCandidates(candidates)
|
this.mentionCandidatesView!!.setMentionCandidates(candidates)
|
||||||
}
|
}
|
||||||
isShowingMentionCandidatesView = true
|
isShowingMentionCandidatesView = true
|
||||||
@ -1040,8 +1079,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
private fun handleRecyclerViewScrolled() {
|
private fun handleRecyclerViewScrolled() {
|
||||||
val binding = binding ?: return
|
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
|
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
|
||||||
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
|
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
|
||||||
|
|
||||||
showScrollToBottomButtonIfApplicable()
|
showScrollToBottomButtonIfApplicable()
|
||||||
val maybeTargetVisiblePosition = if (reverseMessageList) layoutManager?.findFirstVisibleItemPosition() else layoutManager?.findLastVisibleItemPosition()
|
val maybeTargetVisiblePosition = if (reverseMessageList) layoutManager?.findFirstVisibleItemPosition() else layoutManager?.findLastVisibleItemPosition()
|
||||||
val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION
|
val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION
|
||||||
@ -1069,6 +1112,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val blindedRecipient = viewModel.blindedRecipient
|
val blindedRecipient = viewModel.blindedRecipient
|
||||||
val binding = binding ?: return
|
val binding = binding ?: return
|
||||||
val openGroup = viewModel.openGroup
|
val openGroup = viewModel.openGroup
|
||||||
|
|
||||||
val (textResource, insertParam) = when {
|
val (textResource, insertParam) = when {
|
||||||
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
|
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
|
||||||
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
|
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
|
||||||
@ -1148,7 +1192,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun copyOpenGroupUrl(thread: Recipient) {
|
override fun copyOpenGroupUrl(thread: Recipient) {
|
||||||
if (!thread.isOpenGroupRecipient) { return }
|
if (!thread.isCommunityRecipient) { return }
|
||||||
|
|
||||||
val threadId = threadDb.getThreadIdIfExistsFor(thread) ?: return
|
val threadId = threadDb.getThreadIdIfExistsFor(thread) ?: return
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
|
||||||
@ -1286,6 +1330,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
sendEmojiRemoval(emoji, messageRecord)
|
sendEmojiRemoval(emoji, messageRecord)
|
||||||
} else {
|
} else {
|
||||||
sendEmojiReaction(emoji, messageRecord)
|
sendEmojiReaction(emoji, messageRecord)
|
||||||
|
RecentEmojiPageModel.onCodePointSelected(emoji) // Save to recently used reaction emojis
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1312,7 +1357,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
} else originalMessage.individualRecipient.address
|
} else originalMessage.individualRecipient.address
|
||||||
// Send it
|
// Send it
|
||||||
reactionMessage.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, true)
|
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
|
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
|
||||||
viewModel.openGroup?.let {
|
viewModel.openGroup?.let {
|
||||||
OpenGroupApi.addReaction(it.room, it.server, messageServerId, emoji)
|
OpenGroupApi.addReaction(it.room, it.server, messageServerId, emoji)
|
||||||
@ -1336,7 +1381,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
} else originalMessage.individualRecipient.address
|
} else originalMessage.individualRecipient.address
|
||||||
|
|
||||||
message.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, false)
|
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
|
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
|
||||||
viewModel.openGroup?.let {
|
viewModel.openGroup?.let {
|
||||||
OpenGroupApi.deleteReaction(it.room, it.server, messageServerId, emoji)
|
OpenGroupApi.deleteReaction(it.room, it.server, messageServerId, emoji)
|
||||||
@ -1731,7 +1776,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
sendAttachments(slideDeck.asAttachments(), body)
|
sendAttachments(slideDeck.asAttachments(), body)
|
||||||
}
|
}
|
||||||
INVITE_CONTACTS -> {
|
INVITE_CONTACTS -> {
|
||||||
if (viewModel.recipient?.isOpenGroupRecipient != true) { return }
|
if (viewModel.recipient?.isCommunityRecipient != true) { return }
|
||||||
val extras = intent?.extras ?: return
|
val extras = intent?.extras ?: return
|
||||||
if (!intent.hasExtra(selectedContactsKey)) { return }
|
if (!intent.hasExtra(selectedContactsKey)) { return }
|
||||||
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
|
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
|
||||||
@ -1797,19 +1842,61 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
handleLongPress(messages.first(), 0) //TODO: begin selection mode
|
handleLongPress(messages.first(), 0) //TODO: begin selection mode
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessages(messages: Set<MessageRecord>) {
|
// The option to "Delete just for me" or "Delete for everyone"
|
||||||
val recipient = viewModel.recipient ?: return
|
private fun showDeleteOrDeleteForEveryoneInCommunityUI(messages: Set<MessageRecord>) {
|
||||||
val allSentByCurrentUser = messages.all { it.isOutgoing }
|
val bottomSheet = DeleteOptionsBottomSheet()
|
||||||
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
|
bottomSheet.recipient = viewModel.recipient!!
|
||||||
if (recipient.isOpenGroupRecipient) {
|
bottomSheet.onDeleteForMeTapped = {
|
||||||
val messageCount = 1
|
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 {
|
showSessionDialog {
|
||||||
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
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))
|
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::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
|
||||||
|
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 the recipient is a community OR a Note-to-Self then we delete the message for everyone
|
||||||
|
if (recipient.isCommunityRecipient || recipient.isLocalNumber) {
|
||||||
|
val messageCount = 1 // Only used for plurals string
|
||||||
|
showSessionDialog {
|
||||||
|
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||||
|
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
||||||
|
button(R.string.delete) {
|
||||||
|
messages.forEach(viewModel::deleteForEveryone); endActionMode()
|
||||||
|
}
|
||||||
cancelButton { 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) {
|
} else if (allSentByCurrentUser && allHasHash) {
|
||||||
val bottomSheet = DeleteOptionsBottomSheet()
|
val bottomSheet = DeleteOptionsBottomSheet()
|
||||||
bottomSheet.recipient = recipient
|
bottomSheet.recipient = recipient
|
||||||
@ -1828,13 +1915,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
endActionMode()
|
endActionMode()
|
||||||
}
|
}
|
||||||
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
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
|
val messageCount = 1
|
||||||
|
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
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))
|
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)
|
cancelButton(::endActionMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1853,7 +1943,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.ConversationFragment_ban_selected_user)
|
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.")
|
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)
|
cancelButton(::endActionMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1935,7 +2025,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val message = messages.first() as MmsMessageRecord
|
val message = messages.first() as MmsMessageRecord
|
||||||
|
|
||||||
// Do not allow the user to download a file attachment before it has finished downloading
|
// Do not allow the user to download a file attachment before it has finished downloading
|
||||||
// TODO: Localise the msg in this toast!
|
|
||||||
if (message.isMediaPending) {
|
if (message.isMediaPending) {
|
||||||
Toast.makeText(this, resources.getString(R.string.conversation_activity__wait_until_attachment_has_finished_downloading), Toast.LENGTH_LONG).show()
|
Toast.makeText(this, resources.getString(R.string.conversation_activity__wait_until_attachment_has_finished_downloading), Toast.LENGTH_LONG).show()
|
||||||
return
|
return
|
||||||
@ -2105,4 +2194,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdapterDataObserver implementation to scroll us to the bottom of the ConversationRecyclerView
|
||||||
|
// when we're already near the bottom and we send or receive a message.
|
||||||
|
inner class ConversationAdapterDataObserver(val recyclerView: ConversationRecyclerView, val adapter: ConversationAdapter) : RecyclerView.AdapterDataObserver() {
|
||||||
|
override fun onChanged() {
|
||||||
|
super.onChanged()
|
||||||
|
if (recyclerView.isScrolledToWithin30dpOfBottom) {
|
||||||
|
// Note: The adapter itemCount is zero based - so calling this with the itemCount in
|
||||||
|
// a non-zero based manner scrolls us to the bottom of the last message (including
|
||||||
|
// to the bottom of long messages as required by Jira SES-789 / GitHub 1364).
|
||||||
|
recyclerView.scrollToPosition(adapter.itemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -22,10 +22,12 @@ import kotlinx.coroutines.launch
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
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.ControlMessageView
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
@ -57,6 +59,7 @@ class ConversationAdapter(
|
|||||||
private val contactCache = SparseArray<Contact>(100)
|
private val contactCache = SparseArray<Contact>(100)
|
||||||
private val contactLoadedCache = SparseBooleanArray(100)
|
private val contactLoadedCache = SparseBooleanArray(100)
|
||||||
private val lastSeen = AtomicLong(originalLastSeen)
|
private val lastSeen = AtomicLong(originalLastSeen)
|
||||||
|
private var lastSentMessageId: Long = -1L
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycleCoroutineScope.launch(IO) {
|
lifecycleCoroutineScope.launch(IO) {
|
||||||
@ -136,7 +139,8 @@ class ConversationAdapter(
|
|||||||
senderId,
|
senderId,
|
||||||
lastSeen.get(),
|
lastSeen.get(),
|
||||||
visibleMessageViewDelegate,
|
visibleMessageViewDelegate,
|
||||||
onAttachmentNeedsDownload
|
onAttachmentNeedsDownload,
|
||||||
|
lastSentMessageId
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!message.isDeleted) {
|
if (!message.isDeleted) {
|
||||||
@ -205,8 +209,23 @@ class ConversationAdapter(
|
|||||||
return messageDB.readerFor(cursor).current
|
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?) {
|
override fun changeCursor(cursor: Cursor?) {
|
||||||
super.changeCursor(cursor)
|
super.changeCursor(cursor)
|
||||||
|
|
||||||
val toRemove = mutableSetOf<MessageRecord>()
|
val toRemove = mutableSetOf<MessageRecord>()
|
||||||
val toDeselect = mutableSetOf<Pair<Int, MessageRecord>>()
|
val toDeselect = mutableSetOf<Pair<Int, MessageRecord>>()
|
||||||
for (selected in selectedItems) {
|
for (selected in selectedItems) {
|
||||||
@ -224,6 +243,11 @@ class ConversationAdapter(
|
|||||||
toDeselect.iterator().forEach { (pos, record) ->
|
toDeselect.iterator().forEach { (pos, record) ->
|
||||||
onDeselect(record, pos)
|
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? {
|
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
|
||||||
|
@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.components.menu.ActionItem
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanBanSelectedUsers
|
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanBanSelectedUsers
|
||||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanDeleteSelectedItems
|
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanDeleteSelectedItems
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||||
@ -541,7 +540,7 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
|
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
|
||||||
}
|
}
|
||||||
// Copy Session ID
|
// 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) })
|
items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_session_id, { handleActionItemClicked(Action.COPY_SESSION_ID) })
|
||||||
}
|
}
|
||||||
// Delete message
|
// Delete message
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
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.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class ConversationViewModel(
|
class ConversationViewModel(
|
||||||
@ -144,9 +152,14 @@ class ConversationViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
|
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)
|
repository.deleteForEveryone(threadId, recipient, message)
|
||||||
|
.onSuccess {
|
||||||
|
Log.d("Loki", "Deleted message ${message.id} ")
|
||||||
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
|
Log.w("Loki", "FAILED TO delete message ${message.id} ")
|
||||||
showMessage("Couldn't delete message due to error: $it")
|
showMessage("Couldn't delete message due to error: $it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,10 +181,15 @@ class ConversationViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch {
|
fun banAndDeleteAll(messageRecord: MessageRecord) = viewModelScope.launch {
|
||||||
repository.banAndDeleteAll(threadId, recipient)
|
|
||||||
|
repository.banAndDeleteAll(threadId, messageRecord.individualRecipient)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
|
// At this point the server side messages have been successfully deleted..
|
||||||
showMessage("Successfully banned user and deleted all their messages")
|
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 {
|
.onFailure {
|
||||||
showMessage("Couldn't execute request due to error: $it")
|
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
|
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
|
||||||
// Copy Session ID
|
// Copy Session ID
|
||||||
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
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
|
// Message detail
|
||||||
menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1
|
menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1
|
||||||
// Resend
|
// Resend
|
||||||
|
@ -50,7 +50,7 @@ object ConversationMenuHelper {
|
|||||||
) {
|
) {
|
||||||
// Prepare
|
// Prepare
|
||||||
menu.clear()
|
menu.clear()
|
||||||
val isOpenGroup = thread.isOpenGroupRecipient
|
val isOpenGroup = thread.isCommunityRecipient
|
||||||
// Base menu (options that should always be present)
|
// Base menu (options that should always be present)
|
||||||
inflater.inflate(R.menu.menu_conversation, menu)
|
inflater.inflate(R.menu.menu_conversation, menu)
|
||||||
// Expiring messages
|
// Expiring messages
|
||||||
@ -253,7 +253,7 @@ object ConversationMenuHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun copyOpenGroupUrl(context: Context, thread: Recipient) {
|
private fun copyOpenGroupUrl(context: Context, thread: Recipient) {
|
||||||
if (!thread.isOpenGroupRecipient) { return }
|
if (!thread.isCommunityRecipient) { return }
|
||||||
val listener = context as? ConversationMenuListener ?: return
|
val listener = context as? ConversationMenuListener ?: return
|
||||||
listener.copyOpenGroupUrl(thread)
|
listener.copyOpenGroupUrl(thread)
|
||||||
}
|
}
|
||||||
@ -300,7 +300,7 @@ object ConversationMenuHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun inviteContacts(context: Context, thread: Recipient) {
|
private fun inviteContacts(context: Context, thread: Recipient) {
|
||||||
if (!thread.isOpenGroupRecipient) { return }
|
if (!thread.isCommunityRecipient) { return }
|
||||||
val intent = Intent(context, SelectContactsActivity::class.java)
|
val intent = Intent(context, SelectContactsActivity::class.java)
|
||||||
val activity = context as AppCompatActivity
|
val activity = context as AppCompatActivity
|
||||||
activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS)
|
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.contacts.Contact.ContactContext
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.ViewUtil
|
import org.session.libsession.utilities.ViewUtil
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import org.session.libsession.utilities.modifyLayoutParams
|
import org.session.libsession.utilities.modifyLayoutParams
|
||||||
@ -131,7 +132,8 @@ class VisibleMessageView : LinearLayout {
|
|||||||
senderSessionID: String,
|
senderSessionID: String,
|
||||||
lastSeen: Long,
|
lastSeen: Long,
|
||||||
delegate: VisibleMessageViewDelegate? = null,
|
delegate: VisibleMessageViewDelegate? = null,
|
||||||
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
onAttachmentNeedsDownload: (Long, Long) -> Unit,
|
||||||
|
lastSentMessageId: Long
|
||||||
) {
|
) {
|
||||||
val threadID = message.threadId
|
val threadID = message.threadId
|
||||||
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
||||||
@ -164,7 +166,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
binding.profilePictureView.publicKey = senderSessionID
|
binding.profilePictureView.publicKey = senderSessionID
|
||||||
binding.profilePictureView.update(message.individualRecipient)
|
binding.profilePictureView.update(message.individualRecipient)
|
||||||
binding.profilePictureView.setOnClickListener {
|
binding.profilePictureView.setOnClickListener {
|
||||||
if (thread.isOpenGroupRecipient) {
|
if (thread.isCommunityRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
||||||
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
||||||
// TODO: support v2 soon
|
// TODO: support v2 soon
|
||||||
@ -177,7 +179,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
maybeShowUserDetails(senderSessionID, threadID)
|
maybeShowUserDetails(senderSessionID, threadID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (thread.isOpenGroupRecipient) {
|
if (thread.isCommunityRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||||
var standardPublicKey = ""
|
var standardPublicKey = ""
|
||||||
var blindedPublicKey: String? = null
|
var blindedPublicKey: String? = null
|
||||||
@ -193,16 +195,20 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected))
|
binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected))
|
||||||
val contactContext =
|
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
|
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
|
||||||
|
|
||||||
// Unread marker
|
// Unread marker
|
||||||
binding.unreadMarkerContainer.isVisible = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing
|
binding.unreadMarkerContainer.isVisible = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing
|
||||||
|
|
||||||
// Date break
|
// Date break
|
||||||
val showDateBreak = isStartOfMessageCluster || snIsSelected
|
val showDateBreak = isStartOfMessageCluster || snIsSelected
|
||||||
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
||||||
binding.dateBreakTextView.isVisible = showDateBreak
|
binding.dateBreakTextView.isVisible = showDateBreak
|
||||||
|
|
||||||
// Message status indicator
|
// Message status indicator
|
||||||
showStatusMessage(message)
|
showStatusMessage(message)
|
||||||
|
|
||||||
// Emoji Reactions
|
// Emoji Reactions
|
||||||
val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams
|
val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams
|
||||||
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||||
@ -238,7 +244,8 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showStatusMessage(message: MessageRecord) {
|
private fun showStatusMessage(message: MessageRecord) {
|
||||||
val disappearing = message.expiresIn > 0
|
|
||||||
|
val scheduledToDisappear = message.expiresIn > 0
|
||||||
|
|
||||||
binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> {
|
binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> {
|
||||||
gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
||||||
@ -250,7 +257,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
|
|
||||||
binding.expirationTimerView.isGone = true
|
binding.expirationTimerView.isGone = true
|
||||||
|
|
||||||
if (message.isOutgoing || disappearing) {
|
if (message.isOutgoing || scheduledToDisappear) {
|
||||||
val (iconID, iconColor, textId) = getMessageStatusImage(message)
|
val (iconID, iconColor, textId) = getMessageStatusImage(message)
|
||||||
textId?.let(binding.messageStatusTextView::setText)
|
textId?.let(binding.messageStatusTextView::setText)
|
||||||
iconColor?.let(binding.messageStatusTextView::setTextColor)
|
iconColor?.let(binding.messageStatusTextView::setTextColor)
|
||||||
@ -258,13 +265,14 @@ class VisibleMessageView : LinearLayout {
|
|||||||
?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
|
?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
|
||||||
?.let(binding.messageStatusImageView::setImageDrawable)
|
?.let(binding.messageStatusImageView::setImageDrawable)
|
||||||
|
|
||||||
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
// Always show the delivery status of the last sent message
|
||||||
val isLastMessage = message.id == lastMessageID
|
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
|
||||||
binding.messageStatusTextView.isVisible =
|
val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
|
||||||
textId != null && (!message.isSent || isLastMessage || disappearing)
|
val isLastSentMessage = lastSentMessageId == message.id
|
||||||
val showTimer = disappearing && !message.isPending
|
|
||||||
binding.messageStatusImageView.isVisible =
|
binding.messageStatusTextView.isVisible = textId != null && (isLastSentMessage || scheduledToDisappear)
|
||||||
iconID != null && !showTimer && (!message.isSent || isLastMessage)
|
val showTimer = scheduledToDisappear && !message.isPending
|
||||||
|
binding.messageStatusImageView.isVisible = iconID != null && !showTimer && (!message.isSent || isLastSentMessage)
|
||||||
|
|
||||||
binding.messageStatusImageView.bringToFront()
|
binding.messageStatusImageView.bringToFront()
|
||||||
binding.expirationTimerView.bringToFront()
|
binding.expirationTimerView.bringToFront()
|
||||||
|
@ -26,6 +26,7 @@ import androidx.annotation.NonNull;
|
|||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.session.libsession.utilities.WindowDebouncer;
|
import org.session.libsession.utilities.WindowDebouncer;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
@ -77,11 +78,11 @@ public abstract class Database {
|
|||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setNotifyConverationListeners(Cursor cursor, long threadId) {
|
protected void setNotifyConversationListeners(Cursor cursor, long threadId) {
|
||||||
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getUriForThread(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);
|
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.ExpirationConfiguration
|
||||||
import org.session.libsession.messaging.messages.ExpirationDatabaseMetadata
|
import org.session.libsession.messaging.messages.ExpirationDatabaseMetadata
|
||||||
import org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX
|
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.COMMUNITY_INBOX_PREFIX
|
||||||
import org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX
|
import org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
|
|
||||||
class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
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}
|
INSERT INTO $TABLE_NAME ($THREAD_ID) SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ID}
|
||||||
FROM ${ThreadDatabase.TABLE_NAME}, ${RecipientDatabase.TABLE_NAME}
|
FROM ${ThreadDatabase.TABLE_NAME}, ${RecipientDatabase.TABLE_NAME}
|
||||||
WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$CLOSED_GROUP_PREFIX%'
|
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 '$COMMUNITY_PREFIX%'
|
||||||
AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$OPEN_GROUP_INBOX_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)
|
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()
|
""".trimIndent()
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import android.content.ContentValues
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE
|
import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE
|
||||||
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
|
|
||||||
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
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 = ?",
|
"${Companion.messageID} = ? AND $messageType = ?",
|
||||||
arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor ->
|
arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor ->
|
||||||
cursor.getInt(serverID).toLong()
|
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()
|
database.beginTransaction()
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ public class MediaDatabase extends Database {
|
|||||||
public Cursor getGalleryMediaForThread(long threadId) {
|
public Cursor getGalleryMediaForThread(long threadId) {
|
||||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = database.rawQuery(GALLERY_MEDIA_QUERY, new String[]{threadId+""});
|
Cursor cursor = database.rawQuery(GALLERY_MEDIA_QUERY, new String[]{threadId+""});
|
||||||
setNotifyConverationListeners(cursor, threadId);
|
setNotifyConversationListeners(cursor, threadId);
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ public class MediaDatabase extends Database {
|
|||||||
public Cursor getDocumentMediaForThread(long threadId) {
|
public Cursor getDocumentMediaForThread(long threadId) {
|
||||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, new String[]{threadId+""});
|
Cursor cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, new String[]{threadId+""});
|
||||||
setNotifyConverationListeners(cursor, threadId);
|
setNotifyConversationListeners(cursor, threadId);
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,9 +19,9 @@ package org.thoughtcrime.securesms.database
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.provider.ContactsContract.CommonDataKinds.BaseTypes
|
|
||||||
import com.annimon.stream.Stream
|
import com.annimon.stream.Stream
|
||||||
import com.google.android.mms.pdu_alt.PduHeaders
|
import com.google.android.mms.pdu_alt.PduHeaders
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
@ -214,7 +214,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
|
|
||||||
fun getMessage(messageId: Long): Cursor {
|
fun getMessage(messageId: Long): Cursor {
|
||||||
val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString()))
|
val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString()))
|
||||||
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId))
|
setNotifyConversationListeners(cursor, getThreadIdForMessage(messageId))
|
||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -630,6 +630,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
if (retrieved.isExpirationUpdate) deleteExpirationTimerMessages(threadId, true.takeUnless { retrieved.isGroup })
|
if (retrieved.isExpirationUpdate) deleteExpirationTimerMessages(threadId, true.takeUnless { retrieved.isGroup })
|
||||||
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
|
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
|
||||||
if (messageId == -1L) {
|
if (messageId == -1L) {
|
||||||
|
Log.w(TAG, "insertSecureDecryptedMessageOutbox believes the MmsDatabase insertion failed.")
|
||||||
return Optional.absent()
|
return Optional.absent()
|
||||||
}
|
}
|
||||||
markAsSent(messageId, true)
|
markAsSent(messageId, true)
|
||||||
@ -859,8 +860,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
*/
|
*/
|
||||||
private fun deleteMessages(messageIds: Array<String?>) {
|
private fun deleteMessages(messageIds: Array<String?>) {
|
||||||
if (messageIds.isEmpty()) {
|
if (messageIds.isEmpty()) {
|
||||||
|
Log.w(TAG, "No message Ids provided to MmsDatabase.deleteMessages - aborting delete operation!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't need thread IDs
|
// don't need thread IDs
|
||||||
val queryBuilder = StringBuilder()
|
val queryBuilder = StringBuilder()
|
||||||
for (i in messageIds.indices) {
|
for (i in messageIds.indices) {
|
||||||
@ -883,6 +886,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
notifyStickerPackListeners()
|
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 {
|
override fun deleteMessage(messageId: Long): Boolean {
|
||||||
val threadId = getThreadIdForMessage(messageId)
|
val threadId = getThreadIdForMessage(messageId)
|
||||||
val attachmentDatabase = get(context).attachmentDatabase()
|
val attachmentDatabase = get(context).attachmentDatabase()
|
||||||
@ -899,14 +904,15 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean {
|
override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean {
|
||||||
val attachmentDatabase = get(context).attachmentDatabase()
|
val argsArray = messageIds.map { "?" }
|
||||||
val groupReceiptDatabase = get(context).groupReceiptDatabase()
|
val argValues = messageIds.map { it.toString() }.toTypedArray()
|
||||||
|
|
||||||
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessages(messageIds) })
|
val db = databaseHelper.writableDatabase
|
||||||
groupReceiptDatabase.deleteRowsForMessages(messageIds)
|
db.delete(
|
||||||
|
TABLE_NAME,
|
||||||
val database = databaseHelper.writableDatabase
|
ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
|
||||||
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
|
argValues
|
||||||
|
)
|
||||||
|
|
||||||
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
|
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
|
||||||
notifyConversationListeners(threadId)
|
notifyConversationListeners(threadId)
|
||||||
@ -1089,8 +1095,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
}
|
}
|
||||||
val whereString = where.substring(0, where.length - 4)
|
val whereString = where.substring(0, where.length - 4)
|
||||||
try {
|
try {
|
||||||
cursor =
|
cursor = db!!.query(TABLE_NAME, arrayOf<String?>(ID), whereString, null, null, null, null)
|
||||||
db!!.query(TABLE_NAME, arrayOf<String?>(ID), whereString, null, null, null, null)
|
|
||||||
val toDeleteStringMessageIds = mutableListOf<String>()
|
val toDeleteStringMessageIds = mutableListOf<String>()
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
toDeleteStringMessageIds += cursor.getLong(0).toString()
|
toDeleteStringMessageIds += cursor.getLong(0).toString()
|
||||||
|
@ -30,6 +30,7 @@ import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
@ -115,6 +116,53 @@ public class MmsSmsDatabase extends Database {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @Nullable MessageRecord getSentMessageFor(long timestamp, String serializedAuthor) {
|
||||||
|
// Early exit if the author is not us
|
||||||
|
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
|
||||||
|
if (!isOwnNumber) {
|
||||||
|
Log.i(TAG, "Asked to find sent messages but provided author is not us - returning null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
|
||||||
|
MmsSmsDatabase.Reader reader = readerFor(cursor);
|
||||||
|
|
||||||
|
MessageRecord messageRecord;
|
||||||
|
while ((messageRecord = reader.getNext()) != null) {
|
||||||
|
if (messageRecord.isOutgoing())
|
||||||
|
{
|
||||||
|
return messageRecord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Could not find any message sent from us at provided timestamp - returning null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageRecord getLastSentMessageRecordFromSender(long threadId, String serializedAuthor) {
|
||||||
|
// Early exit if the author is not us
|
||||||
|
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
|
||||||
|
if (!isOwnNumber) {
|
||||||
|
Log.i(TAG, "Asked to find last sent message but provided author is not us - returning null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
|
|
||||||
|
// Try everything with resources so that they auto-close on end of scope
|
||||||
|
try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) {
|
||||||
|
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
|
||||||
|
MessageRecord messageRecord;
|
||||||
|
while ((messageRecord = reader.getNext()) != null) {
|
||||||
|
if (messageRecord.isOutgoing()) { return messageRecord; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Could not find last sent message from us in given thread - returning null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) {
|
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) {
|
||||||
return getMessageFor(timestamp, author.serialize());
|
return getMessageFor(timestamp, author.serialize());
|
||||||
}
|
}
|
||||||
@ -183,7 +231,7 @@ public class MmsSmsDatabase extends Database {
|
|||||||
String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null;
|
String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null;
|
||||||
|
|
||||||
Cursor cursor = queryTables(PROJECTION, selection, order, limitStr);
|
Cursor cursor = queryTables(PROJECTION, selection, order, limitStr);
|
||||||
setNotifyConverationListeners(cursor, threadId);
|
setNotifyConversationListeners(cursor, threadId);
|
||||||
|
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
@ -209,6 +257,69 @@ public class MmsSmsDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Builds up and returns a list of all all the messages sent by this user in the given thread.
|
||||||
|
// Used to do a pass through our local database to remove records when a user has "Ban & Delete"
|
||||||
|
// called on them in a Community.
|
||||||
|
public Set<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) {
|
||||||
|
|
||||||
|
// Early exit
|
||||||
|
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
|
||||||
|
if (!isOwnNumber) {
|
||||||
|
Log.i(TAG, "Asked to find last sent message but sender isn't us - returning null.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
|
|
||||||
|
// Try everything with resources so that they auto-close on end of scope
|
||||||
|
try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) {
|
||||||
|
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
|
||||||
|
MessageRecord messageRecord;
|
||||||
|
while ((messageRecord = reader.getNext()) != null) {
|
||||||
|
if (messageRecord.isOutgoing()) { return messageRecord.id; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Could not find last sent message from us - returning -1.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
public Cursor getUnread() {
|
public Cursor getUnread() {
|
||||||
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC";
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC";
|
||||||
String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0";
|
String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.database;
|
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.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -123,18 +123,18 @@ public class RecipientDatabase extends Database {
|
|||||||
public static String getUpdateApprovedCommand() {
|
public static String getUpdateApprovedCommand() {
|
||||||
return "UPDATE "+ TABLE_NAME + " " +
|
return "UPDATE "+ TABLE_NAME + " " +
|
||||||
"SET " + APPROVED + " = 1, " + APPROVED_ME + " = 1 " +
|
"SET " + APPROVED + " = 1, " + APPROVED_ME + " = 1 " +
|
||||||
"WHERE " + ADDRESS + " NOT LIKE '" + OPEN_GROUP_PREFIX + "%'";
|
"WHERE " + ADDRESS + " NOT LIKE '" + COMMUNITY_PREFIX + "%'";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUpdateResetApprovedCommand() {
|
public static String getUpdateResetApprovedCommand() {
|
||||||
return "UPDATE "+ TABLE_NAME + " " +
|
return "UPDATE "+ TABLE_NAME + " " +
|
||||||
"SET " + APPROVED + " = 0, " + APPROVED_ME + " = 0 " +
|
"SET " + APPROVED + " = 0, " + APPROVED_ME + " = 0 " +
|
||||||
"WHERE " + ADDRESS + " NOT LIKE '" + OPEN_GROUP_PREFIX + "%'";
|
"WHERE " + ADDRESS + " NOT LIKE '" + COMMUNITY_PREFIX + "%'";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUpdateApprovedSelectConversations() {
|
public static String getUpdateApprovedSelectConversations() {
|
||||||
return "UPDATE "+ TABLE_NAME + " SET "+APPROVED+" = 1, "+APPROVED_ME+" = 1 "+
|
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) "+
|
"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+")))";
|
"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 net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -115,11 +116,9 @@ public class SearchDatabase extends Database {
|
|||||||
public Cursor queryMessages(@NonNull String query) {
|
public Cursor queryMessages(@NonNull String query) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
String prefixQuery = adjustQuery(query);
|
String prefixQuery = adjustQuery(query);
|
||||||
|
|
||||||
int queryLimit = Math.min(query.length()*50,500);
|
int queryLimit = Math.min(query.length()*50,500);
|
||||||
|
|
||||||
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery, String.valueOf(queryLimit) });
|
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery, String.valueOf(queryLimit) });
|
||||||
setNotifyConverationListListeners(cursor);
|
setNotifyConversationListListeners(cursor);
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +127,7 @@ public class SearchDatabase extends Database {
|
|||||||
String prefixQuery = adjustQuery(query);
|
String prefixQuery = adjustQuery(query);
|
||||||
|
|
||||||
Cursor cursor = db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { prefixQuery, String.valueOf(threadId), prefixQuery, String.valueOf(threadId) });
|
Cursor cursor = db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { prefixQuery, String.valueOf(threadId), prefixQuery, String.valueOf(threadId) });
|
||||||
setNotifyConverationListListeners(cursor);
|
setNotifyConversationListListeners(cursor);
|
||||||
return cursor;
|
return cursor;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -621,16 +621,19 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
public Cursor getMessageCursor(long messageId) {
|
public Cursor getMessageCursor(long messageId) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId + ""}, null, null, null);
|
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;
|
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
|
@Override
|
||||||
public boolean deleteMessage(long messageId) {
|
public boolean deleteMessage(long messageId) {
|
||||||
Log.i("MessageDatabase", "Deleting: " + messageId);
|
Log.i("MessageDatabase", "Deleting: " + messageId);
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
long threadId = getThreadIdForMessage(messageId);
|
long threadId = getThreadIdForMessage(messageId);
|
||||||
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
|
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
|
||||||
|
notifyConversationListeners(threadId);
|
||||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||||
return threadDeleted;
|
return threadDeleted;
|
||||||
}
|
}
|
||||||
@ -645,9 +648,6 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
argValues[i] = (messageIds[i] + "");
|
argValues[i] = (messageIds[i] + "");
|
||||||
}
|
}
|
||||||
|
|
||||||
String combinedMessageIdArgss = StringUtils.join(messageIds, ',');
|
|
||||||
String combinedMessageIds = StringUtils.join(messageIds, ',');
|
|
||||||
Log.i("MessageDatabase", "Deleting: " + combinedMessageIds);
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.delete(
|
db.delete(
|
||||||
TABLE_NAME,
|
TABLE_NAME,
|
||||||
|
@ -92,7 +92,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority
|
|||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol
|
import org.thoughtcrime.securesms.util.SessionMetaProtocol
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import kotlin.time.Duration.Companion.days
|
|
||||||
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
|
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
|
||||||
|
|
||||||
private const val TAG = "Storage"
|
private const val TAG = "Storage"
|
||||||
@ -121,7 +120,7 @@ open class Storage(
|
|||||||
)
|
)
|
||||||
volatile.set(newVolatileParams)
|
volatile.set(newVolatileParams)
|
||||||
}
|
}
|
||||||
} else if (address.isOpenGroup) {
|
} else if (address.isCommunity) {
|
||||||
// these should be added on the group join / group info fetch
|
// 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")
|
Log.w("Loki", "Thread created called for open group address, not adding any extra information")
|
||||||
}
|
}
|
||||||
@ -152,7 +151,7 @@ open class Storage(
|
|||||||
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
||||||
volatile.eraseLegacyClosedGroup(sessionId)
|
volatile.eraseLegacyClosedGroup(sessionId)
|
||||||
groups.eraseLegacyGroup(sessionId)
|
groups.eraseLegacyGroup(sessionId)
|
||||||
} else if (address.isOpenGroup) {
|
} else if (address.isCommunity) {
|
||||||
// these should be removed in the group leave / handling new configs
|
// 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")
|
Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
|
||||||
}
|
}
|
||||||
@ -257,7 +256,7 @@ open class Storage(
|
|||||||
// recipient closed group
|
// recipient closed group
|
||||||
recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize()))
|
recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize()))
|
||||||
// recipient is open group
|
// recipient is open group
|
||||||
recipient.isOpenGroupRecipient -> {
|
recipient.isCommunityRecipient -> {
|
||||||
val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return
|
val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return
|
||||||
BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
|
BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
|
||||||
config.getOrConstructCommunity(base, room, pubKey)
|
config.getOrConstructCommunity(base, room, pubKey)
|
||||||
@ -327,7 +326,7 @@ open class Storage(
|
|||||||
setRecipientApprovedMe(targetRecipient, true)
|
setRecipientApprovedMe(targetRecipient, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (message.threadID == null && !targetRecipient.isOpenGroupRecipient) {
|
if (message.threadID == null && !targetRecipient.isCommunityRecipient) {
|
||||||
// open group recipients should explicitly create threads
|
// open group recipients should explicitly create threads
|
||||||
message.threadID = getOrCreateThreadIdFor(targetAddress)
|
message.threadID = getOrCreateThreadIdFor(targetAddress)
|
||||||
}
|
}
|
||||||
@ -767,13 +766,36 @@ open class Storage(
|
|||||||
|
|
||||||
override fun markAsSent(timestamp: Long, author: String) {
|
override fun markAsSent(timestamp: Long, author: String) {
|
||||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
val messageRecord = database.getSentMessageFor(timestamp, author)
|
||||||
|
if (messageRecord == null) {
|
||||||
|
Log.w(TAG, "Failed to retrieve local message record in Storage.markAsSent - aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (messageRecord.isMms) {
|
if (messageRecord.isMms) {
|
||||||
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
|
DatabaseComponent.get(context).mmsDatabase().markAsSent(messageRecord.getId(), true)
|
||||||
mmsDatabase.markAsSent(messageRecord.getId(), true)
|
|
||||||
} else {
|
} else {
|
||||||
val smsDatabase = DatabaseComponent.get(context).smsDatabase()
|
DatabaseComponent.get(context).smsDatabase().markAsSent(messageRecord.getId(), true)
|
||||||
smsDatabase.markAsSent(messageRecord.getId(), true)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method that marks a message as sent in Communities (only!) - where the server modifies the
|
||||||
|
// message timestamp and as such we cannot use that to identify the local message.
|
||||||
|
override fun markAsSentToCommunity(threadId: Long, messageID: Long) {
|
||||||
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
|
val message = database.getLastSentMessageRecordFromSender(threadId, TextSecurePreferences.getLocalNumber(context))
|
||||||
|
|
||||||
|
// Ensure we can find the local message..
|
||||||
|
if (message == null) {
|
||||||
|
Log.w(TAG, "Could not find local message in Storage.markAsSentToCommunity - aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..and mark as sent if found.
|
||||||
|
if (message.isMms) {
|
||||||
|
DatabaseComponent.get(context).mmsDatabase().markAsSent(message.getId(), true)
|
||||||
|
} else {
|
||||||
|
DatabaseComponent.get(context).smsDatabase().markAsSent(message.getId(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -808,7 +830,11 @@ open class Storage(
|
|||||||
|
|
||||||
override fun markUnidentified(timestamp: Long, author: String) {
|
override fun markUnidentified(timestamp: Long, author: String) {
|
||||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
val messageRecord = database.getMessageFor(timestamp, author)
|
||||||
|
if (messageRecord == null) {
|
||||||
|
Log.w(TAG, "Could not identify message with timestamp: $timestamp from author: $author")
|
||||||
|
return
|
||||||
|
}
|
||||||
if (messageRecord.isMms) {
|
if (messageRecord.isMms) {
|
||||||
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
|
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
|
||||||
mmsDatabase.markUnidentified(messageRecord.getId(), true)
|
mmsDatabase.markUnidentified(messageRecord.getId(), true)
|
||||||
@ -818,6 +844,26 @@ open class Storage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method that marks a message as unidentified in Communities (only!) - where the server
|
||||||
|
// modifies the message timestamp and as such we cannot use that to identify the local message.
|
||||||
|
override fun markUnidentifiedInCommunity(threadId: Long, messageId: Long) {
|
||||||
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
|
val message = database.getLastSentMessageRecordFromSender(threadId, TextSecurePreferences.getLocalNumber(context))
|
||||||
|
|
||||||
|
// Check to ensure the message exists
|
||||||
|
if (message == null) {
|
||||||
|
Log.w(TAG, "Could not find local message in Storage.markUnidentifiedInCommunity - aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark it as unidentified if we found the message successfully
|
||||||
|
if (message.isMms) {
|
||||||
|
DatabaseComponent.get(context).mmsDatabase().markUnidentified(message.getId(), true)
|
||||||
|
} else {
|
||||||
|
DatabaseComponent.get(context).smsDatabase().markUnidentified(message.getId(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun markAsSentFailed(timestamp: Long, author: String, error: Exception) {
|
override fun markAsSentFailed(timestamp: Long, author: String, error: Exception) {
|
||||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||||
@ -971,7 +1017,10 @@ open class Storage(
|
|||||||
val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, 0, true, null, listOf(), listOf())
|
val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, 0, true, null, listOf(), listOf())
|
||||||
val mmsDB = DatabaseComponent.get(context).mmsDatabase()
|
val mmsDB = DatabaseComponent.get(context).mmsDatabase()
|
||||||
val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return
|
if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) {
|
||||||
|
Log.w(TAG, "Bailing from insertOutgoingInfoMessage because we believe the message has already been sent!")
|
||||||
|
return
|
||||||
|
}
|
||||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true)
|
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true)
|
||||||
mmsDB.markAsSent(infoMessageID, true)
|
mmsDB.markAsSent(infoMessageID, true)
|
||||||
}
|
}
|
||||||
@ -1289,7 +1338,7 @@ open class Storage(
|
|||||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
||||||
)
|
)
|
||||||
groups.set(newGroupInfo)
|
groups.set(newGroupInfo)
|
||||||
} else if (threadRecipient.isOpenGroupRecipient) {
|
} else if (threadRecipient.isCommunityRecipient) {
|
||||||
val openGroup = getOpenGroup(threadID) ?: return
|
val openGroup = getOpenGroup(threadID) ?: return
|
||||||
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
|
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
|
||||||
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
|
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
import static org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX;
|
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 static org.thoughtcrime.securesms.database.GroupDatabase.GROUP_ID;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
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);
|
Cursor cursor = cursors.size() > 1 ? new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) : cursors.get(0);
|
||||||
setNotifyConverationListListeners(cursor);
|
setNotifyConversationListListeners(cursor);
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,7 +491,7 @@ public class ThreadDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Cursor getConversationList() {
|
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 ";
|
"AND " + ARCHIVED + " = 0 ";
|
||||||
return getConversationList(where);
|
return getConversationList(where);
|
||||||
}
|
}
|
||||||
@ -502,7 +502,7 @@ public class ThreadDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Cursor getApprovedConversationList() {
|
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 ";
|
"AND " + ARCHIVED + " = 0 ";
|
||||||
return getConversationList(where);
|
return getConversationList(where);
|
||||||
}
|
}
|
||||||
@ -515,18 +515,12 @@ public class ThreadDatabase extends Database {
|
|||||||
return getConversationList(where);
|
return getConversationList(where);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cursor getArchivedConversationList() {
|
|
||||||
String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
|
|
||||||
"AND " + ARCHIVED + " = 1 ";
|
|
||||||
return getConversationList(where);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Cursor getConversationList(String where) {
|
private Cursor getConversationList(String where) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
String query = createQuery(where, 0);
|
String query = createQuery(where, 0);
|
||||||
Cursor cursor = db.rawQuery(query, null);
|
Cursor cursor = db.rawQuery(query, null);
|
||||||
|
|
||||||
setNotifyConverationListListeners(cursor);
|
setNotifyConversationListListeners(cursor);
|
||||||
|
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
@ -547,7 +541,7 @@ public class ThreadDatabase extends Database {
|
|||||||
// edge case where we set the last seen time for a conversation before it loads messages (joining community for example)
|
// 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();
|
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
||||||
Recipient forThreadId = getRecipientForThreadId(threadId);
|
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();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
@ -750,10 +744,7 @@ public class ThreadDatabase extends Database {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
MmsSmsDatabase.Reader reader = null;
|
try (MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId))) {
|
||||||
|
|
||||||
try {
|
|
||||||
reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId));
|
|
||||||
MessageRecord record = null;
|
MessageRecord record = null;
|
||||||
if (reader != null) {
|
if (reader != null) {
|
||||||
record = reader.getNext();
|
record = reader.getNext();
|
||||||
@ -771,11 +762,10 @@ public class ThreadDatabase extends Database {
|
|||||||
deleteThread(threadId);
|
deleteThread(threadId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// todo: add empty snippet that clears existing data
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (reader != null)
|
|
||||||
reader.close();
|
|
||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
}
|
}
|
||||||
@ -822,7 +812,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
private boolean deleteThreadOnEmpty(long threadId) {
|
private boolean deleteThreadOnEmpty(long threadId) {
|
||||||
Recipient threadRecipient = getRecipientForThreadId(threadId);
|
Recipient threadRecipient = getRecipientForThreadId(threadId);
|
||||||
return threadRecipient != null && !threadRecipient.isOpenGroupRecipient();
|
return threadRecipient != null && !threadRecipient.isCommunityRecipient();
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
||||||
|
@ -22,7 +22,10 @@ import android.text.SpannableString;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,6 +51,9 @@ public abstract class DisplayRecord {
|
|||||||
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
|
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
|
||||||
long type, int readReceiptCount)
|
long type, int readReceiptCount)
|
||||||
{
|
{
|
||||||
|
// TODO: This gets hit very, very often and it likely shouldn't - place a Log.d statement in it to see.
|
||||||
|
//Log.d("[ACL]", "Creating a display record with delivery status of: " + deliveryStatus);
|
||||||
|
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
this.recipient = recipient;
|
this.recipient = recipient;
|
||||||
this.dateSent = dateSent;
|
this.dateSent = dateSent;
|
||||||
@ -76,9 +82,7 @@ public abstract class DisplayRecord {
|
|||||||
&& deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
|
&& deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSent() {
|
public boolean isSent() { return MmsSmsColumns.Types.isSentType(type); }
|
||||||
return !isFailed() && !isPending();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSyncing() {
|
public boolean isSyncing() {
|
||||||
return MmsSmsColumns.Types.isSyncingType(type);
|
return MmsSmsColumns.Types.isSyncingType(type);
|
||||||
@ -99,9 +103,10 @@ public abstract class DisplayRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPending() {
|
public boolean isPending() {
|
||||||
return MmsSmsColumns.Types.isPendingMessageType(type)
|
boolean isPending = MmsSmsColumns.Types.isPendingMessageType(type) &&
|
||||||
&& !MmsSmsColumns.Types.isIdentityVerified(type)
|
!MmsSmsColumns.Types.isIdentityVerified(type) &&
|
||||||
&& !MmsSmsColumns.Types.isIdentityDefault(type);
|
!MmsSmsColumns.Types.isIdentityDefault(type);
|
||||||
|
return isPending;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRead() { return readReceiptCount > 0; }
|
public boolean isRead() { return readReceiptCount > 0; }
|
||||||
|
@ -78,7 +78,7 @@ class CreateGroupFragment : Fragment() {
|
|||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
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()
|
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
val selectedMembers = adapter.selectedMembers
|
val selectedMembers = adapter.selectedMembers
|
||||||
|
@ -11,7 +11,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
|
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
|
||||||
import org.thoughtcrime.securesms.util.getConversationUnread
|
import org.thoughtcrime.securesms.util.getConversationUnread
|
||||||
import javax.inject.Inject
|
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.visibility = if (!recipient.isGroupRecipient && !recipient.isLocalNumber) View.VISIBLE else View.GONE
|
||||||
binding.copyConversationId.setOnClickListener(this)
|
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.copyCommunityUrl.setOnClickListener(this)
|
||||||
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
||||||
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
||||||
|
@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredWidth
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.material.Divider
|
import androidx.compose.material.Divider
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
@ -99,11 +98,11 @@ import org.thoughtcrime.securesms.preferences.SettingsActivity
|
|||||||
import org.thoughtcrime.securesms.showMuteDialog
|
import org.thoughtcrime.securesms.showMuteDialog
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
import org.thoughtcrime.securesms.ui.AppTheme
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import org.thoughtcrime.securesms.ui.OutlineButton
|
import org.thoughtcrime.securesms.ui.OutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
import org.thoughtcrime.securesms.ui.SessionShieldIcon
|
import org.thoughtcrime.securesms.ui.SessionShieldIcon
|
||||||
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
||||||
import org.thoughtcrime.securesms.ui.contentDescription
|
|
||||||
import org.thoughtcrime.securesms.ui.h8
|
import org.thoughtcrime.securesms.ui.h8
|
||||||
import org.thoughtcrime.securesms.ui.small
|
import org.thoughtcrime.securesms.ui.small
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
@ -226,7 +225,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up empty state view
|
// Set up empty state view
|
||||||
binding.emptyStateContainer.setContent { EmptyView() }
|
binding.emptyStateContainer.setContent { EmptyView(ApplicationContext.getInstance(this).newAccount) }
|
||||||
|
|
||||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||||
startObservingUpdates()
|
startObservingUpdates()
|
||||||
@ -317,7 +316,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
val newData = contactResults + messageResults
|
val newData = contactResults + messageResults
|
||||||
|
|
||||||
globalSearchAdapter.setNewData(result.query, newData)
|
globalSearchAdapter.setNewData(result.query, newData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -365,16 +363,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
) {
|
) {
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Row {
|
Row {
|
||||||
Text("Save your recovery password", style = MaterialTheme.typography.h8)
|
Text(stringResource(R.string.save_your_recovery_password), style = MaterialTheme.typography.h8)
|
||||||
Spacer(Modifier.requiredWidth(8.dp))
|
Spacer(Modifier.requiredWidth(8.dp))
|
||||||
SessionShieldIcon()
|
SessionShieldIcon()
|
||||||
}
|
}
|
||||||
Text("Save your recovery password to make sure you don't lose access to your account.", style = MaterialTheme.typography.small)
|
Text(stringResource(R.string.save_your_recovery_password_to_make_sure_you_don_t_lose_access_to_your_account), style = MaterialTheme.typography.small)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(12.dp))
|
Spacer(Modifier.width(12.dp))
|
||||||
OutlineButton(
|
OutlineButton(
|
||||||
stringResource(R.string.continue_2),
|
stringResource(R.string.continue_2),
|
||||||
Modifier.align(Alignment.CenterVertically)
|
Modifier.align(Alignment.CenterVertically),
|
||||||
|
contentDescription = GetString(R.string.AccessibilityId_reveal_recovery_phrase_button)
|
||||||
) { startRecoveryPasswordActivity() }
|
) { startRecoveryPasswordActivity() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -382,7 +381,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmptyView() {
|
private fun EmptyView(newAccount: Boolean) {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@ -392,18 +391,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.emoji_tada),
|
painter = painterResource(id = if (newAccount) R.drawable.emoji_tada_large else R.drawable.ic_logo_large),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.Unspecified
|
tint = Color.Unspecified
|
||||||
)
|
)
|
||||||
Text("Account Created", style = MaterialTheme.typography.h4, textAlign = TextAlign.Center)
|
if (newAccount) Text(stringResource(R.string.onboardingAccountCreated), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center)
|
||||||
Text("Welcome to Session", color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
if (newAccount) Text(stringResource(R.string.welcome_to_session), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||||
|
|
||||||
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
Text("You don't have any conversations yet",
|
Text(
|
||||||
|
stringResource(R.string.conversationsNone),
|
||||||
style = MaterialTheme.typography.h8,
|
style = MaterialTheme.typography.h8,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(bottom = 12.dp))
|
modifier = Modifier.padding(bottom = 12.dp))
|
||||||
Text("Hit the plus button to start a chat, create a group, or join an official communitiy!", textAlign = TextAlign.Center)
|
Text(stringResource(R.string.onboardingHitThePlusButton), textAlign = TextAlign.Center)
|
||||||
Spacer(modifier = Modifier.weight(2f))
|
Spacer(modifier = Modifier.weight(2f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -585,7 +586,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
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 threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) ?: return@onCopyConversationId Unit
|
||||||
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit
|
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit
|
||||||
|
|
||||||
@ -718,7 +719,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
val message = if (recipient.isGroupRecipient) {
|
val message = if (recipient.isGroupRecipient) {
|
||||||
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
|
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
|
||||||
if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) {
|
if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) {
|
||||||
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
getString(R.string.admin_group_leave_warning)
|
||||||
} else {
|
} else {
|
||||||
resources.getString(R.string.activity_home_leave_group_dialog_message)
|
resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||||
}
|
}
|
||||||
@ -774,7 +775,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
private fun hideMessageRequests() {
|
private fun hideMessageRequests() {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
text("Hide message requests?")
|
text(getString(R.string.hide_message_requests))
|
||||||
button(R.string.yes) {
|
button(R.string.yes) {
|
||||||
textSecurePreferences.setHasHiddenMessageRequests()
|
textSecurePreferences.setHasHiddenMessageRequests()
|
||||||
setupMessageRequestsBanner()
|
setupMessageRequestsBanner()
|
||||||
|
@ -21,6 +21,7 @@ import network.loki.messenger.R
|
|||||||
import network.loki.messenger.databinding.ActivityPathBinding
|
import network.loki.messenger.databinding.ActivityPathBinding
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.Snode
|
import org.session.libsignal.utilities.Snode
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
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.session.libsignal.utilities.IdPrefix
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@ -34,6 +33,9 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
|
|
||||||
private lateinit var binding: FragmentUserDetailsBottomSheetBinding
|
private lateinit var binding: FragmentUserDetailsBottomSheetBinding
|
||||||
|
|
||||||
|
private var previousContactNickname: String = ""
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ARGUMENT_PUBLIC_KEY = "publicKey"
|
const val ARGUMENT_PUBLIC_KEY = "publicKey"
|
||||||
const val ARGUMENT_THREAD_ID = "threadId"
|
const val ARGUMENT_THREAD_ID = "threadId"
|
||||||
@ -89,10 +91,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
&& !threadRecipient.isOpenGroupInboxRecipient
|
&& !threadRecipient.isOpenGroupInboxRecipient
|
||||||
&& !threadRecipient.isOpenGroupOutboxRecipient
|
&& !threadRecipient.isOpenGroupOutboxRecipient
|
||||||
|
|
||||||
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
|
publicKeyTextView.isVisible = !threadRecipient.isCommunityRecipient
|
||||||
&& !threadRecipient.isOpenGroupInboxRecipient
|
&& !threadRecipient.isOpenGroupInboxRecipient
|
||||||
&& !threadRecipient.isOpenGroupOutboxRecipient
|
&& !threadRecipient.isOpenGroupOutboxRecipient
|
||||||
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient || IdPrefix.fromValue(publicKey)?.isBlinded() == true
|
messageButton.isVisible = !threadRecipient.isCommunityRecipient || IdPrefix.fromValue(publicKey)?.isBlinded() == true
|
||||||
publicKeyTextView.text = publicKey
|
publicKeyTextView.text = publicKey
|
||||||
publicKeyTextView.setOnLongClickListener {
|
publicKeyTextView.setOnLongClickListener {
|
||||||
val clipboard =
|
val clipboard =
|
||||||
@ -130,9 +132,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
nameTextViewContainer.visibility = View.VISIBLE
|
nameTextViewContainer.visibility = View.VISIBLE
|
||||||
nameEditTextContainer.visibility = View.INVISIBLE
|
nameEditTextContainer.visibility = View.INVISIBLE
|
||||||
var newNickName: String? = null
|
var newNickName: String? = null
|
||||||
if (nicknameEditText.text.isNotEmpty()) {
|
if (nicknameEditText.text.isNotEmpty() && nicknameEditText.text.trim().length != 0) {
|
||||||
newNickName = nicknameEditText.text.toString()
|
newNickName = nicknameEditText.text.toString()
|
||||||
}
|
}
|
||||||
|
else { newNickName = previousContactNickname }
|
||||||
val publicKey = recipient.address.serialize()
|
val publicKey = recipient.address.serialize()
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val contact = storage.getContactWithSessionID(publicKey) ?: Contact(publicKey)
|
val contact = storage.getContactWithSessionID(publicKey) ?: Contact(publicKey)
|
||||||
@ -145,6 +148,9 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
fun showSoftKeyboard() {
|
fun showSoftKeyboard() {
|
||||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
imm?.showSoftInput(binding.nicknameEditText, 0)
|
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() {
|
fun hideSoftKeyboard() {
|
||||||
|
@ -53,7 +53,7 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
|
|||||||
public void setMostRecentSender(Recipient recipient, Recipient threadRecipient) {
|
public void setMostRecentSender(Recipient recipient, Recipient threadRecipient) {
|
||||||
String displayName = recipient.toShortString();
|
String displayName = recipient.toShortString();
|
||||||
if (threadRecipient.isGroupRecipient()) {
|
if (threadRecipient.isGroupRecipient()) {
|
||||||
displayName = getGroupDisplayName(recipient, threadRecipient.isOpenGroupRecipient());
|
displayName = getGroupDisplayName(recipient, threadRecipient.isCommunityRecipient());
|
||||||
}
|
}
|
||||||
if (privacy.isDisplayContact()) {
|
if (privacy.isDisplayContact()) {
|
||||||
setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, displayName));
|
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) {
|
public void addMessageBody(@NonNull Recipient sender, Recipient threadRecipient, @Nullable CharSequence body) {
|
||||||
String displayName = sender.toShortString();
|
String displayName = sender.toShortString();
|
||||||
if (threadRecipient.isGroupRecipient()) {
|
if (threadRecipient.isGroupRecipient()) {
|
||||||
displayName = getGroupDisplayName(sender, threadRecipient.isOpenGroupRecipient());
|
displayName = getGroupDisplayName(sender, threadRecipient.isCommunityRecipient());
|
||||||
}
|
}
|
||||||
if (privacy.isDisplayMessage()) {
|
if (privacy.isDisplayMessage()) {
|
||||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||||
|
@ -125,7 +125,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||||||
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
||||||
|
|
||||||
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
|
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
|
||||||
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient());
|
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isCommunityRecipient());
|
||||||
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||||||
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
||||||
|
|
||||||
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
|
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
|
||||||
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient());
|
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isCommunityRecipient());
|
||||||
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import androidx.compose.ui.platform.ComposeView
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
@ -34,7 +35,10 @@ import org.thoughtcrime.securesms.showSessionDialog
|
|||||||
import org.thoughtcrime.securesms.ui.AppTheme
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
import org.thoughtcrime.securesms.ui.BorderlessButton
|
import org.thoughtcrime.securesms.ui.BorderlessButton
|
||||||
import org.thoughtcrime.securesms.ui.FilledButton
|
import org.thoughtcrime.securesms.ui.FilledButton
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import org.thoughtcrime.securesms.ui.OutlineButton
|
import org.thoughtcrime.securesms.ui.OutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
||||||
import org.thoughtcrime.securesms.ui.classicDarkColors
|
import org.thoughtcrime.securesms.ui.classicDarkColors
|
||||||
import org.thoughtcrime.securesms.ui.contentDescription
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
import org.thoughtcrime.securesms.ui.session_accent
|
import org.thoughtcrime.securesms.ui.session_accent
|
||||||
@ -52,7 +56,7 @@ class LandingActivity : BaseActionBarActivity() {
|
|||||||
setUpActionBarSessionLogo(true)
|
setUpActionBarSessionLogo(true)
|
||||||
|
|
||||||
ComposeView(this)
|
ComposeView(this)
|
||||||
.apply { setContent { LandingScreen() } }
|
.apply { setContent { AppTheme { LandingScreen() } } }
|
||||||
.let(::setContentView)
|
.let(::setContentView)
|
||||||
|
|
||||||
IdentityKeyUtil.generateIdentityKeyPair(this)
|
IdentityKeyUtil.generateIdentityKeyPair(this)
|
||||||
@ -62,9 +66,17 @@ class LandingActivity : BaseActionBarActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun LandingScreen(
|
||||||
|
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
||||||
|
) {
|
||||||
|
PreviewTheme(themeResId) {
|
||||||
|
LandingScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LandingScreen() {
|
private fun LandingScreen() {
|
||||||
AppTheme {
|
|
||||||
Column(modifier = Modifier.padding(horizontal = 36.dp)) {
|
Column(modifier = Modifier.padding(horizontal = 36.dp)) {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Text(stringResource(R.string.onboardingBubblePrivacyInYourPocket), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center)
|
Text(stringResource(R.string.onboardingBubblePrivacyInYourPocket), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center)
|
||||||
@ -82,31 +94,43 @@ class LandingActivity : BaseActionBarActivity() {
|
|||||||
text = stringResource(R.string.onboardingAccountCreate),
|
text = stringResource(R.string.onboardingAccountCreate),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(262.dp)
|
.width(262.dp)
|
||||||
.align(Alignment.CenterHorizontally)) { startPickDisplayNameActivity() }
|
.align(Alignment.CenterHorizontally),
|
||||||
|
contentDescription = GetString(R.string.AccessibilityId_create_account_button)
|
||||||
|
) { startPickDisplayNameActivity() }
|
||||||
Spacer(modifier = Modifier.height(14.dp))
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
FilledButton(text = stringResource(R.string.onboardingAccountExists), modifier = Modifier
|
FilledButton(
|
||||||
|
text = stringResource(R.string.onboardingAccountExists),
|
||||||
|
modifier = Modifier
|
||||||
.width(262.dp)
|
.width(262.dp)
|
||||||
.align(Alignment.CenterHorizontally)) { startLinkDeviceActivity() }
|
.align(Alignment.CenterHorizontally),
|
||||||
|
contentDescription = GetString(R.string.AccessibilityId_restore_account_button)
|
||||||
|
) { startLinkDeviceActivity() }
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BorderlessButton(
|
BorderlessButton(
|
||||||
text = stringResource(R.string.onboardingTosPrivacy),
|
text = stringResource(R.string.onboardingTosPrivacy),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(262.dp)
|
.width(262.dp)
|
||||||
.align(Alignment.CenterHorizontally),
|
.align(Alignment.CenterHorizontally),
|
||||||
|
contentDescription = GetString(R.string.AccessibilityId_privacy_policy_link),
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
lineHeight = 13.sp
|
lineHeight = 13.sp
|
||||||
) { openDialog() }
|
) { openDialog() }
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun openDialog() {
|
private fun openDialog() {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.urlOpen)
|
title(R.string.urlOpen)
|
||||||
text(R.string.urlOpenBrowser)
|
text(R.string.urlOpenBrowser)
|
||||||
button(R.string.activity_landing_terms_of_service) { open("https://getsession.org/terms-of-service") }
|
button(
|
||||||
button(R.string.activity_landing_privacy_policy) { open("https://getsession.org/privacy-policy") }
|
R.string.activity_landing_terms_of_service,
|
||||||
|
contentDescriptionRes = R.string.AccessibilityId_terms_of_service_link
|
||||||
|
) { open("https://getsession.org/terms-of-service") }
|
||||||
|
button(
|
||||||
|
R.string.activity_landing_privacy_policy,
|
||||||
|
contentDescriptionRes = R.string.AccessibilityId_privacy_policy_link
|
||||||
|
) { open("https://getsession.org/privacy-policy") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,8 +160,8 @@ class LandingActivity : BaseActionBarActivity() {
|
|||||||
private fun ChatText(
|
private fun ChatText(
|
||||||
text: String,
|
text: String,
|
||||||
color: Color,
|
color: Color,
|
||||||
textColor: Color = Color.Unspecified,
|
modifier: Modifier = Modifier,
|
||||||
modifier: Modifier = Modifier
|
textColor: Color = Color.Unspecified
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
|
@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.ui.baseBold
|
|||||||
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
||||||
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
||||||
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val TAG = "LinkDeviceActivity"
|
private const val TAG = "LinkDeviceActivity"
|
||||||
@ -130,15 +131,22 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on
|
|||||||
Spacer(Modifier.size(24.dp))
|
Spacer(Modifier.size(24.dp))
|
||||||
SessionOutlinedTextField(
|
SessionOutlinedTextField(
|
||||||
text = state.recoveryPhrase,
|
text = state.recoveryPhrase,
|
||||||
|
modifier = Modifier
|
||||||
|
.contentDescription(R.string.AccessibilityId_recovery_phrase_input)
|
||||||
|
.padding(horizontal = 64.dp),
|
||||||
placeholder = stringResource(R.string.recoveryPasswordEnter),
|
placeholder = stringResource(R.string.recoveryPasswordEnter),
|
||||||
onChange = onChange,
|
onChange = onChange,
|
||||||
onContinue = onContinue,
|
onContinue = onContinue,
|
||||||
error = state.error,
|
error = state.error
|
||||||
modifier = Modifier.padding(horizontal = 64.dp)
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.size(12.dp))
|
Spacer(Modifier.size(12.dp))
|
||||||
state.error?.let {
|
state.error?.let {
|
||||||
Text(it, style = MaterialTheme.typography.baseBold, color = MaterialTheme.colors.error)
|
Text(
|
||||||
|
it,
|
||||||
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_error_message),
|
||||||
|
style = MaterialTheme.typography.baseBold,
|
||||||
|
color = MaterialTheme.colors.error
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.weight(2f))
|
Spacer(Modifier.weight(2f))
|
||||||
OutlineButton(
|
OutlineButton(
|
||||||
|
@ -18,16 +18,21 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.AppTextSecurePreferences
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.onboarding.messagenotifications.startPNModeActivity
|
import org.thoughtcrime.securesms.onboarding.messagenotifications.startPNModeActivity
|
||||||
import org.thoughtcrime.securesms.onboarding.pickname.startPickDisplayNameActivity
|
import org.thoughtcrime.securesms.onboarding.pickname.startPickDisplayNameActivity
|
||||||
import org.thoughtcrime.securesms.ui.AppTheme
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
import org.thoughtcrime.securesms.ui.ProgressArc
|
import org.thoughtcrime.securesms.ui.ProgressArc
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -63,6 +68,8 @@ class LoadingActivity: BaseActionBarActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
ApplicationContext.getInstance(this).newAccount = false
|
||||||
|
|
||||||
ComposeView(this)
|
ComposeView(this)
|
||||||
.apply { setContent { LoadingScreen() } }
|
.apply { setContent { LoadingScreen() } }
|
||||||
.let(::setContentView)
|
.let(::setContentView)
|
||||||
@ -98,9 +105,9 @@ class LoadingActivity: BaseActionBarActivity() {
|
|||||||
AppTheme {
|
AppTheme {
|
||||||
Column {
|
Column {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
ProgressArc(animatable.value, modifier = Modifier.align(Alignment.CenterHorizontally))
|
ProgressArc(animatable.value, modifier = Modifier.align(Alignment.CenterHorizontally).contentDescription(R.string.AccessibilityId_loading_animation))
|
||||||
Text("One moment please..", modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h6)
|
Text(stringResource(R.string.waitOneMoment), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h6)
|
||||||
Text("Loading your account", modifier = Modifier.align(Alignment.CenterHorizontally))
|
Text(stringResource(R.string.loadAccountProgressMessage), modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||||
Spacer(modifier = Modifier.weight(2f))
|
Spacer(modifier = Modifier.weight(2f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,9 +38,11 @@ import org.thoughtcrime.securesms.BaseActionBarActivity
|
|||||||
import org.thoughtcrime.securesms.home.HomeActivity
|
import org.thoughtcrime.securesms.home.HomeActivity
|
||||||
import org.thoughtcrime.securesms.notifications.PushRegistry
|
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||||
import org.thoughtcrime.securesms.ui.AppTheme
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import org.thoughtcrime.securesms.ui.OutlineButton
|
import org.thoughtcrime.securesms.ui.OutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
import org.thoughtcrime.securesms.ui.h8
|
import org.thoughtcrime.securesms.ui.h8
|
||||||
import org.thoughtcrime.securesms.ui.h9
|
import org.thoughtcrime.securesms.ui.h9
|
||||||
import org.thoughtcrime.securesms.ui.session_accent
|
import org.thoughtcrime.securesms.ui.session_accent
|
||||||
@ -61,16 +63,16 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
|
|||||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||||
|
|
||||||
ComposeView(this)
|
ComposeView(this)
|
||||||
.apply { setContent { MessageNotifications() } }
|
.apply { setContent { MessageNotificationsScreen() } }
|
||||||
.let(::setContentView)
|
.let(::setContentView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageNotifications() {
|
private fun MessageNotificationsScreen() {
|
||||||
val state by viewModel.stateFlow.collectAsState()
|
val state by viewModel.stateFlow.collectAsState()
|
||||||
|
|
||||||
AppTheme {
|
AppTheme {
|
||||||
MessageNotifications(state, viewModel::setEnabled, ::register)
|
MessageNotificationsScreen(state, viewModel::setEnabled, ::register)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,30 +89,31 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
|
|||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageNotificationsPreview(
|
fun MessageNotificationsScreenPreview(
|
||||||
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
||||||
) {
|
) {
|
||||||
PreviewTheme(themeResId) {
|
PreviewTheme(themeResId) {
|
||||||
MessageNotifications()
|
MessageNotificationsScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageNotifications(
|
fun MessageNotificationsScreen(
|
||||||
state: MessageNotificationsState = MessageNotificationsState(),
|
state: MessageNotificationsState = MessageNotificationsState(),
|
||||||
setEnabled: (Boolean) -> Unit = {},
|
setEnabled: (Boolean) -> Unit = {},
|
||||||
onContinue: () -> Unit = {}
|
onContinue: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Column(Modifier.padding(horizontal = 32.dp)) {
|
Column(Modifier.padding(horizontal = 32.dp)) {
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
Text("Message notifications", style = MaterialTheme.typography.h4)
|
Text(stringResource(R.string.notificationsMessage), style = MaterialTheme.typography.h4)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Text("There are two ways Session can notify you of new messages.")
|
Text(stringResource(R.string.onboardingMessageNotificationExplaination))
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
NotificationRadioButton(
|
NotificationRadioButton(
|
||||||
R.string.activity_pn_mode_fast_mode,
|
R.string.activity_pn_mode_fast_mode,
|
||||||
R.string.activity_pn_mode_fast_mode_explanation,
|
R.string.activity_pn_mode_fast_mode_explanation,
|
||||||
R.string.activity_pn_mode_recommended_option_tag,
|
R.string.activity_pn_mode_recommended_option_tag,
|
||||||
|
contentDescription = R.string.AccessibilityId_fast_mode_notifications_button,
|
||||||
selected = state.pushEnabled,
|
selected = state.pushEnabled,
|
||||||
onClick = { setEnabled(true) }
|
onClick = { setEnabled(true) }
|
||||||
)
|
)
|
||||||
@ -118,6 +121,7 @@ fun MessageNotifications(
|
|||||||
NotificationRadioButton(
|
NotificationRadioButton(
|
||||||
R.string.activity_pn_mode_slow_mode,
|
R.string.activity_pn_mode_slow_mode,
|
||||||
R.string.activity_pn_mode_slow_mode_explanation,
|
R.string.activity_pn_mode_slow_mode_explanation,
|
||||||
|
contentDescription = R.string.AccessibilityId_slow_mode_notifications_button,
|
||||||
selected = state.pushDisabled,
|
selected = state.pushDisabled,
|
||||||
onClick = { setEnabled(false) }
|
onClick = { setEnabled(false) }
|
||||||
)
|
)
|
||||||
@ -138,13 +142,14 @@ fun NotificationRadioButton(
|
|||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
@StringRes explanation: Int,
|
@StringRes explanation: Int,
|
||||||
@StringRes tag: Int? = null,
|
@StringRes tag: Int? = null,
|
||||||
|
@StringRes contentDescription: Int? = null,
|
||||||
selected: Boolean = false,
|
selected: Boolean = false,
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f).contentDescription(contentDescription),
|
||||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.background, contentColor = Color.White),
|
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.background, contentColor = Color.White),
|
||||||
border = if (selected) BorderStroke(ButtonDefaults.OutlinedBorderSize, session_accent) else ButtonDefaults.outlinedBorder,
|
border = if (selected) BorderStroke(ButtonDefaults.OutlinedBorderSize, session_accent) else ButtonDefaults.outlinedBorder,
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
@ -31,6 +31,8 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.AppTextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.onboarding.messagenotifications.startPNModeActivity
|
import org.thoughtcrime.securesms.onboarding.messagenotifications.startPNModeActivity
|
||||||
import org.thoughtcrime.securesms.ui.AppTheme
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
@ -105,7 +107,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
|
|||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.displayName,
|
value = state.displayName,
|
||||||
modifier = Modifier.contentDescription(R.string.displayNameEnter),
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_enter_display_name),
|
||||||
onValueChange = { onChange(it) },
|
onValueChange = { onChange(it) },
|
||||||
placeholder = { Text(stringResource(R.string.displayNameEnter)) },
|
placeholder = { Text(stringResource(R.string.displayNameEnter)) },
|
||||||
colors = outlinedTextFieldColors(state.error != null),
|
colors = outlinedTextFieldColors(state.error != null),
|
||||||
@ -137,6 +139,8 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Context.startPickDisplayNameActivity(failedToLoad: Boolean = false, flags: Int = 0) {
|
fun Context.startPickDisplayNameActivity(failedToLoad: Boolean = false, flags: Int = 0) {
|
||||||
|
ApplicationContext.getInstance(this).newAccount = !failedToLoad
|
||||||
|
|
||||||
Intent(this, PickDisplayNameActivity::class.java)
|
Intent(this, PickDisplayNameActivity::class.java)
|
||||||
.apply { putExtra(EXTRA_PICK_NEW_NAME, failedToLoad) }
|
.apply { putExtra(EXTRA_PICK_NEW_NAME, failedToLoad) }
|
||||||
.also { it.flags = flags }
|
.also { it.flags = flags }
|
||||||
|
@ -6,7 +6,6 @@ import android.graphics.Bitmap
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@ -15,7 +14,6 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@ -27,9 +25,13 @@ import androidx.compose.material.Icon
|
|||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
@ -42,11 +44,16 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
import org.thoughtcrime.securesms.ui.AppTheme
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
|
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import org.thoughtcrime.securesms.ui.LaunchedEffectAsync
|
||||||
import org.thoughtcrime.securesms.ui.LocalExtraColors
|
import org.thoughtcrime.securesms.ui.LocalExtraColors
|
||||||
import org.thoughtcrime.securesms.ui.OutlineButton
|
import org.thoughtcrime.securesms.ui.OutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
@ -54,8 +61,10 @@ import org.thoughtcrime.securesms.ui.SessionShieldIcon
|
|||||||
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
||||||
import org.thoughtcrime.securesms.ui.classicDarkColors
|
import org.thoughtcrime.securesms.ui.classicDarkColors
|
||||||
import org.thoughtcrime.securesms.ui.colorDestructive
|
import org.thoughtcrime.securesms.ui.colorDestructive
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
import org.thoughtcrime.securesms.ui.h8
|
import org.thoughtcrime.securesms.ui.h8
|
||||||
import org.thoughtcrime.securesms.ui.small
|
import org.thoughtcrime.securesms.ui.small
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class RecoveryPasswordActivity : BaseActionBarActivity() {
|
class RecoveryPasswordActivity : BaseActionBarActivity() {
|
||||||
|
|
||||||
@ -74,21 +83,22 @@ class RecoveryPasswordActivity : BaseActionBarActivity() {
|
|||||||
|
|
||||||
private fun onHide() {
|
private fun onHide() {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title("Hide Recovery Password Permanently")
|
title(R.string.recoveryPasswordHidePermanently)
|
||||||
text("Without your recovery password, you cannot load your account on new devices.\n" +
|
htmlText(R.string.recoveryPasswordHidePermanentlyDescription1)
|
||||||
"\n" +
|
|
||||||
"We strongly recommend you save your recovery password in a safe and secure place before continuing.")
|
|
||||||
destructiveButton(R.string.continue_2) { onHideConfirm() }
|
destructiveButton(R.string.continue_2) { onHideConfirm() }
|
||||||
button(R.string.cancel) {}
|
cancelButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onHideConfirm() {
|
private fun onHideConfirm() {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title("Hide Recovery Password Permanently")
|
title(R.string.recoveryPasswordHidePermanently)
|
||||||
text("Are you sure you want to permanently hide your recovery password on this device? This cannot be undone.")
|
text(R.string.recoveryPasswordHidePermanentlyDescription2)
|
||||||
button(R.string.cancel) {}
|
cancelButton()
|
||||||
destructiveButton(R.string.yes) {
|
destructiveButton(
|
||||||
|
R.string.yes,
|
||||||
|
contentDescription = R.string.AccessibilityId_confirm_button
|
||||||
|
) {
|
||||||
viewModel.permanentlyHidePassword()
|
viewModel.permanentlyHidePassword()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
@ -98,7 +108,7 @@ class RecoveryPasswordActivity : BaseActionBarActivity() {
|
|||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewMessageDetails(
|
fun PreviewRecoveryPassword(
|
||||||
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
||||||
) {
|
) {
|
||||||
PreviewTheme(themeResId) {
|
PreviewTheme(themeResId) {
|
||||||
@ -132,24 +142,21 @@ fun RecoveryPasswordCell(seed: String = "", qrBitmap: Bitmap? = null, copySeed:(
|
|||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val copied = remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
CellWithPaddingAndMargin {
|
CellWithPaddingAndMargin {
|
||||||
Column {
|
Column {
|
||||||
Row {
|
Row {
|
||||||
Text("Recovery Password")
|
Text(stringResource(R.string.sessionRecoveryPassword))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
SessionShieldIcon()
|
SessionShieldIcon()
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("Use your recovery password to load your account on new devices.\n\nYour account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone.")
|
Text(stringResource(R.string.recoveryPasswordDescription))
|
||||||
|
|
||||||
AnimatedVisibility(!showQr.value) {
|
AnimatedVisibility(!showQr.value) {
|
||||||
Text(
|
Text(
|
||||||
seed,
|
seed,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.contentDescription(R.string.AccessibilityId_hide_recovery_password_button)
|
||||||
.padding(vertical = 24.dp)
|
.padding(vertical = 24.dp)
|
||||||
.border(
|
.border(
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
@ -181,16 +188,21 @@ fun RecoveryPasswordCell(seed: String = "", qrBitmap: Bitmap? = null, copySeed:(
|
|||||||
|
|
||||||
AnimatedVisibility(!showQr.value) {
|
AnimatedVisibility(!showQr.value) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
||||||
Crossfade(targetState = if (copied.value) R.string.copied else R.string.copy, modifier = Modifier.weight(1f), label = "Copy to Copied CrossFade") {
|
OutlineButton(
|
||||||
OutlineButton(text = stringResource(it), modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.onPrimary) { copySeed(); copied.value = true }
|
modifier = Modifier.weight(1f),
|
||||||
|
color = MaterialTheme.colors.onPrimary,
|
||||||
|
onClick = copySeed,
|
||||||
|
temporaryContent = { Text(stringResource(R.string.copied)) }
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.copy))
|
||||||
}
|
}
|
||||||
OutlineButton(text = "View QR", modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) { showQr.toggle() }
|
OutlineButton(text = stringResource(R.string.qrView), modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) { showQr.toggle() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(showQr.value, modifier = Modifier.align(Alignment.CenterHorizontally)) {
|
AnimatedVisibility(showQr.value, modifier = Modifier.align(Alignment.CenterHorizontally)) {
|
||||||
OutlineButton(
|
OutlineButton(
|
||||||
text = "View Password",
|
text = stringResource(R.string.recoveryPasswordView),
|
||||||
color = MaterialTheme.colors.onPrimary,
|
color = MaterialTheme.colors.onPrimary,
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
) { showQr.toggle() }
|
) { showQr.toggle() }
|
||||||
@ -229,11 +241,12 @@ fun HideRecoveryPasswordCell(onHide: () -> Unit = {}) {
|
|||||||
CellWithPaddingAndMargin {
|
CellWithPaddingAndMargin {
|
||||||
Row {
|
Row {
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(text = "Hide Recovery Password", style = MaterialTheme.typography.h8)
|
Text(text = stringResource(R.string.recoveryPasswordHideRecoveryPassword), style = MaterialTheme.typography.h8)
|
||||||
Text(text = "Permanently hide your recovery password on this device.")
|
Text(text = stringResource(R.string.recoveryPasswordHideRecoveryPasswordDescription))
|
||||||
}
|
}
|
||||||
OutlineButton(
|
OutlineButton(
|
||||||
"Hide",
|
stringResource(R.string.hide),
|
||||||
|
contentDescription = GetString(R.string.AccessibilityId_hide_recovery_password_button),
|
||||||
modifier = Modifier.align(Alignment.CenterVertically),
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
color = colorDestructive
|
color = colorDestructive
|
||||||
) { onHide() }
|
) { onHide() }
|
||||||
|
@ -5,9 +5,14 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
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() {
|
private fun shareLogs() {
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.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()
|
Toast.makeText(requireActivity(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
.onAllGranted {
|
.onAllGranted {
|
||||||
ShareLogsDialog().show(parentFragmentManager,"Share Logs Dialog")
|
ShareLogsDialog(::updateExportButtonAndProgressBarUI).show(parentFragmentManager,"Share Logs Dialog")
|
||||||
}
|
}
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import android.view.ActionMode
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
@ -215,6 +216,21 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
binding.displayNameEditText.selectAll()
|
binding.displayNameEditText.selectAll()
|
||||||
binding.displayNameEditText.requestFocus()
|
binding.displayNameEditText.requestFocus()
|
||||||
inputMethodManager.showSoftInput(binding.displayNameEditText, 0)
|
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 {
|
} else {
|
||||||
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
||||||
}
|
}
|
||||||
|
@ -11,55 +11,73 @@ import android.os.Bundle
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
import network.loki.messenger.BuildConfig
|
import network.loki.messenger.BuildConfig
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
|
||||||
import org.session.libsignal.utilities.ExternalStorageUtil
|
import org.session.libsignal.utilities.ExternalStorageUtil
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.createSessionDialog
|
import org.thoughtcrime.securesms.createSessionDialog
|
||||||
import org.thoughtcrime.securesms.util.FileProviderUtil
|
import org.thoughtcrime.securesms.util.FileProviderUtil
|
||||||
import org.thoughtcrime.securesms.util.StreamUtil
|
import org.thoughtcrime.securesms.util.StreamUtil
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
import java.util.concurrent.TimeUnit
|
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
|
private var shareJob: Job? = null
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||||
title(R.string.dialog_share_logs_title)
|
title(R.string.dialog_share_logs_title)
|
||||||
text(R.string.dialog_share_logs_explanation)
|
text(R.string.dialog_share_logs_explanation)
|
||||||
button(R.string.share, dismiss = false) { shareLogs() }
|
button(R.string.share, dismiss = false) { runShareLogsJob() }
|
||||||
cancelButton { dismiss() }
|
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()
|
shareJob?.cancel()
|
||||||
|
|
||||||
|
updateCallback(true)
|
||||||
|
|
||||||
shareJob = lifecycleScope.launch(Dispatchers.IO) {
|
shareJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val persistentLogger = ApplicationContext.getInstance(context).persistentLogger
|
val persistentLogger = ApplicationContext.getInstance(context).persistentLogger
|
||||||
try {
|
try {
|
||||||
|
Log.d(TAG, "Starting share logs job...")
|
||||||
|
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
val outputUri: Uri = ExternalStorageUtil.getDownloadUri()
|
val outputUri: Uri = ExternalStorageUtil.getDownloadUri()
|
||||||
val mediaUri = getExternalFile()
|
val mediaUri = getExternalFile() ?: return@launch
|
||||||
if (mediaUri == null) {
|
|
||||||
// show toast saying media saved
|
|
||||||
dismiss()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val inputStream = persistentLogger.logs.get().byteInputStream()
|
val inputStream = persistentLogger.logs.get().byteInputStream()
|
||||||
val updateValues = ContentValues()
|
val updateValues = ContentValues()
|
||||||
|
|
||||||
|
// Add details into the output or media files as appropriate
|
||||||
if (outputUri.scheme == ContentResolver.SCHEME_FILE) {
|
if (outputUri.scheme == ContentResolver.SCHEME_FILE) {
|
||||||
FileOutputStream(mediaUri.path).use { outputStream ->
|
FileOutputStream(mediaUri.path).use { outputStream ->
|
||||||
StreamUtil.copy(inputStream, outputStream)
|
StreamUtil.copy(inputStream, outputStream)
|
||||||
@ -73,6 +91,7 @@ class ShareLogsDialog : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT > 28) {
|
if (Build.VERSION.SDK_INT > 28) {
|
||||||
updateValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
updateValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||||
}
|
}
|
||||||
@ -95,13 +114,35 @@ class ShareLogsDialog : DialogFragment() {
|
|||||||
}
|
}
|
||||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
|
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
|
||||||
}
|
}
|
||||||
|
|
||||||
dismiss()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
Log.e("Loki", "Error saving logs", e)
|
Log.e("Loki", "Error saving logs", e)
|
||||||
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
|
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()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,5 +199,4 @@ class ShareLogsDialog : DialogFragment() {
|
|||||||
return context.contentResolver.insert(outputUri, contentValues)
|
return context.contentResolver.insert(outputUri, contentValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,13 +1,22 @@
|
|||||||
package org.thoughtcrime.securesms.repository
|
package org.thoughtcrime.securesms.repository
|
||||||
|
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
import app.cash.copper.Query
|
import app.cash.copper.Query
|
||||||
import app.cash.copper.flow.observeQuery
|
import app.cash.copper.flow.observeQuery
|
||||||
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
import org.session.libsession.database.MessageDataProvider
|
import org.session.libsession.database.MessageDataProvider
|
||||||
import org.session.libsession.messaging.messages.Destination
|
import org.session.libsession.messaging.messages.Destination
|
||||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||||
@ -22,7 +31,10 @@ import org.session.libsession.utilities.Address
|
|||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||||
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
|
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
|
||||||
@ -39,10 +51,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
|||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
interface ConversationRepository {
|
interface ConversationRepository {
|
||||||
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
||||||
@ -55,37 +65,19 @@ interface ConversationRepository {
|
|||||||
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
||||||
fun setBlocked(recipient: Recipient, blocked: Boolean)
|
fun setBlocked(recipient: Recipient, blocked: Boolean)
|
||||||
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
||||||
|
fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord)
|
||||||
fun setApproved(recipient: Recipient, isApproved: Boolean)
|
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?
|
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 banUser(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||||
|
|
||||||
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||||
|
|
||||||
suspend fun deleteThread(threadId: Long): ResultOf<Unit>
|
suspend fun deleteThread(threadId: Long): ResultOf<Unit>
|
||||||
|
|
||||||
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit>
|
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit>
|
||||||
|
|
||||||
suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit>
|
suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit>
|
||||||
|
|
||||||
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||||
|
|
||||||
fun declineMessageRequest(threadId: Long)
|
fun declineMessageRequest(threadId: Long)
|
||||||
|
|
||||||
fun hasReceived(threadId: Long): Boolean
|
fun hasReceived(threadId: Long): Boolean
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultConversationRepository @Inject constructor(
|
class DefaultConversationRepository @Inject constructor(
|
||||||
@ -184,6 +176,15 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
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) {
|
override fun setApproved(recipient: Recipient, isApproved: Boolean) {
|
||||||
storage.setRecipientApproved(recipient, isApproved)
|
storage.setRecipientApproved(recipient, isApproved)
|
||||||
}
|
}
|
||||||
@ -196,18 +197,38 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||||
MessageSender.send(unsendRequest, recipient.address)
|
MessageSender.send(unsendRequest, recipient.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||||
if (openGroup != null) {
|
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)
|
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||||
.success {
|
.success {
|
||||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
continuation.resume(ResultOf.Success(Unit))
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
}.fail { error ->
|
}.fail { error ->
|
||||||
|
Log.w("TAG", "Call to OpenGroupApi.deleteForEveryone failed - attempting to resume..")
|
||||||
continuation.resumeWithException(error)
|
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 {
|
} else {
|
||||||
|
smsDb.deleteMessage(message.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // If this thread is NOT in a Community
|
||||||
|
{
|
||||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
messageDataProvider.getServerHashForMessage(message.id, message.isMms)?.let { serverHash ->
|
messageDataProvider.getServerHashForMessage(message.id, message.isMms)?.let { serverHash ->
|
||||||
var publicKey = recipient.address.serialize()
|
var publicKey = recipient.address.serialize()
|
||||||
@ -218,6 +239,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
.success {
|
.success {
|
||||||
continuation.resume(ResultOf.Success(Unit))
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
}.fail { error ->
|
}.fail { error ->
|
||||||
|
Log.w("[onversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..")
|
||||||
continuation.resumeWithException(error)
|
continuation.resumeWithException(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,7 +247,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? {
|
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
|
messageDataProvider.getServerHashForMessage(message.id, message.isMms) ?: return null
|
||||||
return UnsendRequest(
|
return UnsendRequest(
|
||||||
author = message.takeUnless { it.isOutgoing }?.run { individualRecipient.address.contactIdentifier() } ?: textSecurePreferences.getLocalNumber(),
|
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> =
|
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> =
|
||||||
suspendCoroutine { continuation ->
|
suspendCoroutine { continuation ->
|
||||||
|
// Note: This sessionId could be the blinded Id
|
||||||
val sessionID = recipient.address.toString()
|
val sessionID = recipient.address.toString()
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||||
|
|
||||||
OpenGroupApi.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
|
OpenGroupApi.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
|
||||||
.success {
|
.success {
|
||||||
continuation.resume(ResultOf.Success(Unit))
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
|
@ -95,6 +95,16 @@ public class SearchRepository {
|
|||||||
Stopwatch timer = new Stopwatch("FtsQuery");
|
Stopwatch timer = new Stopwatch("FtsQuery");
|
||||||
|
|
||||||
String cleanQuery = sanitizeQuery(query);
|
String cleanQuery = sanitizeQuery(query);
|
||||||
|
|
||||||
|
// If the search is for a single character and it was stripped by `sanitizeQuery` then abort
|
||||||
|
// the search for an empty string to avoid SQLite error.
|
||||||
|
if (cleanQuery.length() == 0)
|
||||||
|
{
|
||||||
|
Log.d(TAG, "Aborting empty search query.");
|
||||||
|
timer.stop(TAG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
timer.split("clean");
|
timer.split("clean");
|
||||||
|
|
||||||
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
|
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
|
||||||
@ -119,10 +129,11 @@ public class SearchRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
long startTime = System.currentTimeMillis();
|
// If the sanitized search query is empty then abort the search to prevent SQLite errors.
|
||||||
CursorList<MessageResult> messages = queryMessages(sanitizeQuery(query), threadId);
|
String cleanQuery = sanitizeQuery(query).trim();
|
||||||
Log.d(TAG, "[ConversationQuery] " + (System.currentTimeMillis() - startTime) + " ms");
|
if (cleanQuery.isEmpty()) { return; }
|
||||||
|
|
||||||
|
CursorList<MessageResult> messages = queryMessages(cleanQuery, threadId);
|
||||||
callback.onResult(messages);
|
callback.onResult(messages);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,12 @@ import androidx.compose.material.RadioButton
|
|||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TextButton
|
import androidx.compose.material.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
@ -58,6 +64,9 @@ import androidx.compose.ui.unit.TextUnit
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsession.utilities.runIf
|
import org.session.libsession.utilities.runIf
|
||||||
@ -65,17 +74,19 @@ import org.thoughtcrime.securesms.components.ProfilePictureView
|
|||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCard
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCard
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OutlineButton(
|
fun OutlineButton(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
contentDescription: GetString = GetString(text),
|
||||||
color: Color = LocalExtraColors.current.prominentButtonColor,
|
color: Color = LocalExtraColors.current.prominentButtonColor,
|
||||||
loading: Boolean = false,
|
loading: Boolean = false,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = modifier.contentDescription(GetString(text)),
|
modifier = modifier.contentDescription(contentDescription),
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
border = BorderStroke(1.dp, color),
|
border = BorderStroke(1.dp, color),
|
||||||
shape = RoundedCornerShape(50), // = 50% percent
|
shape = RoundedCornerShape(50), // = 50% percent
|
||||||
@ -96,7 +107,68 @@ fun OutlineButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FilledButton(text: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
fun OutlineButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = LocalExtraColors.current.prominentButtonColor,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
content: @Composable () -> Unit = {}
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
border = BorderStroke(1.dp, color),
|
||||||
|
shape = RoundedCornerShape(percent = 50),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = color,
|
||||||
|
backgroundColor = Color.Unspecified
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OutlineButton(
|
||||||
|
temporaryContent: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = LocalExtraColors.current.prominentButtonColor,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
content: @Composable () -> Unit = {}
|
||||||
|
) {
|
||||||
|
var clicked by remember { mutableStateOf(false) }
|
||||||
|
if (clicked) LaunchedEffectAsync {
|
||||||
|
delay(2.seconds)
|
||||||
|
clicked = false
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = {
|
||||||
|
onClick()
|
||||||
|
clicked = true
|
||||||
|
},
|
||||||
|
border = BorderStroke(1.dp, color),
|
||||||
|
shape = RoundedCornerShape(percent = 50),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = color,
|
||||||
|
backgroundColor = Color.Unspecified
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(clicked) {
|
||||||
|
temporaryContent()
|
||||||
|
}
|
||||||
|
AnimatedVisibility(!clicked) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FilledButton(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentDescription: GetString? = GetString(text),
|
||||||
|
onClick: () -> Unit) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = modifier.size(108.dp, 34.dp),
|
modifier = modifier.size(108.dp, 34.dp),
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
@ -126,6 +198,7 @@ fun BorderlessButtonSecondary(
|
|||||||
fun BorderlessButton(
|
fun BorderlessButton(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
contentDescription: GetString = GetString(text),
|
||||||
fontSize: TextUnit = TextUnit.Unspecified,
|
fontSize: TextUnit = TextUnit.Unspecified,
|
||||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||||
contentColor: Color = MaterialTheme.colors.onBackground,
|
contentColor: Color = MaterialTheme.colors.onBackground,
|
||||||
@ -134,7 +207,7 @@ fun BorderlessButton(
|
|||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier.contentDescription(contentDescription),
|
||||||
shape = RoundedCornerShape(percent = 50),
|
shape = RoundedCornerShape(percent = 50),
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
contentColor = contentColor,
|
contentColor = contentColor,
|
||||||
@ -284,8 +357,10 @@ fun <T> TitledRadioButton(option: RadioOption<T>, onClick: () -> Unit) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Modifier.contentDescription(text: GetString?): Modifier {
|
fun Modifier.contentDescription(text: GetString?): Modifier {
|
||||||
|
return text?.let {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
return text?.let { semantics { contentDescription = it(context) } } ?: this
|
semantics { contentDescription = it(context) }
|
||||||
|
} ?: this
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -435,3 +510,8 @@ fun RowScope.SessionShieldIcon() {
|
|||||||
.wrapContentSize(unbounded = true)
|
.wrapContentSize(unbounded = true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LaunchedEffectAsync(block: suspend CoroutineScope.() -> Unit) {
|
||||||
|
rememberCoroutineScope().apply { LaunchedEffect(Unit) { launch { block() } } }
|
||||||
|
}
|
||||||
|
@ -193,7 +193,7 @@ object ConfigurationMessageUtilities {
|
|||||||
while (current != null) {
|
while (current != null) {
|
||||||
val recipient = current.recipient
|
val recipient = current.recipient
|
||||||
val contact = when {
|
val contact = when {
|
||||||
recipient.isOpenGroupRecipient -> {
|
recipient.isCommunityRecipient -> {
|
||||||
val openGroup = storage.getOpenGroup(current.threadId) ?: continue
|
val openGroup = storage.getOpenGroup(current.threadId) ?: continue
|
||||||
val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue
|
val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue
|
||||||
convoConfig.getOrConstructCommunity(base, room, pubKey)
|
convoConfig.getOrConstructCommunity(base, room, pubKey)
|
||||||
@ -279,7 +279,7 @@ object ConfigurationMessageUtilities {
|
|||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val DELETE_INACTIVE_ONE_TO_ONES: String = """
|
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()
|
""".trimIndent()
|
||||||
|
|
||||||
}
|
}
|
@ -37,3 +37,8 @@ val RecyclerView.isScrolledToBottom: Boolean
|
|||||||
get() = computeVerticalScrollOffset().coerceAtLeast(0) +
|
get() = computeVerticalScrollOffset().coerceAtLeast(0) +
|
||||||
computeVerticalScrollExtent() +
|
computeVerticalScrollExtent() +
|
||||||
toPx(50, resources) >= computeVerticalScrollRange()
|
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
|
return getOneToOne(recipient.address.serialize())?.unread == true
|
||||||
} else if (recipient.isClosedGroupRecipient) {
|
} else if (recipient.isClosedGroupRecipient) {
|
||||||
return getLegacyClosedGroup(GroupUtil.doubleDecodeGroupId(recipient.address.toGroupString()))?.unread == true
|
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
|
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(thread.threadId) ?: return false
|
||||||
return getCommunity(openGroup.server, openGroup.room)?.unread == true
|
return getCommunity(openGroup.server, openGroup.room)?.unread == true
|
||||||
}
|
}
|
||||||
|
@ -408,6 +408,10 @@ class CallManager(
|
|||||||
|
|
||||||
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
|
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
|
||||||
localCameraState = newCameraState
|
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) {
|
fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) {
|
||||||
@ -639,7 +643,11 @@ class CallManager(
|
|||||||
peerConnection?.let { connection ->
|
peerConnection?.let { connection ->
|
||||||
connection.flipCamera()
|
connection.flipCamera()
|
||||||
localCameraState = connection.getCameraState()
|
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) {
|
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
|
||||||
// mirror rotation offset
|
|
||||||
rotationVideoSink.mirrored = newCameraState.activeDirection == CameraState.Direction.FRONT
|
|
||||||
cameraEventListener.onCameraSwitchCompleted(newCameraState)
|
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")
|
Log.w(TAG, "Tried to flip camera without capturer or less than 2 cameras")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
activeDirection = PENDING
|
activeDirection = PENDING // Note: The activeDirection will be PENDING until `onCameraSwitchDone`
|
||||||
capturer.switchCamera(this)
|
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>
|
@ -1,80 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="96dp"
|
|
||||||
android:height="96dp"
|
|
||||||
android:viewportWidth="96"
|
|
||||||
android:viewportHeight="96">
|
|
||||||
<path
|
|
||||||
android:pathData="M5.586,92.452C7.289,94.298 14.301,91.08 19.836,88.5C23.984,86.573 40.581,79.762 48.831,76.23C51.059,75.277 54.299,74.033 56.631,70.965C58.701,68.235 64.191,56.64 53.136,44.895C41.916,32.97 30.359,36.263 26.039,39.322C23.496,41.123 21.426,45.18 20.496,47.243C16.566,55.958 10.964,71.925 8.684,78.202C7.011,82.83 3.899,90.622 5.586,92.452Z"
|
|
||||||
android:fillColor="#FFC107"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M19.386,49.868C19.491,51.173 19.753,53.295 20.653,57.382C21.268,60.188 22.273,63.127 23.091,65.07C25.543,70.912 28.986,73.268 32.473,75.158C38.398,78.368 42.426,78.967 42.426,78.967L37.596,80.94C37.596,80.94 34.671,80.332 30.681,78.368C26.878,76.493 22.918,73.32 20.023,67.11C18.771,64.418 18.043,61.807 17.623,59.737C17.106,57.173 17.023,55.717 17.023,55.717L19.386,49.868Z"
|
|
||||||
android:fillColor="#FF8F00"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M13.453,65.077C13.453,65.077 14.053,69.945 18.073,76.088C22.783,83.272 29.36,84.45 29.36,84.45L24.988,86.25C24.988,86.25 20.105,84.757 15.463,78.42C12.568,74.467 11.758,69.743 11.758,69.743L13.453,65.077Z"
|
|
||||||
android:fillColor="#FF8F00"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M9.291,76.613C9.291,76.613 10.386,80.813 12.786,83.948C15.644,87.69 19.281,88.777 19.281,88.777L15.929,90.262C15.929,90.262 13.386,89.73 10.604,86.205C8.489,83.527 7.889,80.452 7.889,80.452L9.291,76.613Z"
|
|
||||||
android:fillColor="#FF8F00"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M7.469,87.277C7.319,86.94 7.319,86.558 7.477,86.228L26.579,46.612L29.722,58.425L9.622,87.457C9.082,88.268 7.866,88.162 7.469,87.277Z"
|
|
||||||
android:strokeAlpha="0.44"
|
|
||||||
android:fillColor="#FFFDE7"
|
|
||||||
android:fillAlpha="0.44"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M31.237,62.393C40.162,72.832 50.324,71.527 53.707,68.887C57.097,66.24 59.774,57.143 50.887,46.875C41.572,36.12 31.027,39.188 28.589,41.513C26.152,43.838 23.047,52.815 31.237,62.393Z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:startX="55.787"
|
|
||||||
android:startY="46.379"
|
|
||||||
android:endX="33.462"
|
|
||||||
android:endY="59.774"
|
|
||||||
android:type="linear">
|
|
||||||
<item android:offset="0.024" android:color="#FF8F4700"/>
|
|
||||||
<item android:offset="1" android:color="#FF703E2D"/>
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:pathData="M61.888,66.69C58.633,63.96 56.901,64.448 54.576,65.415C51.576,66.66 46.858,67.582 40.453,65.415L42.381,60.772C46.183,62.055 48.936,61.432 51.313,60.03C54.373,58.23 58.558,55.763 65.068,61.23C67.783,63.51 70.566,65.025 72.606,64.335C74.091,63.84 74.878,61.627 75.276,59.865C75.313,59.708 75.373,59.257 75.418,58.86C75.778,56.107 76.378,50.167 80.803,47.13C85.536,43.882 90.508,43.882 90.508,43.882L91.408,52.822C89.121,52.485 87.531,52.95 86.188,53.693C81.133,56.505 85.536,67.305 77.668,70.935C70.101,74.452 63.913,68.385 61.888,66.69Z"
|
|
||||||
android:fillColor="#03A9F4"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M34.048,55.29L30.793,52.373C36.771,45.697 35.195,40.792 34.048,37.222C33.815,36.502 33.598,35.82 33.456,35.167C32.945,32.857 32.84,30.847 32.998,29.093C30.703,26.235 29.691,23.243 29.623,23.04C28.228,18.817 29.278,14.7 31.685,10.837C36.553,3 45.366,3 45.366,3L48.306,10.868C46.07,10.778 38.743,10.89 36.493,14.438C33.651,18.907 35.518,21.667 35.653,21.983C36.201,21.27 36.756,20.7 37.25,20.257C40.843,17.07 43.963,16.612 45.951,16.792C48.186,16.995 50.21,18.12 51.658,19.965C53.241,21.99 53.893,24.622 53.39,27.015C52.903,29.347 51.35,31.32 49.018,32.572C44.945,34.763 41.556,34.463 39.283,33.705C39.298,33.757 39.306,33.818 39.32,33.87C39.403,34.245 39.568,34.77 39.763,35.377C41.09,39.487 43.558,46.013 34.048,55.29ZM39.561,27.142C39.995,27.458 40.453,27.72 40.925,27.907C42.5,28.538 44.218,28.327 46.168,27.278C47.315,26.663 47.451,26.003 47.495,25.785C47.631,25.132 47.405,24.3 46.918,23.677C46.491,23.13 45.995,22.845 45.403,22.785C44.278,22.688 42.756,23.4 41.233,24.757C40.506,25.41 39.951,26.212 39.561,27.142Z"
|
|
||||||
android:fillColor="#F44336"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M47.076,56.513L42.418,56.385C42.418,56.385 44.631,43.89 51.793,41.79C53.136,41.4 54.605,41.002 56.083,40.785C56.96,40.65 58.348,40.447 59.03,40.193C59.188,39.015 58.693,37.515 58.138,35.813C57.703,34.493 57.253,33.135 57.013,31.65C56.548,28.755 57.32,26.198 59.188,24.435C61.468,22.298 65.15,21.615 69.305,22.56C71.675,23.1 73.423,24.263 74.961,25.282C77.158,26.745 78.44,27.487 81.126,25.68C84.373,23.49 80.128,14.917 77.87,9.968L86.293,6.458C87.425,8.933 92.893,21.667 89.285,28.935C88.071,31.38 85.978,33 83.233,33.607C77.263,34.943 73.768,32.618 71.218,30.923C70.01,30.12 68.953,29.49 67.805,29.16C59.833,26.888 70.963,38.618 65.751,43.89C62.623,47.048 54.98,47.88 54.486,48C49.565,49.185 47.076,56.513 47.076,56.513Z"
|
|
||||||
android:fillColor="#F48FB1"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M32.992,29.093C32.849,30.743 32.782,31.725 33.209,33.87C35.272,35.385 39.764,35.385 39.764,35.385C39.569,34.778 39.397,34.252 39.322,33.877C39.307,33.825 39.299,33.765 39.284,33.713C34.717,31.433 32.992,29.093 32.992,29.093Z"
|
|
||||||
android:fillColor="#C92B27"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M23.648,36.48L15.893,32.678L19.755,27.097L25.838,31.125L23.648,36.48Z"
|
|
||||||
android:fillColor="#FFC107"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M12.217,25.95C8.257,25.417 4.222,22.058 3.779,21.675L7.672,17.108C8.849,18.105 11.347,19.778 13.019,20.003L12.217,25.95Z"
|
|
||||||
android:fillColor="#FB8C00"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M19.206,15.953L13.506,14.085C14.158,12.09 14.331,9.937 13.993,7.86L19.918,6.907C20.406,9.922 20.158,13.05 19.206,15.953Z"
|
|
||||||
android:fillColor="#03A9F4"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M60.666,10.212L54.805,11.494L56.497,19.231L62.359,17.949L60.666,10.212Z"
|
|
||||||
android:fillColor="#FB8C00"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M69.344,13.328L65.219,8.97C67.379,6.923 67.874,4.245 67.874,4.215L73.799,5.183C73.724,5.655 72.966,9.9 69.344,13.328Z"
|
|
||||||
android:fillColor="#FFC107"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M76.877,34.798L71.637,36.436L73.426,42.163L78.667,40.525L76.877,34.798Z"
|
|
||||||
android:fillColor="#FB8C00"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M73.162,84.772L67.2,84.067C67.455,81.945 65.872,79.342 65.438,78.765L70.238,75.165C70.598,75.637 73.725,79.965 73.162,84.772Z"
|
|
||||||
android:fillColor="#F44336"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M90.276,77.168C88.034,76.83 85.739,76.695 83.474,76.777L83.271,70.777C85.904,70.688 88.566,70.838 91.169,71.235L90.276,77.168Z"
|
|
||||||
android:fillColor="#FB8C00"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M86.425,81.157L82.209,85.426L88.01,91.154L92.226,86.885L86.425,81.157Z"
|
|
||||||
android:fillColor="#F48FB1"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M74.159,52.455L69.824,47.498L64.868,51.833L69.203,56.79L74.159,52.455Z"
|
|
||||||
android:fillColor="#F44336"/>
|
|
||||||
</vector>
|
|
80
app/src/main/res/drawable/emoji_tada_large.xml
Normal file
80
app/src/main/res/drawable/emoji_tada_large.xml
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="96dp"
|
||||||
|
android:height="97dp"
|
||||||
|
android:viewportWidth="96"
|
||||||
|
android:viewportHeight="97">
|
||||||
|
<path
|
||||||
|
android:pathData="M5.586,92.751C7.289,94.596 14.301,91.379 19.836,88.799C23.984,86.871 40.581,80.061 48.831,76.529C51.059,75.576 54.299,74.331 56.631,71.264C58.701,68.534 64.191,56.939 53.136,45.194C41.916,33.269 30.359,36.561 26.039,39.621C23.496,41.421 21.426,45.479 20.496,47.541C16.566,56.256 10.964,72.224 8.684,78.501C7.011,83.129 3.899,90.921 5.586,92.751Z"
|
||||||
|
android:fillColor="#FFC107"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M19.386,50.166C19.491,51.471 19.753,53.594 20.653,57.681C21.268,60.486 22.273,63.426 23.091,65.369C25.543,71.211 28.986,73.566 32.473,75.456C38.398,78.666 42.426,79.266 42.426,79.266L37.596,81.239C37.596,81.239 34.671,80.631 30.681,78.666C26.878,76.791 22.918,73.619 20.023,67.409C18.771,64.716 18.043,62.106 17.623,60.036C17.106,57.471 17.023,56.016 17.023,56.016L19.386,50.166Z"
|
||||||
|
android:fillColor="#FF8F00"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M13.453,65.376C13.453,65.376 14.053,70.244 18.073,76.386C22.783,83.571 29.36,84.749 29.36,84.749L24.988,86.549C24.988,86.549 20.105,85.056 15.463,78.719C12.568,74.766 11.758,70.041 11.758,70.041L13.453,65.376Z"
|
||||||
|
android:fillColor="#FF8F00"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M9.291,76.911C9.291,76.911 10.386,81.111 12.786,84.246C15.644,87.989 19.281,89.076 19.281,89.076L15.929,90.561C15.929,90.561 13.386,90.029 10.604,86.504C8.489,83.826 7.889,80.751 7.889,80.751L9.291,76.911Z"
|
||||||
|
android:fillColor="#FF8F00"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M7.469,87.576C7.319,87.239 7.319,86.856 7.477,86.526L26.579,46.911L29.722,58.724L9.622,87.756C9.082,88.566 7.866,88.461 7.469,87.576Z"
|
||||||
|
android:strokeAlpha="0.44"
|
||||||
|
android:fillColor="#FFFDE7"
|
||||||
|
android:fillAlpha="0.44"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M31.237,62.691C40.162,73.131 50.324,71.826 53.707,69.186C57.097,66.539 59.774,57.441 50.887,47.174C41.572,36.419 31.027,39.486 28.589,41.811C26.152,44.136 23.047,53.114 31.237,62.691Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="55.787"
|
||||||
|
android:startY="46.678"
|
||||||
|
android:endX="33.462"
|
||||||
|
android:endY="60.073"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.024" android:color="#FF8F4700"/>
|
||||||
|
<item android:offset="1" android:color="#FF703E2D"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M61.888,66.989C58.633,64.259 56.901,64.746 54.576,65.714C51.576,66.959 46.858,67.881 40.453,65.714L42.381,61.071C46.183,62.354 48.936,61.731 51.313,60.329C54.373,58.529 58.558,56.061 65.068,61.529C67.783,63.809 70.566,65.324 72.606,64.634C74.091,64.139 74.878,61.926 75.276,60.164C75.313,60.006 75.373,59.556 75.418,59.159C75.778,56.406 76.378,50.466 80.803,47.429C85.536,44.181 90.508,44.181 90.508,44.181L91.408,53.121C89.121,52.784 87.531,53.249 86.188,53.991C81.133,56.804 85.536,67.604 77.668,71.234C70.101,74.751 63.913,68.684 61.888,66.989Z"
|
||||||
|
android:fillColor="#03A9F4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M34.048,55.589L30.793,52.671C36.771,45.996 35.195,41.091 34.048,37.521C33.815,36.801 33.598,36.119 33.456,35.466C32.945,33.156 32.84,31.146 32.998,29.391C30.703,26.534 29.691,23.541 29.623,23.339C28.228,19.116 29.278,14.999 31.685,11.136C36.553,3.299 45.366,3.299 45.366,3.299L48.306,11.166C46.07,11.076 38.743,11.189 36.493,14.736C33.651,19.206 35.518,21.966 35.653,22.281C36.201,21.569 36.756,20.999 37.25,20.556C40.843,17.369 43.963,16.911 45.951,17.091C48.186,17.294 50.21,18.419 51.658,20.264C53.241,22.289 53.893,24.921 53.39,27.314C52.903,29.646 51.35,31.619 49.018,32.871C44.945,35.061 41.556,34.761 39.283,34.004C39.298,34.056 39.306,34.116 39.32,34.169C39.403,34.544 39.568,35.069 39.763,35.676C41.09,39.786 43.558,46.311 34.048,55.589ZM39.561,27.441C39.995,27.756 40.453,28.019 40.925,28.206C42.5,28.836 44.218,28.626 46.168,27.576C47.315,26.961 47.451,26.301 47.495,26.084C47.631,25.431 47.405,24.599 46.918,23.976C46.491,23.429 45.995,23.144 45.403,23.084C44.278,22.986 42.756,23.699 41.233,25.056C40.506,25.709 39.951,26.511 39.561,27.441Z"
|
||||||
|
android:fillColor="#F44336"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M47.076,56.811L42.418,56.684C42.418,56.684 44.631,44.189 51.793,42.089C53.136,41.699 54.605,41.301 56.083,41.084C56.96,40.949 58.348,40.746 59.03,40.491C59.188,39.314 58.693,37.814 58.138,36.111C57.703,34.791 57.253,33.434 57.013,31.949C56.548,29.054 57.32,26.496 59.188,24.734C61.468,22.596 65.15,21.914 69.305,22.859C71.675,23.399 73.423,24.561 74.961,25.581C77.158,27.044 78.44,27.786 81.126,25.979C84.373,23.789 80.128,15.216 77.87,10.266L86.293,6.756C87.425,9.231 92.893,21.966 89.285,29.234C88.071,31.679 85.978,33.299 83.233,33.906C77.263,35.241 73.768,32.916 71.218,31.221C70.01,30.419 68.953,29.789 67.805,29.459C59.833,27.186 70.963,38.916 65.751,44.189C62.623,47.346 54.98,48.179 54.486,48.299C49.565,49.484 47.076,56.811 47.076,56.811Z"
|
||||||
|
android:fillColor="#F48FB1"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M32.992,29.391C32.849,31.041 32.782,32.024 33.209,34.169C35.272,35.684 39.764,35.684 39.764,35.684C39.569,35.076 39.397,34.551 39.322,34.176C39.307,34.124 39.299,34.064 39.284,34.011C34.717,31.731 32.992,29.391 32.992,29.391Z"
|
||||||
|
android:fillColor="#C92B27"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M23.648,36.779L15.893,32.976L19.755,27.396L25.838,31.424L23.648,36.779Z"
|
||||||
|
android:fillColor="#FFC107"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.217,26.249C8.257,25.716 4.222,22.356 3.779,21.974L7.672,17.406C8.849,18.404 11.347,20.076 13.019,20.301L12.217,26.249Z"
|
||||||
|
android:fillColor="#FB8C00"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M19.206,16.251L13.506,14.384C14.158,12.389 14.331,10.236 13.993,8.159L19.918,7.206C20.406,10.221 20.158,13.349 19.206,16.251Z"
|
||||||
|
android:fillColor="#03A9F4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M60.666,10.51L54.805,11.792L56.497,19.53L62.359,18.247L60.666,10.51Z"
|
||||||
|
android:fillColor="#FB8C00"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M69.344,13.626L65.219,9.269C67.379,7.221 67.874,4.544 67.874,4.514L73.799,5.481C73.724,5.954 72.966,10.199 69.344,13.626Z"
|
||||||
|
android:fillColor="#FFC107"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M76.877,35.097L71.637,36.735L73.426,42.462L78.667,40.824L76.877,35.097Z"
|
||||||
|
android:fillColor="#FB8C00"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M73.162,85.071L67.2,84.366C67.455,82.244 65.872,79.641 65.438,79.064L70.238,75.464C70.598,75.936 73.725,80.264 73.162,85.071Z"
|
||||||
|
android:fillColor="#F44336"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M90.276,77.466C88.034,77.129 85.739,76.994 83.474,77.076L83.271,71.076C85.904,70.986 88.566,71.136 91.169,71.534L90.276,77.466Z"
|
||||||
|
android:fillColor="#FB8C00"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M86.425,81.456L82.209,85.725L88.01,91.453L92.226,87.184L86.425,81.456Z"
|
||||||
|
android:fillColor="#F48FB1"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M74.159,52.754L69.824,47.797L64.868,52.132L69.203,57.089L74.159,52.754Z"
|
||||||
|
android:fillColor="#F44336"/>
|
||||||
|
</vector>
|
30
app/src/main/res/drawable/ic_logo_large.xml
Normal file
30
app/src/main/res/drawable/ic_logo_large.xml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="162dp"
|
||||||
|
android:height="146dp"
|
||||||
|
android:viewportWidth="162"
|
||||||
|
android:viewportHeight="146">
|
||||||
|
<path
|
||||||
|
android:pathData="M0.22,138.16H5.57C5.55,139.54 7.31,140.98 11.56,141.04C15.5,141.06 18.54,140.54 18.54,138.71C18.54,134.7 0.67,139.37 0.67,130.55C0.67,126.34 5.05,123.78 11.56,123.78C18.29,123.78 22.9,126.61 22.94,130.97H17.56C17.58,129.55 16.06,128.11 11.93,128.09C8.37,128.05 5.78,128.59 5.78,130.34C5.78,134.2 23.63,129.93 23.63,138.56C23.63,142.79 18.94,145.36 12,145.36C4.82,145.36 0.13,142.5 0.22,138.16Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M26.74,124.28H46.82V128.57H31.8V132.41H46.55V136.43H31.8V140.58H46.82V144.88H26.74V124.28Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M49.65,138.16H55.01C54.99,139.54 56.74,140.98 60.99,141.04C64.94,141.06 67.98,140.54 67.98,138.71C67.98,134.7 50.11,139.37 50.11,130.55C50.11,126.34 54.49,123.78 60.99,123.78C67.73,123.78 72.34,126.61 72.38,130.97H66.98C67,129.55 65.48,128.11 61.35,128.09C57.8,128.05 55.2,128.59 55.2,130.34C55.2,134.2 73.05,129.93 73.05,138.56C73.05,142.79 68.35,145.36 61.41,145.36C54.24,145.36 49.55,142.5 49.65,138.16Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M74.92,138.16H80.28C80.26,139.54 82.01,140.98 86.27,141.04C90.21,141.06 93.25,140.54 93.25,138.71C93.25,134.7 75.38,139.37 75.38,130.55C75.38,126.34 79.76,123.78 86.27,123.78C93,123.78 97.61,126.61 97.65,130.97H92.27C92.29,129.55 90.77,128.11 86.64,128.09C83.08,128.05 80.49,128.59 80.49,130.34C80.49,134.2 98.34,129.93 98.34,138.56C98.34,142.79 93.65,145.36 86.7,145.36C79.53,145.36 74.84,142.5 74.92,138.16Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M101.44,124.28H106.51V144.88H101.44V124.28Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M109.24,134.58C109.24,128.09 114.22,123.8 122.25,123.8C130.32,123.8 135.28,128.09 135.28,134.58C135.28,141.06 130.32,145.36 122.25,145.36C114.22,145.36 109.24,141.06 109.24,134.58ZM130.24,134.58C130.24,130.41 127.21,128.09 122.25,128.09C117.33,128.09 114.31,130.39 114.31,134.58C114.31,138.77 117.33,141.06 122.25,141.06C127.21,141.06 130.24,138.73 130.24,134.58Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M138.04,124.28H142.27L156.72,137.37V124.28H161.79V144.88H157.51L143.08,131.78V144.88H138.02V124.28H138.04Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M112.78,52.82L93.41,42.1H106.57C118.01,42.1 127.29,32.82 127.29,21.37C127.29,9.92 118.01,0.65 106.57,0.65H60.67C46.32,0.65 34.71,12.26 34.71,26.61C34.71,36.81 40.26,46.24 49.17,51.16L68.54,61.88H55.43C43.99,61.88 34.71,71.16 34.71,82.61C34.71,94.06 43.99,103.33 55.43,103.33H101.28C115.63,103.33 127.24,91.72 127.24,77.37C127.24,67.17 121.69,57.79 112.78,52.82ZM52.27,45.57C45.54,41.84 41.29,34.79 41.14,27.12C40.88,16.09 50.15,7.07 61.18,7.07H106.15C113.82,7.07 120.45,12.98 120.86,20.65C121.28,28.52 115.17,35.26 107.29,35.67C107.08,35.67 106.82,35.67 106.62,35.67H80.72C79.11,35.67 77.87,36.97 77.87,38.52V59.66L52.27,45.57ZM100.82,96.96H55.85C48.18,96.96 41.55,91.05 41.14,83.38C40.72,75.51 46.83,68.78 54.71,68.36C54.97,68.36 55.18,68.36 55.43,68.36H81.34C82.94,68.36 84.19,67.07 84.19,65.51V44.32L109.68,58.41C116.41,62.14 120.66,69.14 120.81,76.86C121.07,87.89 111.85,96.91 100.82,96.96Z"
|
||||||
|
android:fillColor="#00F782"/>
|
||||||
|
</vector>
|
@ -23,6 +23,10 @@
|
|||||||
|
|
||||||
</androidx.appcompat.widget.Toolbar>
|
</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
|
<org.thoughtcrime.securesms.conversation.v2.ConversationRecyclerView
|
||||||
android:focusable="false"
|
android:focusable="false"
|
||||||
android:id="@+id/conversationRecyclerView"
|
android:id="@+id/conversationRecyclerView"
|
||||||
@ -31,6 +35,7 @@
|
|||||||
android:layout_above="@+id/typingIndicatorViewContainer"
|
android:layout_above="@+id/typingIndicatorViewContainer"
|
||||||
android:layout_below="@id/toolbar" />
|
android:layout_below="@id/toolbar" />
|
||||||
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer
|
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer
|
||||||
android:focusable="false"
|
android:focusable="false"
|
||||||
android:id="@+id/typingIndicatorViewContainer"
|
android:id="@+id/typingIndicatorViewContainer"
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
android:id="@+id/btnCancelGroupNameEdit"
|
android:id="@+id/btnCancelGroupNameEdit"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginLeft="@dimen/medium_spacing"
|
||||||
android:contentDescription="@string/AccessibilityId_cancel_name_change"
|
android:contentDescription="@string/AccessibilityId_cancel_name_change"
|
||||||
android:src="@drawable/ic_baseline_clear_24"/>
|
android:src="@drawable/ic_baseline_clear_24"/>
|
||||||
|
|
||||||
@ -49,6 +50,7 @@
|
|||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionDone"
|
||||||
|
android:maxLength="@integer/max_group_and_community_name_length_chars"
|
||||||
android:contentDescription="@string/AccessibilityId_group_name"
|
android:contentDescription="@string/AccessibilityId_group_name"
|
||||||
android:hint="@string/activity_edit_closed_group_edit_text_hint" />
|
android:hint="@string/activity_edit_closed_group_edit_text_hint" />
|
||||||
|
|
||||||
@ -57,6 +59,7 @@
|
|||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginRight="@dimen/medium_spacing"
|
||||||
android:contentDescription="@string/AccessibilityId_accept_name_change"
|
android:contentDescription="@string/AccessibilityId_accept_name_change"
|
||||||
android:src="@drawable/ic_baseline_done_24"/>
|
android:src="@drawable/ic_baseline_done_24"/>
|
||||||
|
|
||||||
|
@ -47,7 +47,11 @@
|
|||||||
android:paddingTop="12dp"
|
android:paddingTop="12dp"
|
||||||
android:paddingBottom="12dp"
|
android:paddingBottom="12dp"
|
||||||
android:visibility="invisible"
|
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
|
<TextView
|
||||||
android:id="@+id/btnGroupNameDisplay"
|
android:id="@+id/btnGroupNameDisplay"
|
||||||
@ -55,8 +59,10 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:contentDescription="@string/AccessibilityId_username"
|
android:contentDescription="@string/AccessibilityId_username"
|
||||||
|
android:gravity="center"
|
||||||
android:textColor="?android:textColorPrimary"
|
android:textColor="?android:textColorPrimary"
|
||||||
android:textSize="@dimen/very_large_font_size"
|
android:textSize="@dimen/very_large_font_size"
|
||||||
|
android:maxLength="@integer/max_user_nickname_length_chars"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@ -80,7 +86,7 @@
|
|||||||
android:textColor="?android:textColorPrimary"
|
android:textColor="?android:textColorPrimary"
|
||||||
android:fontFamily="@font/space_mono_regular"
|
android:fontFamily="@font/space_mono_regular"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:contentDescription="@string/AccessibilityId_session_id"
|
android:contentDescription="@string/AccessibilityId_account_id"
|
||||||
tools:text="05987d601943c267879be41830888066c6a024cbdc9a548d06813924bf3372ea78" />
|
tools:text="05987d601943c267879be41830888066c6a024cbdc9a548d06813924bf3372ea78" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -374,7 +380,7 @@
|
|||||||
android:paddingHorizontal="@dimen/large_spacing"
|
android:paddingHorizontal="@dimen/large_spacing"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/setting_button_height"
|
android:layout_height="@dimen/setting_button_height"
|
||||||
android:contentDescription="@string/AccessibilityId_recovery_password">
|
android:contentDescription="@string/AccessibilityId_recovery_password_menu_item">
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/passwordContainer"
|
android:id="@+id/passwordContainer"
|
||||||
android:layout_width="@dimen/small_profile_picture_size"
|
android:layout_width="@dimen/small_profile_picture_size"
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/export_logs_button"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
style="@style/Widget.Session.Button.Common.Filled"
|
style="@style/Widget.Session.Button.Common.Filled"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
@ -12,4 +13,5 @@
|
|||||||
android:paddingVertical="12dp"
|
android:paddingVertical="12dp"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
@ -62,10 +62,14 @@
|
|||||||
android:layout_marginBottom="@dimen/medium_spacing"
|
android:layout_marginBottom="@dimen/medium_spacing"
|
||||||
android:contentDescription="@string/AccessibilityId_group_name_input"
|
android:contentDescription="@string/AccessibilityId_group_name_input"
|
||||||
android:hint="@string/activity_create_closed_group_edit_text_hint"
|
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_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="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
|
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||||
android:id="@+id/contactSearch"
|
android:id="@+id/contactSearch"
|
||||||
|
@ -29,6 +29,8 @@
|
|||||||
android:id="@+id/nameTextViewContainer"
|
android:id="@+id/nameTextViewContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="@dimen/medium_spacing"
|
||||||
|
android:paddingEnd="@dimen/medium_spacing"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
@ -42,6 +44,7 @@
|
|||||||
android:id="@+id/nameTextView"
|
android:id="@+id/nameTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginStart="@dimen/small_spacing"
|
android:layout_marginStart="@dimen/small_spacing"
|
||||||
android:layout_marginEnd="@dimen/small_spacing"
|
android:layout_marginEnd="@dimen/small_spacing"
|
||||||
@ -57,6 +60,7 @@
|
|||||||
android:layout_height="22dp"
|
android:layout_height="22dp"
|
||||||
android:contentDescription="@string/AccessibilityId_edit_user_nickname"
|
android:contentDescription="@string/AccessibilityId_edit_user_nickname"
|
||||||
android:paddingTop="2dp"
|
android:paddingTop="2dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
android:src="@drawable/ic_baseline_edit_24" />
|
android:src="@drawable/ic_baseline_edit_24" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@ -73,6 +77,7 @@
|
|||||||
android:id="@+id/cancelNicknameEditingButton"
|
android:id="@+id/cancelNicknameEditingButton"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginLeft="@dimen/large_spacing"
|
||||||
android:contentDescription="@string/AccessibilityId_cancel"
|
android:contentDescription="@string/AccessibilityId_cancel"
|
||||||
android:src="@drawable/ic_baseline_clear_24" />
|
android:src="@drawable/ic_baseline_clear_24" />
|
||||||
|
|
||||||
@ -82,12 +87,12 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginHorizontal="@dimen/small_spacing"
|
|
||||||
android:contentDescription="@string/AccessibilityId_username"
|
android:contentDescription="@string/AccessibilityId_username"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:paddingVertical="12dp"
|
android:paddingVertical="12dp"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:singleLine="true"
|
android:maxLength="@integer/max_user_nickname_length_chars"
|
||||||
|
android:maxLines="1"
|
||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionDone"
|
||||||
android:textColorHint="?android:textColorSecondary"
|
android:textColorHint="?android:textColorSecondary"
|
||||||
android:hint="@string/fragment_user_details_bottom_sheet_edit_text_hint" />
|
android:hint="@string/fragment_user_details_bottom_sheet_edit_text_hint" />
|
||||||
@ -96,6 +101,7 @@
|
|||||||
android:id="@+id/saveNicknameButton"
|
android:id="@+id/saveNicknameButton"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginRight="@dimen/large_spacing"
|
||||||
android:contentDescription="@string/AccessibilityId_apply"
|
android:contentDescription="@string/AccessibilityId_apply"
|
||||||
android:src="@drawable/ic_baseline_done_24" />
|
android:src="@drawable/ic_baseline_done_24" />
|
||||||
|
|
||||||
|
@ -1,23 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/container"
|
android:id="@+id/export_progress_container"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="16dp"
|
android:layout_gravity="bottom" >
|
||||||
android:gravity="bottom">
|
|
||||||
|
|
||||||
<ProgressBar android:id="@+id/progress_bar"
|
<ProgressBar
|
||||||
|
android:id="@+id/export_progress_bar"
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:indeterminate="true"/>
|
android:indeterminate="true"
|
||||||
|
android:visibility="invisible" />
|
||||||
<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"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
@ -15,7 +15,7 @@
|
|||||||
android:layout_width="17dp"
|
android:layout_width="17dp"
|
||||||
android:layout_height="17dp" />
|
android:layout_height="17dp" />
|
||||||
|
|
||||||
<Space
|
<View
|
||||||
android:id="@+id/reactions_pill_spacer"
|
android:id="@+id/reactions_pill_spacer"
|
||||||
android:layout_width="4dp"
|
android:layout_width="4dp"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/medium_spacing"
|
android:layout_marginStart="@dimen/medium_spacing"
|
||||||
|
android:maxLength="@integer/max_user_nickname_length_chars"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
|
@ -165,7 +165,7 @@
|
|||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textColor="?android:textColorPrimary"
|
android:textColor="?android:textColorPrimary"
|
||||||
android:textSize="@dimen/medium_font_size"
|
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"
|
<include layout="@layout/view_typing_indicator"
|
||||||
android:id="@+id/typingIndicatorView"
|
android:id="@+id/typingIndicatorView"
|
||||||
|
@ -6,4 +6,7 @@
|
|||||||
<integer name="reaction_scrubber_reveal_offset">100</integer>
|
<integer name="reaction_scrubber_reveal_offset">100</integer>
|
||||||
<integer name="reaction_scrubber_hide_duration">150</integer>
|
<integer name="reaction_scrubber_hide_duration">150</integer>
|
||||||
<integer name="reaction_scrubber_emoji_reveal_duration_start_delay_factor">10</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>
|
</resources>
|
@ -20,6 +20,7 @@
|
|||||||
<string name="AccessibilityId_link_device">Link Device</string>
|
<string name="AccessibilityId_link_device">Link Device</string>
|
||||||
<!-- Session ID Page-->
|
<!-- Session ID Page-->
|
||||||
<string name="AccessibilityId_session_id">Session ID</string>
|
<string name="AccessibilityId_session_id">Session ID</string>
|
||||||
|
<string name="AccessibilityId_account_id">Account ID</string>
|
||||||
<string name="AccessibilityId_continue">Continue</string>
|
<string name="AccessibilityId_continue">Continue</string>
|
||||||
<!-- Recovery phrase input -->
|
<!-- Recovery phrase input -->
|
||||||
|
|
||||||
@ -132,7 +133,7 @@
|
|||||||
<string name="AccessibilityId_user_settings">User settings</string>
|
<string name="AccessibilityId_user_settings">User settings</string>
|
||||||
<string name="AccessibilityId_username">Username</string>
|
<string name="AccessibilityId_username">Username</string>
|
||||||
<string name="AccessibilityId_privacy">Privacy</string>
|
<string name="AccessibilityId_privacy">Privacy</string>
|
||||||
<string name="AccessibilityId_recovery_password">Show recovery password</string>
|
<string name="AccessibilityId_recovery_password_menu_item">Recovery password menu item</string>
|
||||||
<string name="AccessibilityId_edit_user_nickname">Edit user nickname</string>
|
<string name="AccessibilityId_edit_user_nickname">Edit user nickname</string>
|
||||||
<string name="AccessibilityId_apply">Apply</string>
|
<string name="AccessibilityId_apply">Apply</string>
|
||||||
<string name="AccessibilityId_cancel">Cancel</string>
|
<string name="AccessibilityId_cancel">Cancel</string>
|
||||||
@ -1059,6 +1060,7 @@
|
|||||||
|
|
||||||
<string name="onboardingBubblePrivacyInYourPocket">Privacy in your pocket.</string>
|
<string name="onboardingBubblePrivacyInYourPocket">Privacy in your pocket.</string>
|
||||||
<string name="onboardingBubbleWelcomeToSession">Welcome to Session 👋</string>
|
<string name="onboardingBubbleWelcomeToSession">Welcome to Session 👋</string>
|
||||||
|
<string name="welcome_to_session">Welcome to Session</string>
|
||||||
<string name="onboardingBubbleSessionIsEngineered">Session is engineered to protect your privacy.</string>
|
<string name="onboardingBubbleSessionIsEngineered">Session is engineered to protect your privacy.</string>
|
||||||
<string name="onboardingBubbleNoPhoneNumber">"You don’t even need a phone number to sign up. "</string>
|
<string name="onboardingBubbleNoPhoneNumber">"You don’t even need a phone number to sign up. "</string>
|
||||||
<string name="onboardingBubbleCreatingAnAccountIsEasy">Creating an account is \ninstant, free, and \nanonymous 👇</string>
|
<string name="onboardingBubbleCreatingAnAccountIsEasy">Creating an account is \ninstant, free, and \nanonymous 👇</string>
|
||||||
@ -1081,6 +1083,38 @@
|
|||||||
<string name="activity_link_load_account">Load Account</string>
|
<string name="activity_link_load_account">Load Account</string>
|
||||||
<string name="activity_link_camera_permission_permanently_denied_configure_in_settings">Camera Permission permanently denied. Configure in settings.</string>
|
<string name="activity_link_camera_permission_permanently_denied_configure_in_settings">Camera Permission permanently denied. Configure in settings.</string>
|
||||||
<string name="activity_link_enter_your_recovery_password_to_load_your_account_if_you_haven_t_saved_it_you_can_find_it_in_your_app_settings">Enter your recovery password to load your account. If you haven\'t saved it, you can find it in your app settings.</string>
|
<string name="activity_link_enter_your_recovery_password_to_load_your_account_if_you_haven_t_saved_it_you_can_find_it_in_your_app_settings">Enter your recovery password to load your account. If you haven\'t saved it, you can find it in your app settings.</string>
|
||||||
|
<string name="waitOneMoment">One moment please..</string>
|
||||||
|
<string name="loadAccountProgressMessage">Loading your account</string>
|
||||||
|
<string name="notificationsMessage">Message notifications</string>
|
||||||
|
<string name="onboardingMessageNotificationExplaination">There are two ways Session can notify you of new messages.</string>
|
||||||
|
<string name="recoveryPasswordHidePermanently">Hide Recovery Password Permanently</string>
|
||||||
|
<string name="recoveryPasswordHidePermanentlyDescription1"><![CDATA[Without your recovery password, you cannot load your account on new devices. <br /><br />We strongly recommend you save your recovery password in a safe and secure place before continuing.]]></string>
|
||||||
|
<string name="recoveryPasswordHidePermanentlyDescription2">Are you sure you want to permanently hide your recovery password on this device? This cannot be undone.</string>
|
||||||
|
<string name="recoveryPasswordDescription">Use your recovery password to load your account on new devices. Your account cannot be recovered without your recovery password. Make sure it\'s stored somewhere safe and secure — and don\'t share it with anyone.</string>
|
||||||
|
<string name="hide">Hide</string>
|
||||||
|
<string name="recoveryPasswordHideRecoveryPassword">Hide Recovery Password</string>
|
||||||
|
<string name="qrView">View QR</string>
|
||||||
|
<string name="recoveryPasswordView">View Password</string>
|
||||||
|
<string name="recoveryPasswordHideRecoveryPasswordDescription">Permanently hide your recovery password on this device.</string>
|
||||||
|
<string name="conversationsNone">You don\'t have any conversations yet</string>
|
||||||
|
<string name="onboardingHitThePlusButton">Hit the plus button to start a chat, create a group, or join an official communitiy!</string>
|
||||||
|
<string name="admin_group_leave_warning">Because you are the creator of this group it will be deleted for everyone. This cannot be undone.</string>
|
||||||
|
<string name="hide_message_requests">Hide message requests?</string>
|
||||||
|
<string name="save_your_recovery_password">Save your recovery password</string>
|
||||||
|
<string name="save_your_recovery_password_to_make_sure_you_don_t_lose_access_to_your_account">Save your recovery password to make sure you don\'t lose access to your account.</string>
|
||||||
|
<string name="onboardingAccountCreated">Account Created</string>
|
||||||
|
<string name="AccessibilityId_fast_mode_notifications_button">Fast mode notifications button</string>
|
||||||
|
<string name="AccessibilityId_slow_mode_notifications_button">Slow mode notifications button</string>
|
||||||
|
<string name="AccessibilityId_reveal_recovery_phrase_button">Reveal recovery phrase button</string>
|
||||||
|
<string name="AccessibilityId_create_account_button">Create account button</string>
|
||||||
|
<string name="AccessibilityId_restore_account_button">Restore your session button</string>
|
||||||
|
<string name="AccessibilityId_privacy_policy_link">Privacy policy link</string>
|
||||||
|
<string name="AccessibilityId_terms_of_service_link">Terms of service link</string>
|
||||||
|
<string name="AccessibilityId_loading_animation">Loading animation</string>
|
||||||
|
<string name="AccessibilityId_recovery_phrase_input">Recovery phrase input</string>
|
||||||
|
<string name="AccessibilityId_error_message">Error message</string>
|
||||||
|
<string name="AccessibilityId_hide_recovery_password_button">Hide recovery password button</string>
|
||||||
|
<string name="AccessibilityId_confirm_button">Confirm button</string>
|
||||||
<string name="accountIdErrorInvalid">This Account ID is invalid. Please check and try again.</string>
|
<string name="accountIdErrorInvalid">This Account ID is invalid. Please check and try again.</string>
|
||||||
<string name="enter_account_id">Enter Account ID</string>
|
<string name="enter_account_id">Enter Account ID</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
<PreferenceCategory android:title="@string/preferences__link_previews">
|
<PreferenceCategory android:title="@string/preferences__link_previews">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||||
android:defaultValue="true"
|
android:defaultValue="false"
|
||||||
android:key="pref_link_previews"
|
android:key="pref_link_previews"
|
||||||
android:summary="@string/preferences__link_previews_summary"
|
android:summary="@string/preferences__link_previews_summary"
|
||||||
android:title="@string/preferences__send_link_previews"/>
|
android:title="@string/preferences__send_link_previews"/>
|
||||||
|
@ -7,38 +7,37 @@
|
|||||||
android:title="@string/activity_help_settings__report_bug_title"
|
android:title="@string/activity_help_settings__report_bug_title"
|
||||||
android:summary="@string/activity_help_settings__report_bug_summary"
|
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>
|
||||||
|
|
||||||
<PreferenceCategory>
|
<PreferenceCategory>
|
||||||
<Preference
|
<Preference
|
||||||
android:key="translate_session"
|
android:key="translate_session"
|
||||||
android:title="@string/activity_help_settings__translate_session"
|
android:title="@string/activity_help_settings__translate_session"
|
||||||
android:widgetLayout="@layout/preference_external_link"
|
android:widgetLayout="@layout/preference_external_link" />
|
||||||
/>
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory>
|
<PreferenceCategory>
|
||||||
<Preference
|
<Preference
|
||||||
android:key="feedback"
|
android:key="feedback"
|
||||||
android:title="@string/activity_help_settings__feedback"
|
android:title="@string/activity_help_settings__feedback"
|
||||||
android:widgetLayout="@layout/preference_external_link"
|
android:widgetLayout="@layout/preference_external_link" />
|
||||||
/>
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory>
|
<PreferenceCategory>
|
||||||
<Preference
|
<Preference
|
||||||
android:key="faq"
|
android:key="faq"
|
||||||
android:title="@string/activity_help_settings__faq"
|
android:title="@string/activity_help_settings__faq"
|
||||||
android:widgetLayout="@layout/preference_external_link"
|
android:widgetLayout="@layout/preference_external_link" />
|
||||||
/>
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory>
|
<PreferenceCategory>
|
||||||
<Preference
|
<Preference
|
||||||
android:key="support"
|
android:key="support"
|
||||||
android:title="@string/activity_help_settings__support"
|
android:title="@string/activity_help_settings__support"
|
||||||
android:widgetLayout="@layout/preference_external_link"
|
android:widgetLayout="@layout/preference_external_link" />
|
||||||
/>
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</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
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import org.junit.BeforeClass
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
open class BaseViewModelTest: BaseCoroutineTest() {
|
open class BaseViewModelTest: BaseCoroutineTest() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@BeforeClass
|
||||||
|
@JvmStatic
|
||||||
|
fun setupLogger() {
|
||||||
|
Log.initialize(NoOpLogger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var instantExecutorRule = InstantTaskExecutorRule()
|
var instantExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ import kotlin.time.Duration.Companion.minutes
|
|||||||
private const val THREAD_ID = 1L
|
private const val THREAD_ID = 1L
|
||||||
private const val LOCAL_NUMBER = "05---local---address"
|
private const val LOCAL_NUMBER = "05---local---address"
|
||||||
private val LOCAL_ADDRESS = Address.fromSerialized(LOCAL_NUMBER)
|
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)
|
private val GROUP_ADDRESS = Address.fromSerialized(GROUP_NUMBER)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@ -3,12 +3,14 @@ package org.thoughtcrime.securesms.conversation.v2
|
|||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.hamcrest.CoreMatchers.endsWith
|
import org.hamcrest.CoreMatchers.endsWith
|
||||||
import org.hamcrest.CoreMatchers.equalTo
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
import org.hamcrest.CoreMatchers.notNullValue
|
import org.hamcrest.CoreMatchers.notNullValue
|
||||||
import org.hamcrest.CoreMatchers.nullValue
|
import org.hamcrest.CoreMatchers.nullValue
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
import org.junit.BeforeClass
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.mockito.Mockito.anyLong
|
import org.mockito.Mockito.anyLong
|
||||||
@ -18,7 +20,9 @@ import org.mockito.kotlin.any
|
|||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.BaseViewModelTest
|
import org.thoughtcrime.securesms.BaseViewModelTest
|
||||||
|
import org.thoughtcrime.securesms.NoOpLogger
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
@ -32,6 +36,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
private val threadId = 123L
|
private val threadId = 123L
|
||||||
private val edKeyPair = mock<KeyPair>()
|
private val edKeyPair = mock<KeyPair>()
|
||||||
private lateinit var recipient: Recipient
|
private lateinit var recipient: Recipient
|
||||||
|
private lateinit var messageRecord: MessageRecord
|
||||||
|
|
||||||
private val viewModel: ConversationViewModel by lazy {
|
private val viewModel: ConversationViewModel by lazy {
|
||||||
ConversationViewModel(threadId, edKeyPair, repository, storage)
|
ConversationViewModel(threadId, edKeyPair, repository, storage)
|
||||||
@ -40,6 +45,9 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
recipient = mock()
|
recipient = mock()
|
||||||
|
messageRecord = mock { record ->
|
||||||
|
whenever(record.individualRecipient).thenReturn(recipient)
|
||||||
|
}
|
||||||
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
|
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
|
||||||
whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow())
|
whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow())
|
||||||
}
|
}
|
||||||
@ -144,7 +152,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
val error = Throwable()
|
val error = Throwable()
|
||||||
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Failure(error))
|
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Failure(error))
|
||||||
|
|
||||||
viewModel.banAndDeleteAll(recipient)
|
viewModel.banAndDeleteAll(messageRecord)
|
||||||
|
|
||||||
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
|
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 {
|
fun `should emit a message on ban user and delete all success`() = runBlockingTest {
|
||||||
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Success(Unit))
|
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Success(Unit))
|
||||||
|
|
||||||
viewModel.banAndDeleteAll(recipient)
|
viewModel.banAndDeleteAll(messageRecord)
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
viewModel.uiState.first().uiMessages.first().message,
|
viewModel.uiState.first().uiMessages.first().message,
|
||||||
@ -189,7 +197,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `open group recipient should have no blinded recipient`() {
|
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.isOpenGroupOutboxRecipient).thenReturn(false)
|
||||||
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false)
|
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false)
|
||||||
assertThat(viewModel.blindedRecipient, nullValue())
|
assertThat(viewModel.blindedRecipient, nullValue())
|
||||||
|
@ -16,7 +16,7 @@ android.enableJetifier=true
|
|||||||
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||||
org.gradle.unsafe.configuration-cache=true
|
org.gradle.unsafe.configuration-cache=true
|
||||||
|
|
||||||
gradlePluginVersion=7.3.1
|
gradlePluginVersion=7.4.2
|
||||||
googleServicesVersion=4.3.12
|
googleServicesVersion=4.3.12
|
||||||
kotlinVersion=1.8.21
|
kotlinVersion=1.8.21
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
@ -8,9 +8,11 @@ import androidx.annotation.Nullable;
|
|||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -22,7 +24,7 @@ public class AvatarHelper {
|
|||||||
private static final String AVATAR_DIRECTORY = "avatars";
|
private static final String AVATAR_DIRECTORY = "avatars";
|
||||||
|
|
||||||
public static InputStream getInputStreamFor(@NonNull Context context, @NonNull Address address)
|
public static InputStream getInputStreamFor(@NonNull Context context, @NonNull Address address)
|
||||||
throws IOException
|
throws FileNotFoundException
|
||||||
{
|
{
|
||||||
return new FileInputStream(getAvatarFile(context, address));
|
return new FileInputStream(getAvatarFile(context, address));
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
@ -24,7 +25,7 @@ public class ProfileContactPhoto implements ContactPhoto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream openInputStream(Context context) throws IOException {
|
public InputStream openInputStream(Context context) throws FileNotFoundException {
|
||||||
return AvatarHelper.getInputStreamFor(context, address);
|
return AvatarHelper.getInputStreamFor(context, address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import android.graphics.drawable.LayerDrawable;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
|
||||||
import com.amulyakhare.textdrawable.TextDrawable;
|
import com.amulyakhare.textdrawable.TextDrawable;
|
||||||
import com.makeramen.roundedimageview.RoundedDrawable;
|
import com.makeramen.roundedimageview.RoundedDrawable;
|
||||||
@ -31,7 +32,7 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
|||||||
@Override
|
@Override
|
||||||
public Drawable asDrawable(Context context, int color, boolean inverted) {
|
public Drawable asDrawable(Context context, int color, boolean inverted) {
|
||||||
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
|
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);
|
foreground.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||||
|
|
||||||
@ -39,8 +40,10 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
|||||||
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
||||||
}
|
}
|
||||||
|
|
||||||
Drawable gradient = context.getResources().getDrawable(ThemeUtil.isDarkTheme(context) ? R.drawable.avatar_gradient_dark
|
Drawable gradient = AppCompatResources.getDrawable(
|
||||||
: R.drawable.avatar_gradient_light);
|
context,
|
||||||
|
ThemeUtil.isDarkTheme(context) ? R.drawable.avatar_gradient_dark : R.drawable.avatar_gradient_light
|
||||||
|
);
|
||||||
|
|
||||||
return new ExpandingLayerDrawable(new Drawable[] {background, foreground, gradient});
|
return new ExpandingLayerDrawable(new Drawable[] {background, foreground, gradient});
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,9 @@ interface StorageProtocol {
|
|||||||
fun markAsSyncing(timestamp: Long, author: String)
|
fun markAsSyncing(timestamp: Long, author: String)
|
||||||
fun markAsSending(timestamp: Long, author: String)
|
fun markAsSending(timestamp: Long, author: String)
|
||||||
fun markAsSent(timestamp: Long, author: String)
|
fun markAsSent(timestamp: Long, author: String)
|
||||||
|
fun markAsSentToCommunity(threadID: Long, messageID: Long)
|
||||||
fun markUnidentified(timestamp: Long, author: String)
|
fun markUnidentified(timestamp: Long, author: String)
|
||||||
|
fun markUnidentifiedInCommunity(threadID: Long, messageID: Long)
|
||||||
fun markAsSyncFailed(timestamp: Long, author: String, error: Exception)
|
fun markAsSyncFailed(timestamp: Long, author: String, error: Exception)
|
||||||
fun markAsSentFailed(timestamp: Long, author: String, error: Exception)
|
fun markAsSentFailed(timestamp: Long, author: String, error: Exception)
|
||||||
fun clearErrorMessage(messageID: Long)
|
fun clearErrorMessage(messageID: Long)
|
||||||
|
@ -77,7 +77,7 @@ class Contact(val sessionID: String) {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun contextForRecipient(recipient: Recipient): ContactContext {
|
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) {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val numberToDelete = messageServerIds.size
|
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)
|
// FIXME: This entire process should probably run in a transaction (with the attachment deletion happening only if it succeeded)
|
||||||
try {
|
try {
|
||||||
@ -42,6 +42,7 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
|
|||||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
}
|
}
|
||||||
catch (e: Exception) {
|
catch (e: Exception) {
|
||||||
|
Log.w(TAG, "OpenGroupDeleteJob failed: $e")
|
||||||
delegate?.handleJobFailed(this, dispatcherName, e)
|
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,14 +43,14 @@ sealed class Destination {
|
|||||||
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
|
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
|
||||||
ClosedGroup(groupPublicKey)
|
ClosedGroup(groupPublicKey)
|
||||||
}
|
}
|
||||||
address.isOpenGroup -> {
|
address.isCommunity -> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val threadID = storage.getThreadId(address)!!
|
val threadID = storage.getThreadId(address)!!
|
||||||
storage.getOpenGroup(threadID)?.let {
|
storage.getOpenGroup(threadID)?.let {
|
||||||
OpenGroup(roomToken = it.room, server = it.server, fileIds = fileIds)
|
OpenGroup(roomToken = it.room, server = it.server, fileIds = fileIds)
|
||||||
} ?: throw Exception("Missing open group for thread with ID: $threadID.")
|
} ?: throw Exception("Missing open group for thread with ID: $threadID.")
|
||||||
}
|
}
|
||||||
address.isOpenGroupInbox -> {
|
address.isCommunityInbox -> {
|
||||||
val groupInboxId = GroupUtil.getDecodedGroupID(address.serialize()).split("!")
|
val groupInboxId = GroupUtil.getDecodedGroupID(address.serialize()).split("!")
|
||||||
OpenGroupInbox(
|
OpenGroupInbox(
|
||||||
groupInboxId.dropLast(2).joinToString("!"),
|
groupInboxId.dropLast(2).joinToString("!"),
|
||||||
|
@ -602,8 +602,7 @@ object OpenGroupApi {
|
|||||||
// region Message Deletion
|
// region Message Deletion
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
||||||
val request =
|
val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID))
|
||||||
Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID))
|
|
||||||
return send(request).map {
|
return send(request).map {
|
||||||
Log.d("Loki", "Message deletion successful.")
|
Log.d("Loki", "Message deletion successful.")
|
||||||
}
|
}
|
||||||
@ -659,7 +658,9 @@ object OpenGroupApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
|
||||||
val requests = mutableListOf<BatchRequestInfo<*>>(
|
val requests = mutableListOf<BatchRequestInfo<*>>(
|
||||||
|
// Ban request
|
||||||
BatchRequestInfo(
|
BatchRequestInfo(
|
||||||
request = BatchRequest(
|
request = BatchRequest(
|
||||||
method = POST,
|
method = POST,
|
||||||
@ -669,6 +670,7 @@ object OpenGroupApi {
|
|||||||
endpoint = Endpoint.UserBan(publicKey),
|
endpoint = Endpoint.UserBan(publicKey),
|
||||||
responseType = object: TypeReference<Any>(){}
|
responseType = object: TypeReference<Any>(){}
|
||||||
),
|
),
|
||||||
|
// Delete request
|
||||||
BatchRequestInfo(
|
BatchRequestInfo(
|
||||||
request = BatchRequest(DELETE, "/room/$room/all/$publicKey"),
|
request = BatchRequest(DELETE, "/room/$room/all/$publicKey"),
|
||||||
endpoint = Endpoint.RoomDeleteMessages(room, publicKey),
|
endpoint = Endpoint.RoomDeleteMessages(room, publicKey),
|
||||||
|
@ -39,6 +39,7 @@ import org.session.libsignal.crypto.PushTransportDetails
|
|||||||
import org.session.libsignal.protos.SignalServiceProtos
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.Namespace
|
import org.session.libsignal.utilities.Namespace
|
||||||
import org.session.libsignal.utilities.defaultRequiresAuth
|
import org.session.libsignal.utilities.defaultRequiresAuth
|
||||||
import org.session.libsignal.utilities.hasNamespaces
|
import org.session.libsignal.utilities.hasNamespaces
|
||||||
@ -370,7 +371,7 @@ object MessageSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Result Handling
|
// Result Handling
|
||||||
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
|
private fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey()!!
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
val timestamp = message.sentTimestamp!!
|
val timestamp = message.sentTimestamp!!
|
||||||
@ -392,8 +393,10 @@ object MessageSender {
|
|||||||
|
|
||||||
// in case any errors from previous sends
|
// in case any errors from previous sends
|
||||||
storage.clearErrorMessage(messageID)
|
storage.clearErrorMessage(messageID)
|
||||||
|
|
||||||
// Track the open group server message ID
|
// Track the open group server message ID
|
||||||
if (message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)) {
|
val messageIsAddressedToCommunity = message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)
|
||||||
|
if (messageIsAddressedToCommunity) {
|
||||||
val server: String
|
val server: String
|
||||||
val room: String
|
val room: String
|
||||||
when (destination) {
|
when (destination) {
|
||||||
@ -415,9 +418,26 @@ object MessageSender {
|
|||||||
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Mark the message as sent
|
|
||||||
|
// Mark the message as sent.
|
||||||
|
// Note: When sending a message to a community the server modifies the message timestamp
|
||||||
|
// so when we go to look up the message in the local database by timestamp it fails and
|
||||||
|
// we're left with the message delivery status as "Sending" forever! As such, we use a
|
||||||
|
// pair of modified "markAsSentToCommunity" and "markUnidentifiedInCommunity" methods
|
||||||
|
// to retrieve the local message by thread & message ID rather than timestamp when
|
||||||
|
// handling community messages only so we can tick the delivery status over to 'Sent'.
|
||||||
|
// Fixed in: https://optf.atlassian.net/browse/SES-1567
|
||||||
|
if (messageIsAddressedToCommunity)
|
||||||
|
{
|
||||||
|
storage.markAsSentToCommunity(message.threadID!!, message.id!!)
|
||||||
|
storage.markUnidentifiedInCommunity(message.threadID!!, message.id!!)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
storage.markAsSent(timestamp, userPublicKey)
|
storage.markAsSent(timestamp, userPublicKey)
|
||||||
storage.markUnidentified(timestamp, userPublicKey)
|
storage.markUnidentified(timestamp, userPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
// Start the disappearing messages timer if needed
|
// Start the disappearing messages timer if needed
|
||||||
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(message, startDisappearAfterRead = true)
|
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(message, startDisappearAfterRead = true)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
|
@ -8,7 +8,6 @@ import androidx.annotation.VisibleForTesting
|
|||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Util
|
import org.session.libsignal.utilities.Util
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import java.util.Collections
|
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import java.util.regex.Matcher
|
import java.util.regex.Matcher
|
||||||
@ -23,17 +22,17 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
|
|||||||
get() = GroupUtil.isEncodedGroup(address)
|
get() = GroupUtil.isEncodedGroup(address)
|
||||||
val isClosedGroup: Boolean
|
val isClosedGroup: Boolean
|
||||||
get() = GroupUtil.isClosedGroup(address)
|
get() = GroupUtil.isClosedGroup(address)
|
||||||
val isOpenGroup: Boolean
|
val isCommunity: Boolean
|
||||||
get() = GroupUtil.isOpenGroup(address)
|
get() = GroupUtil.isCommunity(address)
|
||||||
val isOpenGroupInbox: Boolean
|
val isCommunityInbox: Boolean
|
||||||
get() = GroupUtil.isOpenGroupInbox(address)
|
get() = GroupUtil.isCommunityInbox(address)
|
||||||
val isOpenGroupOutbox: Boolean
|
val isCommunityOutbox: Boolean
|
||||||
get() = address.startsWith(IdPrefix.BLINDED.value) || address.startsWith(IdPrefix.BLINDEDV2.value)
|
get() = address.startsWith(IdPrefix.BLINDED.value) || address.startsWith(IdPrefix.BLINDEDV2.value)
|
||||||
val isContact: Boolean
|
val isContact: Boolean
|
||||||
get() = !(isGroup || isOpenGroupInbox)
|
get() = !(isGroup || isCommunityInbox)
|
||||||
|
|
||||||
fun contactIdentifier(): String {
|
fun contactIdentifier(): String {
|
||||||
if (!isContact && !isOpenGroup) {
|
if (!isContact && !isCommunity) {
|
||||||
if (isGroup) throw AssertionError("Not e164, is group")
|
if (isGroup) throw AssertionError("Not e164, is group")
|
||||||
throw AssertionError("Not e164, unknown")
|
throw AssertionError("Not e164, unknown")
|
||||||
}
|
}
|
||||||
@ -168,8 +167,9 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun fromSerializedList(serialized: String, delimiter: Char): List<Address> {
|
fun fromSerializedList(serialized: String, delimiter: Char): List<Address> {
|
||||||
val escapedAddresses = DelimiterUtil.split(serialized, delimiter)
|
val escapedAddresses = DelimiterUtil.split(serialized, delimiter)
|
||||||
|
val set = escapedAddresses.toSet().sorted()
|
||||||
val addresses: MutableList<Address> = LinkedList()
|
val addresses: MutableList<Address> = LinkedList()
|
||||||
for (escapedAddress in escapedAddresses) {
|
for (escapedAddress in set) {
|
||||||
addresses.add(fromSerialized(DelimiterUtil.unescape(escapedAddress, delimiter)))
|
addresses.add(fromSerialized(DelimiterUtil.unescape(escapedAddress, delimiter)))
|
||||||
}
|
}
|
||||||
return addresses
|
return addresses
|
||||||
@ -177,9 +177,9 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun toSerializedList(addresses: List<Address>, delimiter: Char): String {
|
fun toSerializedList(addresses: List<Address>, delimiter: Char): String {
|
||||||
Collections.sort(addresses)
|
val set = addresses.toSet().sorted()
|
||||||
val escapedAddresses: MutableList<String> = LinkedList()
|
val escapedAddresses: MutableList<String> = LinkedList()
|
||||||
for (address in addresses) {
|
for (address in set) {
|
||||||
escapedAddresses.add(DelimiterUtil.escape(address.serialize(), delimiter))
|
escapedAddresses.add(DelimiterUtil.escape(address.serialize(), delimiter))
|
||||||
}
|
}
|
||||||
return Util.join(escapedAddresses, delimiter.toString() + "")
|
return Util.join(escapedAddresses, delimiter.toString() + "")
|
||||||
|
@ -22,7 +22,7 @@ class GroupRecord(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val isOpenGroup: Boolean
|
val isOpenGroup: Boolean
|
||||||
get() = Address.fromSerialized(encodedId).isOpenGroup
|
get() = Address.fromSerialized(encodedId).isCommunity
|
||||||
val isClosedGroup: Boolean
|
val isClosedGroup: Boolean
|
||||||
get() = Address.fromSerialized(encodedId).isClosedGroup
|
get() = Address.fromSerialized(encodedId).isClosedGroup
|
||||||
|
|
||||||
|
@ -8,12 +8,12 @@ import java.io.IOException
|
|||||||
|
|
||||||
object GroupUtil {
|
object GroupUtil {
|
||||||
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
|
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
|
||||||
const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!"
|
const val COMMUNITY_PREFIX = "__loki_public_chat_group__!"
|
||||||
const val OPEN_GROUP_INBOX_PREFIX = "__open_group_inbox__!"
|
const val COMMUNITY_INBOX_PREFIX = "__open_group_inbox__!"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getEncodedOpenGroupID(groupID: ByteArray): String {
|
fun getEncodedOpenGroupID(groupID: ByteArray): String {
|
||||||
return OPEN_GROUP_PREFIX + Hex.toStringCondensed(groupID)
|
return COMMUNITY_PREFIX + Hex.toStringCondensed(groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@ -25,7 +25,7 @@ object GroupUtil {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): Address {
|
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
|
@JvmStatic
|
||||||
@ -69,17 +69,17 @@ object GroupUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isEncodedGroup(groupId: String): Boolean {
|
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
|
@JvmStatic
|
||||||
fun isOpenGroup(groupId: String): Boolean {
|
fun isCommunity(groupId: String): Boolean {
|
||||||
return groupId.startsWith(OPEN_GROUP_PREFIX)
|
return groupId.startsWith(COMMUNITY_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isOpenGroupInbox(groupId: String): Boolean {
|
fun isCommunityInbox(groupId: String): Boolean {
|
||||||
return groupId.startsWith(OPEN_GROUP_INBOX_PREFIX)
|
return groupId.startsWith(COMMUNITY_INBOX_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
@ -459,16 +459,16 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
}
|
}
|
||||||
public boolean is1on1() { return address.isContact() && !isLocalNumber; }
|
public boolean is1on1() { return address.isContact() && !isLocalNumber; }
|
||||||
|
|
||||||
public boolean isOpenGroupRecipient() {
|
public boolean isCommunityRecipient() {
|
||||||
return address.isOpenGroup();
|
return address.isCommunity();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOpenGroupOutboxRecipient() {
|
public boolean isOpenGroupOutboxRecipient() {
|
||||||
return address.isOpenGroupOutbox();
|
return address.isCommunityOutbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOpenGroupInboxRecipient() {
|
public boolean isOpenGroupInboxRecipient() {
|
||||||
return address.isOpenGroupInbox();
|
return address.isCommunityInbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isClosedGroupRecipient() {
|
public boolean isClosedGroupRecipient() {
|
||||||
|
Loading…
Reference in New Issue
Block a user