mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 08:47:46 +00:00
Merge branch 'dev'
This commit is contained in:
commit
d0487c0eb8
@ -110,6 +110,7 @@ dependencies {
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
implementation 'app.cash.copper:copper-flow:1.0.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||
@ -158,8 +159,8 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 279
|
||||
def canonicalVersionName = "1.13.1"
|
||||
def canonicalVersionCode = 282
|
||||
def canonicalVersionName = "1.13.4"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
|
@ -24,7 +24,7 @@ import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.HandlerThread;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
@ -44,6 +44,7 @@ import org.session.libsession.utilities.ProfilePictureUtilities;
|
||||
import org.session.libsession.utilities.SSKEnvironment;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.WindowDebouncer;
|
||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
@ -93,6 +94,7 @@ import java.security.Security;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.Timer;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ -127,7 +129,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
public Poller poller = null;
|
||||
public Broadcaster broadcaster = null;
|
||||
private Job firebaseInstanceIdJob;
|
||||
private Handler conversationListNotificationHandler;
|
||||
private WindowDebouncer conversationListDebouncer;
|
||||
private HandlerThread conversationListHandlerThread;
|
||||
private Handler conversationListHandler;
|
||||
private PersistentLogger persistentLogger;
|
||||
|
||||
@Inject LokiAPIDatabase lokiAPIDatabase;
|
||||
@ -136,9 +140,18 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
@Inject JobDatabase jobDatabase;
|
||||
@Inject TextSecurePreferences textSecurePreferences;
|
||||
CallMessageProcessor callMessageProcessor;
|
||||
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||
|
||||
private volatile boolean isAppVisible;
|
||||
|
||||
@Override
|
||||
public Object getSystemService(String name) {
|
||||
if (MessagingModuleConfiguration.MESSAGING_MODULE_SERVICE.equals(name)) {
|
||||
return messagingModuleConfiguration;
|
||||
}
|
||||
return super.getSystemService(name);
|
||||
}
|
||||
|
||||
public static ApplicationContext getInstance(Context context) {
|
||||
return (ApplicationContext) context.getApplicationContext();
|
||||
}
|
||||
@ -148,10 +161,21 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
}
|
||||
|
||||
public Handler getConversationListNotificationHandler() {
|
||||
if (this.conversationListNotificationHandler == null) {
|
||||
conversationListNotificationHandler = new Handler(Looper.getMainLooper());
|
||||
if (this.conversationListHandlerThread == null) {
|
||||
conversationListHandlerThread = new HandlerThread("ConversationListHandler");
|
||||
conversationListHandlerThread.start();
|
||||
}
|
||||
return this.conversationListNotificationHandler;
|
||||
if (this.conversationListHandler == null) {
|
||||
conversationListHandler = new Handler(conversationListHandlerThread.getLooper());
|
||||
}
|
||||
return conversationListHandler;
|
||||
}
|
||||
|
||||
public WindowDebouncer getConversationListDebouncer() {
|
||||
if (conversationListDebouncer == null) {
|
||||
conversationListDebouncer = new WindowDebouncer(1000, new Timer());
|
||||
}
|
||||
return conversationListDebouncer;
|
||||
}
|
||||
|
||||
public PersistentLogger getPersistentLogger() {
|
||||
@ -161,7 +185,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
@Override
|
||||
public void onCreate() {
|
||||
DatabaseModule.init(this);
|
||||
MessagingModuleConfiguration.configure(this);
|
||||
super.onCreate();
|
||||
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
|
||||
storage,
|
||||
messageDataProvider,
|
||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
|
||||
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
||||
Log.i(TAG, "onCreate()");
|
||||
startKovenant();
|
||||
@ -174,11 +203,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
|
||||
broadcaster = new Broadcaster(this);
|
||||
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
|
||||
MessagingModuleConfiguration.Companion.configure(this,
|
||||
storage,
|
||||
messageDataProvider,
|
||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this)
|
||||
);
|
||||
SnodeModule.Companion.configure(apiDB, broadcaster);
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey != null) {
|
||||
|
@ -39,11 +39,14 @@ import android.widget.ImageView;
|
||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import androidx.core.os.CancellationSignal;
|
||||
|
||||
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.crypto.BiometricSecretProvider;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
|
||||
|
||||
import java.security.Signature;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
@ -61,6 +64,8 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||
private CancellationSignal fingerprintCancellationSignal;
|
||||
private FingerprintListener fingerprintListener;
|
||||
|
||||
private final BiometricSecretProvider biometricSecretProvider = new BiometricSecretProvider();
|
||||
|
||||
private boolean authenticated;
|
||||
private boolean failure;
|
||||
|
||||
@ -200,7 +205,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||
if (fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) {
|
||||
Log.i(TAG, "Listening for fingerprints...");
|
||||
fingerprintCancellationSignal = new CancellationSignal();
|
||||
fingerprintManager.authenticate(null, 0, fingerprintCancellationSignal, fingerprintListener, null);
|
||||
fingerprintManager.authenticate(new FingerprintManagerCompat.CryptoObject(biometricSecretProvider.getOrCreateBiometricSignature(this)), 0, fingerprintCancellationSignal, fingerprintListener, null);
|
||||
} else {
|
||||
Log.i(TAG, "firing intent...");
|
||||
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock Session", "");
|
||||
@ -224,6 +229,27 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
|
||||
Log.i(TAG, "onAuthenticationSucceeded");
|
||||
if (result.getCryptoObject() == null || result.getCryptoObject().getSignature() == null) {
|
||||
// authentication failed
|
||||
onAuthenticationFailed();
|
||||
return;
|
||||
}
|
||||
// Signature object now successfully unlocked
|
||||
boolean authenticationSucceeded = false;
|
||||
try {
|
||||
Signature signature = result.getCryptoObject().getSignature();
|
||||
byte[] random = biometricSecretProvider.getRandomData();
|
||||
signature.update(random);
|
||||
byte[] signed = signature.sign();
|
||||
authenticationSucceeded = biometricSecretProvider.verifySignature(random, signed);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "onAuthentication signature generation and verification failed", e);
|
||||
}
|
||||
if (!authenticationSucceeded) {
|
||||
onAuthenticationFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_check_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
|
||||
fingerprintPrompt.animate().setInterpolator(new BounceInterpolator()).scaleX(1.1f).scaleY(1.1f).setDuration(500).setListener(new AnimationCompleteListener() {
|
||||
@ -239,7 +265,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
Log.w(TAG, "onAuthenticatoinFailed()");
|
||||
Log.w(TAG, "onAuthenticationFailed()");
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_close_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);
|
||||
|
@ -5,7 +5,14 @@ import android.text.TextUtils
|
||||
import com.google.protobuf.ByteString
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.*
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.UploadResult
|
||||
import org.session.libsession.utilities.Util
|
||||
@ -126,7 +133,7 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
|
||||
return mmsDb.getMessage(mmsMessageId).use { cursor ->
|
||||
mmsDb.readerFor(cursor).next
|
||||
}.isOutgoing
|
||||
}?.isOutgoing ?: false
|
||||
}
|
||||
|
||||
override fun isOutgoingMessage(timestamp: Long): Boolean {
|
||||
|
@ -10,29 +10,51 @@ import com.annimon.stream.function.Predicate
|
||||
import com.google.protobuf.ByteString
|
||||
import net.sqlcipher.database.SQLiteDatabase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.utilities.Conversions
|
||||
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.*
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.database.*
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsignal.crypto.kdf.HKDFv3
|
||||
import org.session.libsignal.utilities.ByteUtil
|
||||
import java.io.*
|
||||
import java.lang.Exception
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Header
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Sticker
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
||||
import org.thoughtcrime.securesms.database.JobDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||
import org.thoughtcrime.securesms.database.PushDatabase
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.Flushable
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import javax.crypto.*
|
||||
import java.util.LinkedList
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.IllegalBlockSizeException
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@ -245,8 +267,8 @@ object FullBackupExporter {
|
||||
}
|
||||
|
||||
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
|
||||
val columns = arrayOf(MmsDatabase.EXPIRES_IN)
|
||||
val where = MmsDatabase.ID + " = ?"
|
||||
val columns = arrayOf(MmsSmsColumns.EXPIRES_IN)
|
||||
val where = MmsSmsColumns.ID + " = ?"
|
||||
val args = arrayOf(mmsId.toString())
|
||||
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
|
||||
if (mmsCursor != null && mmsCursor.moveToFirst()) {
|
||||
|
@ -15,19 +15,39 @@ import org.session.libsession.utilities.Util
|
||||
import org.session.libsignal.crypto.kdf.HKDFv3
|
||||
import org.session.libsignal.utilities.ByteUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.*
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
|
||||
import org.thoughtcrime.securesms.database.*
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import java.io.*
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import javax.crypto.*
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.IllegalBlockSizeException
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@ -172,7 +192,7 @@ object FullBackupImporter {
|
||||
}
|
||||
|
||||
private fun trimEntriesForExpiredMessages(context: Context, db: SQLiteDatabase) {
|
||||
val trimmedCondition = " NOT IN (SELECT ${MmsDatabase.ID} FROM ${MmsDatabase.TABLE_NAME})"
|
||||
val trimmedCondition = " NOT IN (SELECT ${MmsSmsColumns.ID} FROM ${MmsDatabase.TABLE_NAME})"
|
||||
db.delete(GroupReceiptDatabase.TABLE_NAME, GroupReceiptDatabase.MMS_ID + trimmedCondition, null)
|
||||
val columns = arrayOf(AttachmentDatabase.ROW_ID, AttachmentDatabase.UNIQUE_ID)
|
||||
val where = AttachmentDatabase.MMS_ID + trimmedCondition
|
||||
|
@ -8,28 +8,26 @@ import android.graphics.Outline;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.provider.ContactsContract;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewOutlineProvider;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
|
||||
|
||||
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import org.session.libsession.avatars.ContactColors;
|
||||
import org.session.libsession.avatars.ContactPhoto;
|
||||
import org.session.libsession.avatars.ResourceContactPhoto;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.RecipientExporter;
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@ -139,7 +137,7 @@ public class AvatarImageView extends AppCompatImageView {
|
||||
requestManager.load(photo.contactPhoto)
|
||||
.fallback(photoPlaceholderDrawable)
|
||||
.error(photoPlaceholderDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.circleCrop()
|
||||
.into(this);
|
||||
} else {
|
||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
@ -10,16 +9,20 @@ import androidx.annotation.DimenRes
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
||||
import org.session.libsession.avatars.ContactColors
|
||||
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
||||
import org.session.libsession.avatars.ProfileContactPhoto
|
||||
import org.session.libsession.avatars.ResourceContactPhoto
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
|
||||
|
||||
class ProfilePictureView : RelativeLayout {
|
||||
private lateinit var binding: ViewProfilePictureBinding
|
||||
class ProfilePictureView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : RelativeLayout(context, attrs) {
|
||||
private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) }
|
||||
lateinit var glide: GlideRequests
|
||||
var publicKey: String? = null
|
||||
var displayName: String? = null
|
||||
@ -28,16 +31,9 @@ class ProfilePictureView : RelativeLayout {
|
||||
var isLarge = false
|
||||
|
||||
private val profilePicturesCache = mutableMapOf<String, String?>()
|
||||
private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
@ -105,21 +101,24 @@ class ProfilePictureView : RelativeLayout {
|
||||
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
|
||||
val signalProfilePicture = recipient.contactPhoto
|
||||
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
||||
val sizeInPX = resources.getDimensionPixelSize(sizeResId)
|
||||
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||
glide.clear(imageView)
|
||||
glide.load(signalProfilePicture)
|
||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||
.circleCrop()
|
||||
.error(AvatarPlaceholderGenerator.generate(context,sizeInPX, publicKey, displayName))
|
||||
.into(imageView)
|
||||
profilePicturesCache[publicKey] = recipient.profileAvatar
|
||||
.placeholder(unknownRecipientDrawable)
|
||||
.centerCrop()
|
||||
.error(unknownRecipientDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.circleCrop()
|
||||
.into(imageView)
|
||||
} else {
|
||||
glide.clear(imageView)
|
||||
glide.load(AvatarPlaceholderGenerator.generate(context, sizeInPX, publicKey, displayName))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
|
||||
profilePicturesCache[publicKey] = recipient.profileAvatar
|
||||
glide.load(placeholder)
|
||||
.placeholder(unknownRecipientDrawable)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
|
||||
}
|
||||
profilePicturesCache[publicKey] = recipient.profileAvatar
|
||||
} else {
|
||||
imageView.setImageDrawable(null)
|
||||
}
|
||||
|
@ -264,7 +264,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
|
||||
}
|
||||
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(thumbnailView);
|
||||
} else if (!documentSlides.isEmpty()){
|
||||
thumbnailView.setVisibility(GONE);
|
||||
|
@ -35,6 +35,13 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
if (holder is UserViewHolder) {
|
||||
holder.view.unbind()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (items[position]) {
|
||||
is ContactSelectionListItem.Header -> ViewType.Divider
|
||||
|
@ -54,8 +54,8 @@ class UserView : LinearLayout {
|
||||
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
|
||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
|
||||
val address = user.address.serialize()
|
||||
binding.profilePictureView.glide = glide
|
||||
binding.profilePictureView.update(user)
|
||||
binding.profilePictureView.root.glide = glide
|
||||
binding.profilePictureView.root.update(user)
|
||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
||||
when (actionIndicator) {
|
||||
@ -83,7 +83,7 @@ class UserView : LinearLayout {
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
|
||||
binding.profilePictureView.root.recycle()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding
|
||||
import network.loki.messenger.databinding.ActivityConversationV2Binding
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.mentions.Mention
|
||||
@ -241,7 +242,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
actionMode?.let {
|
||||
onDeselect(message, position, it)
|
||||
}
|
||||
}
|
||||
},
|
||||
lifecycleCoroutineScope = lifecycleScope
|
||||
)
|
||||
adapter.visibleMessageContentViewDelegate = this
|
||||
adapter
|
||||
@ -314,11 +316,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
scrollToFirstUnreadMessageIfNeeded()
|
||||
showOrHideInputIfNeeded()
|
||||
setUpMessageRequestsBar()
|
||||
if (viewModel.recipient.isOpenGroupRecipient) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
|
||||
if (openGroup == null) {
|
||||
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
|
||||
return finish()
|
||||
viewModel.recipient?.let { recipient ->
|
||||
if (recipient.isOpenGroupRecipient) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
|
||||
if (openGroup == null) {
|
||||
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
|
||||
return finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -326,7 +330,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId)
|
||||
threadDb.markAllAsRead(viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
|
||||
val recipient = viewModel.recipient ?: return
|
||||
threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@ -391,17 +396,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
actionBar.title = ""
|
||||
actionBar.customView = actionBarBinding!!.root
|
||||
actionBar.setDisplayShowCustomEnabled(true)
|
||||
actionBarBinding!!.conversationTitleView.text = viewModel.recipient.toShortString()
|
||||
@DimenRes val sizeID: Int = if (viewModel.recipient.isClosedGroupRecipient) {
|
||||
actionBarBinding!!.conversationTitleView.text = viewModel.recipient?.toShortString()
|
||||
@DimenRes val sizeID: Int = if (viewModel.recipient?.isClosedGroupRecipient == true) {
|
||||
R.dimen.medium_profile_picture_size
|
||||
} else {
|
||||
R.dimen.small_profile_picture_size
|
||||
}
|
||||
val size = resources.getDimension(sizeID).roundToInt()
|
||||
actionBarBinding!!.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
|
||||
actionBarBinding!!.profilePictureView.glide = glide
|
||||
actionBarBinding!!.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
|
||||
actionBarBinding!!.profilePictureView.root.glide = glide
|
||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
|
||||
actionBarBinding!!.profilePictureView.update(viewModel.recipient)
|
||||
val profilePictureView = actionBarBinding!!.profilePictureView.root
|
||||
viewModel.recipient?.let { recipient ->
|
||||
profilePictureView.update(recipient)
|
||||
}
|
||||
}
|
||||
|
||||
// called from onCreate
|
||||
@ -437,7 +445,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
if (mediaURI != null && mediaType != null) {
|
||||
if (AttachmentManager.MediaType.IMAGE == mediaType || AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType) {
|
||||
val media = Media(mediaURI, MediaUtil.getMimeType(this, mediaURI)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent())
|
||||
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient, ""), PICK_FROM_LIBRARY)
|
||||
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient!!, ""), PICK_FROM_LIBRARY)
|
||||
return
|
||||
} else {
|
||||
prepMediaForSending(mediaURI, mediaType).addListener(object : ListenableFuture.Listener<Boolean> {
|
||||
@ -491,11 +499,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
private fun setUpRecipientObserver() {
|
||||
viewModel.recipient.addListener(this)
|
||||
viewModel.recipient?.addListener(this)
|
||||
}
|
||||
|
||||
private fun tearDownRecipientObserver() {
|
||||
viewModel.recipient.removeListener(this)
|
||||
viewModel.recipient?.removeListener(this)
|
||||
}
|
||||
|
||||
private fun getLatestOpenGroupInfoIfNeeded() {
|
||||
@ -505,12 +513,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
// called from onCreate
|
||||
private fun setUpBlockedBanner() {
|
||||
if (viewModel.recipient.isGroupRecipient) { return }
|
||||
val sessionID = viewModel.recipient.address.toString()
|
||||
val recipient = viewModel.recipient ?: return
|
||||
if (recipient.isGroupRecipient) { return }
|
||||
val sessionID = recipient.address.toString()
|
||||
val contact = sessionContactDb.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
binding?.blockedBannerTextView?.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
|
||||
binding?.blockedBanner?.isVisible = viewModel.recipient.isBlocked
|
||||
binding?.blockedBanner?.isVisible = recipient.isBlocked
|
||||
binding?.blockedBanner?.setOnClickListener { viewModel.unblock() }
|
||||
}
|
||||
|
||||
@ -558,13 +567,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
if (!isMessageRequestThread()) {
|
||||
ConversationMenuHelper.onPrepareOptionsMenu(
|
||||
menu,
|
||||
menuInflater,
|
||||
viewModel.recipient,
|
||||
viewModel.threadId,
|
||||
this
|
||||
) { onOptionsItemSelected(it) }
|
||||
val recipient = viewModel.recipient
|
||||
if (recipient != null) {
|
||||
ConversationMenuHelper.onPrepareOptionsMenu(
|
||||
menu,
|
||||
menuInflater,
|
||||
recipient,
|
||||
viewModel.threadId,
|
||||
this
|
||||
) { onOptionsItemSelected(it) }
|
||||
}
|
||||
}
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
return true
|
||||
@ -582,21 +594,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
// region Animation & Updating
|
||||
override fun onModified(recipient: Recipient) {
|
||||
runOnUiThread {
|
||||
if (viewModel.recipient.isContactRecipient) {
|
||||
binding?.blockedBanner?.isVisible = viewModel.recipient.isBlocked
|
||||
val recipient = viewModel.recipient
|
||||
if (recipient != null && recipient.isContactRecipient) {
|
||||
binding?.blockedBanner?.isVisible = recipient.isBlocked
|
||||
}
|
||||
setUpMessageRequestsBar()
|
||||
invalidateOptionsMenu()
|
||||
updateSubtitle()
|
||||
showOrHideInputIfNeeded()
|
||||
actionBarBinding?.profilePictureView?.update(recipient)
|
||||
actionBarBinding?.conversationTitleView?.text = recipient.toShortString()
|
||||
if (recipient != null) {
|
||||
actionBarBinding?.profilePictureView?.root?.update(recipient)
|
||||
}
|
||||
actionBarBinding?.conversationTitleView?.text = recipient?.toShortString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showOrHideInputIfNeeded() {
|
||||
if (viewModel.recipient.isClosedGroupRecipient) {
|
||||
val group = groupDb.getGroup(viewModel.recipient.address.toGroupString()).orNull()
|
||||
val recipient = viewModel.recipient
|
||||
if (recipient != null && recipient.isClosedGroupRecipient) {
|
||||
val group = groupDb.getGroup(recipient.address.toGroupString()).orNull()
|
||||
val isActive = (group?.isActive == true)
|
||||
binding?.inputBar?.showInput = isActive
|
||||
} else {
|
||||
@ -632,17 +648,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
private fun isMessageRequestThread(): Boolean {
|
||||
return !viewModel.recipient.isGroupRecipient && !viewModel.recipient.isApproved
|
||||
val recipient = viewModel.recipient ?: return false
|
||||
return !recipient.isGroupRecipient && !recipient.isApproved
|
||||
}
|
||||
|
||||
private fun isOutgoingMessageRequestThread(): Boolean {
|
||||
return !viewModel.recipient.isGroupRecipient &&
|
||||
!(viewModel.recipient.hasApprovedMe() || viewModel.hasReceived())
|
||||
val recipient = viewModel.recipient ?: return false
|
||||
return !recipient.isGroupRecipient &&
|
||||
!recipient.isLocalNumber &&
|
||||
!(recipient.hasApprovedMe() || viewModel.hasReceived())
|
||||
}
|
||||
|
||||
private fun isIncomingMessageRequestThread(): Boolean {
|
||||
return !viewModel.recipient.isGroupRecipient &&
|
||||
!viewModel.recipient.isApproved &&
|
||||
val recipient = viewModel.recipient ?: return false
|
||||
return !recipient.isGroupRecipient &&
|
||||
!recipient.isApproved &&
|
||||
!recipient.isLocalNumber &&
|
||||
!threadDb.getLastSeenAndHasSent(viewModel.threadId).second() &&
|
||||
threadDb.getMessageCount(viewModel.threadId) > 0
|
||||
}
|
||||
@ -701,17 +722,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
private fun showOrUpdateMentionCandidatesIfNeeded(query: String = "") {
|
||||
val additionalContentContainer = binding?.additionalContentContainer ?: return
|
||||
val recipient = viewModel.recipient ?: return
|
||||
if (!isShowingMentionCandidatesView) {
|
||||
additionalContentContainer.removeAllViews()
|
||||
val view = MentionCandidatesView(this)
|
||||
view.glide = glide
|
||||
view.onCandidateSelected = { handleMentionSelected(it) }
|
||||
additionalContentContainer.addView(view)
|
||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
|
||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
|
||||
this.mentionCandidatesView = view
|
||||
view.show(candidates, viewModel.threadId)
|
||||
} else {
|
||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
|
||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
|
||||
this.mentionCandidatesView!!.setMentionCandidates(candidates)
|
||||
}
|
||||
isShowingMentionCandidatesView = true
|
||||
@ -839,15 +861,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
private fun updateSubtitle() {
|
||||
val actionBarBinding = actionBarBinding ?: return
|
||||
actionBarBinding.muteIconImageView.isVisible = viewModel.recipient.isMuted
|
||||
val recipient = viewModel.recipient ?: return
|
||||
actionBarBinding.muteIconImageView.isVisible = recipient.isMuted
|
||||
actionBarBinding.conversationSubtitleView.isVisible = true
|
||||
if (viewModel.recipient.isMuted) {
|
||||
if (viewModel.recipient.mutedUntil != Long.MAX_VALUE) {
|
||||
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(viewModel.recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
|
||||
if (recipient.isMuted) {
|
||||
if (recipient.mutedUntil != Long.MAX_VALUE) {
|
||||
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
|
||||
} else {
|
||||
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever)
|
||||
}
|
||||
} else if (viewModel.recipient.isGroupRecipient) {
|
||||
} else if (recipient.isGroupRecipient) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
|
||||
if (openGroup != null) {
|
||||
val userCount = lokiApiDb.getUserCount(openGroup.room, openGroup.server) ?: 0
|
||||
@ -866,7 +889,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
if (item.itemId == android.R.id.home) {
|
||||
return false
|
||||
}
|
||||
return ConversationMenuHelper.onOptionItemSelected(this, item, viewModel.recipient)
|
||||
return viewModel.recipient?.let { recipient ->
|
||||
ConversationMenuHelper.onOptionItemSelected(this, item, recipient)
|
||||
} ?: false
|
||||
}
|
||||
|
||||
// `position` is the adapter position; not the visual position
|
||||
@ -896,7 +921,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
// `position` is the adapter position; not the visual position
|
||||
private fun handleSwipeToReply(message: MessageRecord, position: Int) {
|
||||
binding?.inputBar?.draftQuote(viewModel.recipient, message, glide)
|
||||
val recipient = viewModel.recipient ?: return
|
||||
binding?.inputBar?.draftQuote(recipient, message, glide)
|
||||
}
|
||||
|
||||
// `position` is the adapter position; not the visual position
|
||||
@ -1002,12 +1028,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
override fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) {
|
||||
if (indexInAdapter < 0 || indexInAdapter >= adapter.itemCount) { return }
|
||||
val viewHolder = binding?.conversationRecyclerView?.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder ?: return
|
||||
viewHolder.view.playVoiceMessage()
|
||||
val visibleMessageView = ViewVisibleMessageBinding.bind(viewHolder.view).visibleMessageView
|
||||
visibleMessageView.playVoiceMessage()
|
||||
}
|
||||
|
||||
override fun sendMessage() {
|
||||
if (viewModel.recipient.isContactRecipient && viewModel.recipient.isBlocked) {
|
||||
BlockedDialog(viewModel.recipient).show(supportFragmentManager, "Blocked Dialog")
|
||||
val recipient = viewModel.recipient ?: return
|
||||
if (recipient.isContactRecipient && recipient.isBlocked) {
|
||||
BlockedDialog(recipient).show(supportFragmentManager, "Blocked Dialog")
|
||||
return
|
||||
}
|
||||
val binding = binding ?: return
|
||||
@ -1019,24 +1047,26 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun commitInputContent(contentUri: Uri) {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
val media = Media(contentUri, MediaUtil.getMimeType(this, contentUri)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent())
|
||||
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient, getMessageBody()), PICK_FROM_LIBRARY)
|
||||
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), recipient, getMessageBody()), PICK_FROM_LIBRARY)
|
||||
}
|
||||
|
||||
private fun processMessageRequestApproval() {
|
||||
if (isIncomingMessageRequestThread()) {
|
||||
acceptMessageRequest()
|
||||
} else if (!viewModel.recipient.isApproved) {
|
||||
} else if (viewModel.recipient?.isApproved == false) {
|
||||
// edge case for new outgoing thread on new recipient without sending approval messages
|
||||
viewModel.setRecipientApproved()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false) {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
processMessageRequestApproval()
|
||||
val text = getMessageBody()
|
||||
val userPublicKey = textSecurePreferences.getLocalNumber()
|
||||
val isNoteToSelf = (viewModel.recipient.isContactRecipient && viewModel.recipient.address.toString() == userPublicKey)
|
||||
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
|
||||
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
|
||||
val dialog = SendSeedDialog { sendTextOnlyMessage(true) }
|
||||
return dialog.show(supportFragmentManager, "Send Seed Dialog")
|
||||
@ -1045,7 +1075,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val message = VisibleMessage()
|
||||
message.sentTimestamp = System.currentTimeMillis()
|
||||
message.text = text
|
||||
val outgoingTextMessage = OutgoingTextMessage.from(message, viewModel.recipient)
|
||||
val outgoingTextMessage = OutgoingTextMessage.from(message, recipient)
|
||||
// Clear the input bar
|
||||
binding?.inputBar?.text = ""
|
||||
binding?.inputBar?.cancelQuoteDraft()
|
||||
@ -1055,14 +1085,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
currentMentionStartIndex = -1
|
||||
mentions.clear()
|
||||
// Put the message in the database
|
||||
message.id = smsDb.insertMessageOutbox(viewModel.threadId, outgoingTextMessage, false, message.sentTimestamp!!) { }
|
||||
message.id = smsDb.insertMessageOutbox(viewModel.threadId, outgoingTextMessage, false, message.sentTimestamp!!, null, true)
|
||||
// Send it
|
||||
MessageSender.send(message, viewModel.recipient.address)
|
||||
MessageSender.send(message, recipient.address)
|
||||
// Send a typing stopped message
|
||||
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
|
||||
}
|
||||
|
||||
private fun sendAttachments(attachments: List<Attachment>, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
processMessageRequestApproval()
|
||||
// Create the message
|
||||
val message = VisibleMessage()
|
||||
@ -1073,7 +1104,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val sender = if (it.isOutgoing) fromSerialized(textSecurePreferences.getLocalNumber()!!) else it.individualRecipient.address
|
||||
QuoteModel(it.dateSent, sender, it.body, false, quotedAttachments)
|
||||
}
|
||||
val outgoingTextMessage = OutgoingMediaMessage.from(message, viewModel.recipient, attachments, quote, linkPreview)
|
||||
val outgoingTextMessage = OutgoingMediaMessage.from(message, recipient, attachments, quote, linkPreview)
|
||||
// Clear the input bar
|
||||
binding?.inputBar?.text = ""
|
||||
binding?.inputBar?.cancelQuoteDraft()
|
||||
@ -1087,9 +1118,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
// Reset attachments button if needed
|
||||
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
|
||||
// Put the message in the database
|
||||
message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, viewModel.threadId, false) { }
|
||||
message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, viewModel.threadId, false, null, runThreadUpdate = true)
|
||||
// Send it
|
||||
MessageSender.send(message, viewModel.recipient.address, attachments, quote, linkPreview)
|
||||
MessageSender.send(message, recipient.address, attachments, quote, linkPreview)
|
||||
// Send a typing stopped message
|
||||
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
|
||||
}
|
||||
@ -1119,8 +1150,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
private fun pickFromLibrary() {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
binding?.inputBar?.text?.trim()?.let { text ->
|
||||
AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, viewModel.recipient, text)
|
||||
AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, recipient, text)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1187,7 +1219,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
sendAttachments(slideDeck.asAttachments(), body)
|
||||
}
|
||||
INVITE_CONTACTS -> {
|
||||
if (!viewModel.recipient.isOpenGroupRecipient) { return }
|
||||
if (viewModel.recipient?.isOpenGroupRecipient != true) { return }
|
||||
val extras = intent?.extras ?: return
|
||||
if (!intent.hasExtra(selectedContactsKey)) { return }
|
||||
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
|
||||
@ -1268,13 +1300,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun deleteMessages(messages: Set<MessageRecord>) {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
if (!IS_UNSEND_REQUESTS_ENABLED) {
|
||||
deleteMessagesWithoutUnsendRequest(messages)
|
||||
return
|
||||
}
|
||||
val allSentByCurrentUser = messages.all { it.isOutgoing }
|
||||
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id) != null }
|
||||
if (viewModel.recipient.isOpenGroupRecipient) {
|
||||
if (recipient.isOpenGroupRecipient) {
|
||||
val messageCount = messages.size
|
||||
val builder = AlertDialog.Builder(this)
|
||||
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||
@ -1293,7 +1326,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
builder.show()
|
||||
} else if (allSentByCurrentUser && allHasHash) {
|
||||
val bottomSheet = DeleteOptionsBottomSheet()
|
||||
bottomSheet.recipient = viewModel.recipient
|
||||
bottomSheet.recipient = recipient
|
||||
bottomSheet.onDeleteForMeTapped = {
|
||||
for (message in messages) {
|
||||
viewModel.deleteLocally(message)
|
||||
@ -1452,16 +1485,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun reply(messages: Set<MessageRecord>) {
|
||||
binding?.inputBar?.draftQuote(viewModel.recipient, messages.first(), glide)
|
||||
val recipient = viewModel.recipient ?: return
|
||||
binding?.inputBar?.draftQuote(recipient, messages.first(), glide)
|
||||
endActionMode()
|
||||
}
|
||||
|
||||
private fun sendMediaSavedNotification() {
|
||||
if (viewModel.recipient.isGroupRecipient) { return }
|
||||
val recipient = viewModel.recipient ?: return
|
||||
if (recipient.isGroupRecipient) { return }
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val kind = DataExtractionNotification.Kind.MediaSaved(timestamp)
|
||||
val message = DataExtractionNotification(kind)
|
||||
MessageSender.send(message, viewModel.recipient.address)
|
||||
MessageSender.send(message, recipient.address)
|
||||
}
|
||||
|
||||
private fun endActionMode() {
|
||||
|
@ -4,10 +4,25 @@ import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.util.SparseArray
|
||||
import android.util.SparseBooleanArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.util.getOrDefault
|
||||
import androidx.core.util.set
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||
@ -19,13 +34,33 @@ import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||
|
||||
class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
|
||||
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit,
|
||||
private val glide: GlideRequests, private val onDeselect: (MessageRecord, Int) -> Unit)
|
||||
private val glide: GlideRequests, private val onDeselect: (MessageRecord, Int) -> Unit, lifecycleCoroutineScope: LifecycleCoroutineScope)
|
||||
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
|
||||
private val messageDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||
private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() }
|
||||
private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() }
|
||||
var selectedItems = mutableSetOf<MessageRecord>()
|
||||
private var searchQuery: String? = null
|
||||
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null
|
||||
|
||||
private val updateQueue = Channel<String>(1024, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
private val contactCache = SparseArray<Contact>(100)
|
||||
private val contactLoadedCache = SparseBooleanArray(100)
|
||||
init {
|
||||
lifecycleCoroutineScope.launch(IO) {
|
||||
while (isActive) {
|
||||
val item = updateQueue.receive()
|
||||
val contact = getSenderInfo(item) ?: continue
|
||||
contactCache[item.hashCode()] = contact
|
||||
contactLoadedCache[item.hashCode()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getSenderInfo(sender: String): Contact? {
|
||||
return contactDB.getContactWithSessionID(sender)
|
||||
}
|
||||
|
||||
sealed class ViewType(val rawValue: Int) {
|
||||
object Visible : ViewType(0)
|
||||
object Control : ViewType(1)
|
||||
@ -39,7 +74,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
}
|
||||
}
|
||||
|
||||
class VisibleMessageViewHolder(val view: VisibleMessageView) : ViewHolder(view)
|
||||
class VisibleMessageViewHolder(val view: View) : ViewHolder(view)
|
||||
class ControlMessageViewHolder(val view: ControlMessageView) : ViewHolder(view)
|
||||
|
||||
override fun getItemViewType(cursor: Cursor): Int {
|
||||
@ -52,7 +87,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val viewType = ViewType.allValues[viewType]
|
||||
return when (viewType) {
|
||||
ViewType.Visible -> VisibleMessageViewHolder(VisibleMessageView(context))
|
||||
ViewType.Visible -> VisibleMessageViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.view_visible_message, parent, false))
|
||||
ViewType.Control -> ControlMessageViewHolder(ControlMessageView(context))
|
||||
else -> throw IllegalStateException("Unexpected view type: $viewType.")
|
||||
}
|
||||
@ -65,20 +100,31 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
when (viewHolder) {
|
||||
is VisibleMessageViewHolder -> {
|
||||
val view = viewHolder.view
|
||||
val visibleMessageView = ViewVisibleMessageBinding.bind(viewHolder.view).visibleMessageView
|
||||
val isSelected = selectedItems.contains(message)
|
||||
view.snIsSelected = isSelected
|
||||
view.indexInAdapter = position
|
||||
view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery)
|
||||
if (!message.isDeleted) {
|
||||
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
|
||||
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
||||
} else {
|
||||
view.onPress = null
|
||||
view.onSwipeToReply = null
|
||||
view.onLongPress = null
|
||||
visibleMessageView.snIsSelected = isSelected
|
||||
visibleMessageView.indexInAdapter = position
|
||||
val senderId = message.individualRecipient.address.serialize()
|
||||
val senderIdHash = senderId.hashCode()
|
||||
updateQueue.trySend(senderId)
|
||||
if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault(senderIdHash, false)) {
|
||||
getSenderInfo(senderId)?.let { contact ->
|
||||
contactCache[senderIdHash] = contact
|
||||
}
|
||||
}
|
||||
view.contentViewDelegate = visibleMessageContentViewDelegate
|
||||
val contact = contactCache[senderIdHash]
|
||||
|
||||
visibleMessageView.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery, contact, senderId)
|
||||
if (!message.isDeleted) {
|
||||
visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) }
|
||||
visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||
visibleMessageView.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
||||
} else {
|
||||
visibleMessageView.onPress = null
|
||||
visibleMessageView.onSwipeToReply = null
|
||||
visibleMessageView.onLongPress = null
|
||||
}
|
||||
visibleMessageView.contentViewDelegate = visibleMessageContentViewDelegate
|
||||
}
|
||||
is ControlMessageViewHolder -> {
|
||||
viewHolder.view.bind(message, messageBefore)
|
||||
@ -105,7 +151,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
|
||||
override fun onItemViewRecycled(viewHolder: ViewHolder?) {
|
||||
when (viewHolder) {
|
||||
is VisibleMessageViewHolder -> viewHolder.view.recycle()
|
||||
is VisibleMessageViewHolder -> viewHolder.view.findViewById<VisibleMessageView>(R.id.visibleMessageView).recycle()
|
||||
is ControlMessageViewHolder -> viewHolder.view.recycle()
|
||||
}
|
||||
super.onItemViewRecycled(viewHolder)
|
||||
|
@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import java.util.UUID
|
||||
@ -22,8 +23,8 @@ class ConversationViewModel(
|
||||
private val _uiState = MutableStateFlow(ConversationUiState())
|
||||
val uiState: StateFlow<ConversationUiState> = _uiState
|
||||
|
||||
val recipient: Recipient
|
||||
get() = repository.getRecipientForThreadId(threadId)
|
||||
val recipient: Recipient?
|
||||
get() = repository.maybeGetRecipientForThreadId(threadId)
|
||||
|
||||
init {
|
||||
_uiState.update {
|
||||
@ -44,20 +45,24 @@ class ConversationViewModel(
|
||||
}
|
||||
|
||||
fun unblock() {
|
||||
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for unblock action")
|
||||
if (recipient.isContactRecipient) {
|
||||
repository.unblock(recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteLocally(message: MessageRecord) {
|
||||
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for delete locally action")
|
||||
repository.deleteLocally(recipient, message)
|
||||
}
|
||||
|
||||
fun setRecipientApproved() {
|
||||
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for set approved action")
|
||||
repository.setApproved(recipient, true)
|
||||
}
|
||||
|
||||
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
|
||||
val recipient = recipient ?: return@launch
|
||||
repository.deleteForEveryone(threadId, recipient, message)
|
||||
.onFailure {
|
||||
showMessage("Couldn't delete message due to error: $it")
|
||||
@ -92,6 +97,7 @@ class ConversationViewModel(
|
||||
}
|
||||
|
||||
fun acceptMessageRequest() = viewModelScope.launch {
|
||||
val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for accept message request action")
|
||||
repository.acceptMessageRequest(threadId, recipient)
|
||||
.onSuccess {
|
||||
_uiState.update {
|
||||
@ -104,6 +110,7 @@ class ConversationViewModel(
|
||||
}
|
||||
|
||||
fun declineMessageRequest() {
|
||||
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for decline message request action")
|
||||
repository.declineMessageRequest(threadId, recipient)
|
||||
}
|
||||
|
||||
|
@ -118,7 +118,7 @@ class AlbumThumbnailView : FrameLayout {
|
||||
this.slideSize = slides.size
|
||||
}
|
||||
// iterate binding
|
||||
slides.take(5).forEachIndexed { position, slide ->
|
||||
slides.take(MAX_ALBUM_DISPLAY_SIZE).forEachIndexed { position, slide ->
|
||||
val thumbnailView = getThumbnailView(position)
|
||||
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
|
||||
}
|
||||
|
@ -28,11 +28,11 @@ class MentionCandidateView : LinearLayout {
|
||||
|
||||
private fun update() = with(binding) {
|
||||
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
||||
profilePictureView.publicKey = mentionCandidate.publicKey
|
||||
profilePictureView.displayName = mentionCandidate.displayName
|
||||
profilePictureView.additionalPublicKey = null
|
||||
profilePictureView.glide = glide!!
|
||||
profilePictureView.update()
|
||||
profilePictureView.root.publicKey = mentionCandidate.publicKey
|
||||
profilePictureView.root.displayName = mentionCandidate.displayName
|
||||
profilePictureView.root.additionalPublicKey = null
|
||||
profilePictureView.root.glide = glide!!
|
||||
profilePictureView.root.update()
|
||||
if (openGroupServer != null && openGroupRoom != null) {
|
||||
val isUserModerator = OpenGroupAPIV2.isUserModerator(mentionCandidate.publicKey, openGroupRoom!!, openGroupServer!!)
|
||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||
|
@ -9,6 +9,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogJoinOpenGroupBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
@ -37,6 +38,7 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : B
|
||||
val activity = requireContext() as AppCompatActivity
|
||||
ThreadUtils.queue {
|
||||
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
|
||||
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(url)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
|
||||
}
|
||||
dismiss()
|
||||
|
@ -122,9 +122,12 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
linkPreview = null
|
||||
linkPreviewDraftView = null
|
||||
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||
val quoteView = QuoteView(context, QuoteView.Mode.Draft)
|
||||
|
||||
// inflate quoteview with typed array here
|
||||
val layout = LayoutInflater.from(context).inflate(R.layout.view_quote_draft, binding.inputBarAdditionalContentContainer, false)
|
||||
val quoteView = layout.findViewById<QuoteView>(R.id.mainQuoteViewContainer)
|
||||
quoteView.delegate = this
|
||||
binding.inputBarAdditionalContentContainer.addView(quoteView)
|
||||
binding.inputBarAdditionalContentContainer.addView(layout)
|
||||
val attachments = (message as? MmsMessageRecord)?.slideDeck
|
||||
val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize()
|
||||
quoteView.bind(sender, message.body, attachments,
|
||||
|
@ -28,11 +28,11 @@ class MentionCandidateView : RelativeLayout {
|
||||
|
||||
private fun update() = with(binding) {
|
||||
mentionCandidateNameTextView.text = candidate.displayName
|
||||
profilePictureView.publicKey = candidate.publicKey
|
||||
profilePictureView.displayName = candidate.displayName
|
||||
profilePictureView.additionalPublicKey = null
|
||||
profilePictureView.glide = glide!!
|
||||
profilePictureView.update()
|
||||
profilePictureView.root.publicKey = candidate.publicKey
|
||||
profilePictureView.root.displayName = candidate.displayName
|
||||
profilePictureView.root.additionalPublicKey = null
|
||||
profilePictureView.root.glide = glide!!
|
||||
profilePictureView.root.update()
|
||||
if (openGroupServer != null && openGroupRoom != null) {
|
||||
val isUserModerator = OpenGroupAPIV2.isUserModerator(candidate.publicKey, openGroupRoom!!, openGroupServer!!)
|
||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import network.loki.messenger.R
|
||||
@ -11,15 +10,12 @@ import network.loki.messenger.databinding.ViewDeletedMessageBinding
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
class DeletedMessageView : LinearLayout {
|
||||
private lateinit var binding: ViewDeletedMessageBinding
|
||||
private val binding: ViewDeletedMessageBinding by lazy { ViewDeletedMessageBinding.bind(this) }
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
private fun initialize() {
|
||||
binding = ViewDeletedMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
|
@ -3,22 +3,17 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import network.loki.messenger.databinding.ViewDocumentBinding
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
|
||||
class DocumentView : LinearLayout {
|
||||
private lateinit var binding: ViewDocumentBinding
|
||||
private val binding: ViewDocumentBinding by lazy { ViewDocumentBinding.bind(this) }
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
binding = ViewDocumentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@ -14,16 +13,12 @@ import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
class OpenGroupInvitationView : LinearLayout {
|
||||
private lateinit var binding: ViewOpenGroupInvitationBinding
|
||||
private val binding: ViewOpenGroupInvitationBinding by lazy { ViewOpenGroupInvitationBinding.bind(this) }
|
||||
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
|
||||
|
||||
constructor(context: Context): super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
binding = ViewOpenGroupInvitationBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
constructor(context: Context): super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?): super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr)
|
||||
|
||||
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||
// FIXME: This is a really weird approach...
|
||||
|
@ -2,12 +2,11 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.text.StaticLayout
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@ -16,17 +15,13 @@ import network.loki.messenger.databinding.ViewQuoteBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
|
||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||
import org.thoughtcrime.securesms.database.model.Quote
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
// There's quite some calculation going on here. It's a bit complex so don't make changes
|
||||
// if you don't need to. If you do then test:
|
||||
@ -35,27 +30,29 @@ import kotlin.math.min
|
||||
// • Quoted voice messages and documents in both private chats and group chats
|
||||
// • All of the above in both dark mode and light mode
|
||||
@AndroidEntryPoint
|
||||
class QuoteView : LinearLayout {
|
||||
class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) {
|
||||
|
||||
@Inject lateinit var contactDb: SessionContactDatabase
|
||||
|
||||
private lateinit var binding: ViewQuoteBinding
|
||||
private lateinit var mode: Mode
|
||||
private val binding: ViewQuoteBinding by lazy { ViewQuoteBinding.bind(this) }
|
||||
private val vPadding by lazy { toPx(6, resources) }
|
||||
var delegate: QuoteViewDelegate? = null
|
||||
private val mode: Mode
|
||||
|
||||
enum class Mode { Regular, Draft }
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : this(context, Mode.Regular)
|
||||
constructor(context: Context, attrs: AttributeSet) : this(context, Mode.Regular, attrs)
|
||||
init {
|
||||
mode = attrs?.let { attrSet ->
|
||||
context.obtainStyledAttributes(attrSet, R.styleable.QuoteView).use { typedArray ->
|
||||
val modeIndex = typedArray.getInt(R.styleable.QuoteView_quote_mode, 0)
|
||||
Mode.values()[modeIndex]
|
||||
}
|
||||
} ?: Mode.Regular
|
||||
}
|
||||
|
||||
constructor(context: Context, mode: Mode, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
this.mode = mode
|
||||
binding = ViewQuoteBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
// Add padding here (not on binding.mainQuoteViewContainer) to get a bit of a top inset while avoiding
|
||||
// the clipping issue described in getIntrinsicHeight(maxContentWidth:).
|
||||
setPadding(0, toPx(6, resources), 0, 0)
|
||||
// region Lifecycle
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
when (mode) {
|
||||
Mode.Draft -> binding.quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
|
||||
Mode.Regular -> {
|
||||
@ -66,44 +63,6 @@ class QuoteView : LinearLayout {
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region General
|
||||
fun getIntrinsicContentHeight(maxContentWidth: Int): Int {
|
||||
// If we're showing an attachment thumbnail, just constrain to the height of that
|
||||
if (binding.quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) }
|
||||
var result = 0
|
||||
val authorTextViewIntrinsicHeight: Int
|
||||
if (binding.quoteViewAuthorTextView.isVisible) {
|
||||
val author = binding.quoteViewAuthorTextView.text
|
||||
authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, binding.quoteViewAuthorTextView.paint, maxContentWidth)
|
||||
result += authorTextViewIntrinsicHeight
|
||||
}
|
||||
val body = binding.quoteViewBodyTextView.text
|
||||
val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
|
||||
val staticLayout = TextUtilities.getIntrinsicLayout(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
|
||||
result += bodyTextViewIntrinsicHeight
|
||||
if (!binding.quoteViewAuthorTextView.isVisible) {
|
||||
// We want to at least be as high as the cancel button 36DP, and no higher than 3 lines of text.
|
||||
// Height from intrinsic layout is the height of the text before truncation so we shorten
|
||||
// proportionally to our max lines setting.
|
||||
return max(toPx(32, resources) ,min((result / staticLayout.lineCount) * 3, result))
|
||||
} else {
|
||||
// Because we're showing the author text view, we should have a height of at least 32 DP
|
||||
// anyway, so there's no need to constrain to that. We constrain to a max height of 56 DP
|
||||
// because that's approximately the height of the author text view + 2 lines of the body
|
||||
// text view.
|
||||
return min(result, toPx(56, resources))
|
||||
}
|
||||
}
|
||||
|
||||
fun getIntrinsicHeight(maxContentWidth: Int): Int {
|
||||
// The way all this works is that we just calculate the total height the quote view should be
|
||||
// and then center everything inside vertically. This effectively means we're applying padding.
|
||||
// Applying padding the regular way results in a clipping issue though due to a bug in
|
||||
// RelativeLayout.
|
||||
return getIntrinsicContentHeight(maxContentWidth) + (2 * vPadding )
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
|
||||
isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long,
|
||||
@ -115,7 +74,7 @@ class QuoteView : LinearLayout {
|
||||
// Author
|
||||
if (thread.isGroupRecipient) {
|
||||
val author = contactDb.getContactWithSessionID(authorPublicKey)
|
||||
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
|
||||
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: "${authorPublicKey.take(4)}...${authorPublicKey.takeLast(4)}"
|
||||
binding.quoteViewAuthorTextView.text = authorDisplayName
|
||||
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
}
|
||||
@ -190,30 +149,6 @@ class QuoteView : LinearLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateWidth(quote: Quote, bodyWidth: Int, maxContentWidth: Int, thread: Recipient): Int {
|
||||
binding.quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
||||
var paddingWidth = resources.getDimensionPixelSize(R.dimen.medium_spacing) * 5 // initial horizontal padding
|
||||
with (binding) {
|
||||
if (quoteViewAttachmentPreviewContainer.isVisible) {
|
||||
paddingWidth += toPx(40, resources)
|
||||
}
|
||||
if (quoteViewAccentLine.isVisible) {
|
||||
paddingWidth += resources.getDimensionPixelSize(R.dimen.accent_line_thickness)
|
||||
}
|
||||
}
|
||||
val quoteBodyWidth = StaticLayout.getDesiredWidth(binding.quoteViewBodyTextView.text, binding.quoteViewBodyTextView.paint).toInt() + paddingWidth
|
||||
|
||||
val quoteAuthorWidth = if (thread.isGroupRecipient) {
|
||||
val authorPublicKey = quote.author.serialize()
|
||||
val author = contactDb.getContactWithSessionID(authorPublicKey)
|
||||
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
|
||||
StaticLayout.getDesiredWidth(authorDisplayName, binding.quoteViewBodyTextView.paint).toInt() + paddingWidth
|
||||
} else 0
|
||||
|
||||
val quoteWidth = max(quoteBodyWidth, quoteAuthorWidth)
|
||||
val usedWidth = max(quoteWidth, bodyWidth)
|
||||
return min(maxContentWidth, usedWidth)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -14,7 +13,7 @@ import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||
import java.util.Locale
|
||||
|
||||
class UntrustedAttachmentView: LinearLayout {
|
||||
private lateinit var binding: ViewUntrustedAttachmentBinding
|
||||
private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) }
|
||||
enum class AttachmentType {
|
||||
AUDIO,
|
||||
DOCUMENT,
|
||||
@ -22,13 +21,10 @@ class UntrustedAttachmentView: LinearLayout {
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
private fun initialize() {
|
||||
binding = ViewUntrustedAttachmentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
|
@ -5,7 +5,6 @@ import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Spannable
|
||||
import android.text.StaticLayout
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.URLSpan
|
||||
@ -28,6 +27,11 @@ import androidx.core.view.isVisible
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
@ -65,7 +69,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean,
|
||||
glide: GlideRequests, maxWidth: Int, thread: Recipient, searchQuery: String?, contactIsTrusted: Boolean) {
|
||||
glide: GlideRequests, thread: Recipient, searchQuery: String?, contactIsTrusted: Boolean) {
|
||||
// Background
|
||||
val background = getBackground(message.isOutgoing, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
val colorID = if (message.isOutgoing) R.attr.message_sent_background_color else R.attr.message_received_background_color
|
||||
@ -83,14 +87,17 @@ class VisibleMessageContentView : LinearLayout {
|
||||
onContentDoubleTap = null
|
||||
|
||||
if (message.isDeleted) {
|
||||
binding.deletedMessageView.isVisible = true
|
||||
binding.deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message))
|
||||
binding.deletedMessageView.root.isVisible = true
|
||||
binding.deletedMessageView.root.bind(message, VisibleMessageContentView.getTextColor(context,message))
|
||||
return
|
||||
} else {
|
||||
binding.deletedMessageView.isVisible = false
|
||||
binding.deletedMessageView.root.isVisible = false
|
||||
}
|
||||
// clear the
|
||||
binding.bodyTextView.text = null
|
||||
|
||||
binding.quoteView.isVisible = message is MmsMessageRecord && message.quote != null
|
||||
|
||||
binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null
|
||||
|
||||
binding.linkPreviewView.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty()
|
||||
|
||||
@ -98,36 +105,55 @@ class VisibleMessageContentView : LinearLayout {
|
||||
linkPreviewLayout.width = if (mediaThumbnailMessage) 0 else ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
binding.linkPreviewView.layoutParams = linkPreviewLayout
|
||||
|
||||
binding.untrustedView.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty()
|
||||
binding.voiceMessageView.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
|
||||
binding.documentView.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
|
||||
binding.untrustedView.root.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty()
|
||||
binding.voiceMessageView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
|
||||
binding.documentView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
|
||||
binding.albumThumbnailView.isVisible = mediaThumbnailMessage
|
||||
binding.openGroupInvitationView.isVisible = message.isOpenGroupInvitation
|
||||
binding.openGroupInvitationView.root.isVisible = message.isOpenGroupInvitation
|
||||
|
||||
var hideBody = false
|
||||
|
||||
if (message is MmsMessageRecord && message.quote != null) {
|
||||
binding.quoteView.isVisible = true
|
||||
binding.quoteView.root.isVisible = true
|
||||
val quote = message.quote!!
|
||||
// The max content width is the max message bubble size - 2 times the horizontal padding - 2
|
||||
// times the horizontal margin. This unfortunately has to be calculated manually
|
||||
// here to get the layout right.
|
||||
val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - 2 * toPx(16, resources)).roundToInt()
|
||||
val quoteText = if (quote.isOriginalMissing) {
|
||||
context.getString(R.string.QuoteView_original_missing)
|
||||
} else {
|
||||
quote.text
|
||||
}
|
||||
binding.quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
|
||||
binding.quoteView.root.bind(quote.author.toString(), quoteText, quote.attachment, thread,
|
||||
message.isOutgoing, message.isOpenGroupInvitation, message.threadId,
|
||||
quote.isOriginalMissing, glide)
|
||||
onContentClick.add { event ->
|
||||
val r = Rect()
|
||||
binding.quoteView.getGlobalVisibleRect(r)
|
||||
binding.quoteView.root.getGlobalVisibleRect(r)
|
||||
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
|
||||
delegate?.scrollToMessageIfPossible(quote.id)
|
||||
}
|
||||
}
|
||||
val layoutParams = binding.quoteView.root.layoutParams as MarginLayoutParams
|
||||
val hasMedia = message.slideDeck.asAttachments().isNotEmpty()
|
||||
binding.quoteView.root.minWidth = if (hasMedia) 0 else toPx(300,context.resources)
|
||||
}
|
||||
|
||||
if (message is MmsMessageRecord) {
|
||||
message.slideDeck.asAttachments().forEach { attach ->
|
||||
val dbAttachment = attach as? DatabaseAttachment ?: return@forEach
|
||||
val attachmentId = dbAttachment.attachmentId.rowId
|
||||
if (attach.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
|
||||
&& MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) {
|
||||
// start download
|
||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, dbAttachment.mmsId))
|
||||
}
|
||||
}
|
||||
message.linkPreviews.forEach { preview ->
|
||||
val previewThumbnail = preview.getThumbnail().orNull() as? DatabaseAttachment ?: return@forEach
|
||||
val attachmentId = previewThumbnail.attachmentId.rowId
|
||||
if (previewThumbnail.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
|
||||
&& MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) {
|
||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, previewThumbnail.mmsId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
||||
@ -138,26 +164,26 @@ class VisibleMessageContentView : LinearLayout {
|
||||
hideBody = true
|
||||
// Audio attachment
|
||||
if (contactIsTrusted || message.isOutgoing) {
|
||||
binding.voiceMessageView.indexInAdapter = indexInAdapter
|
||||
binding.voiceMessageView.delegate = context as? ConversationActivityV2
|
||||
binding.voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
binding.voiceMessageView.root.indexInAdapter = indexInAdapter
|
||||
binding.voiceMessageView.root.delegate = context as? ConversationActivityV2
|
||||
binding.voiceMessageView.root.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
// We have to use onContentClick (rather than a click listener directly on the voice
|
||||
// message view) so as to not interfere with all the other gestures.
|
||||
onContentClick.add { binding.voiceMessageView.togglePlayback() }
|
||||
onContentDoubleTap = { binding.voiceMessageView.handleDoubleTap() }
|
||||
onContentClick.add { binding.voiceMessageView.root.togglePlayback() }
|
||||
onContentDoubleTap = { binding.voiceMessageView.root.handleDoubleTap() }
|
||||
} else {
|
||||
// TODO: move this out to its own area
|
||||
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
|
||||
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
|
||||
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
|
||||
hideBody = true
|
||||
// Document attachment
|
||||
if (contactIsTrusted || message.isOutgoing) {
|
||||
binding.documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
binding.documentView.root.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
} else {
|
||||
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
|
||||
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
|
||||
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
|
||||
/*
|
||||
@ -178,34 +204,21 @@ class VisibleMessageContentView : LinearLayout {
|
||||
} else {
|
||||
hideBody = true
|
||||
binding.albumThumbnailView.clearViews()
|
||||
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
||||
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
||||
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
} else if (message.isOpenGroupInvitation) {
|
||||
hideBody = true
|
||||
binding.openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
onContentClick.add { binding.openGroupInvitationView.joinOpenGroup() }
|
||||
binding.openGroupInvitationView.root.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
onContentClick.add { binding.openGroupInvitationView.root.joinOpenGroup() }
|
||||
}
|
||||
|
||||
binding.bodyTextView.isVisible = message.body.isNotEmpty() && !hideBody
|
||||
|
||||
// set it to use constraints if not only a text message, otherwise wrap content to whatever width it wants
|
||||
val params = binding.bodyTextView.layoutParams
|
||||
params.width = if (onlyBodyMessage || binding.barrierViewsGone()) ViewGroup.LayoutParams.WRAP_CONTENT else 0
|
||||
params.width = if (onlyBodyMessage || binding.barrierViewsGone()) ViewGroup.LayoutParams.MATCH_PARENT else 0
|
||||
binding.bodyTextView.layoutParams = params
|
||||
binding.bodyTextView.maxWidth = maxWidth
|
||||
|
||||
val bodyWidth = with (binding.bodyTextView) {
|
||||
StaticLayout.getDesiredWidth(text, paint).roundToInt()
|
||||
}
|
||||
|
||||
val quote = (message as? MmsMessageRecord)?.quote
|
||||
val quoteLayoutParams = binding.quoteView.layoutParams
|
||||
quoteLayoutParams.width =
|
||||
if (mediaThumbnailMessage || quote == null) 0
|
||||
else binding.quoteView.calculateWidth(quote, bodyWidth, maxWidth, thread)
|
||||
|
||||
binding.quoteView.layoutParams = quoteLayoutParams
|
||||
|
||||
if (message.body.isNotEmpty() && !hideBody) {
|
||||
val color = getTextColor(context, message)
|
||||
@ -222,7 +235,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
}
|
||||
|
||||
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
||||
listOf<View>(albumThumbnailView, linkPreviewView, voiceMessageView, quoteView).none { it.isVisible }
|
||||
listOf<View>(albumThumbnailView, linkPreviewView, voiceMessageView.root, quoteView.root).none { it.isVisible }
|
||||
|
||||
private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable {
|
||||
val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster)
|
||||
@ -245,20 +258,20 @@ class VisibleMessageContentView : LinearLayout {
|
||||
|
||||
fun recycle() {
|
||||
arrayOf(
|
||||
binding.deletedMessageView,
|
||||
binding.untrustedView,
|
||||
binding.voiceMessageView,
|
||||
binding.openGroupInvitationView,
|
||||
binding.documentView,
|
||||
binding.quoteView,
|
||||
binding.deletedMessageView.root,
|
||||
binding.untrustedView.root,
|
||||
binding.voiceMessageView.root,
|
||||
binding.openGroupInvitationView.root,
|
||||
binding.documentView.root,
|
||||
binding.quoteView.root,
|
||||
binding.linkPreviewView,
|
||||
binding.albumThumbnailView,
|
||||
binding.bodyTextView
|
||||
).forEach { view -> view.isVisible = false }
|
||||
).forEach { view: View -> view.isVisible = false }
|
||||
}
|
||||
|
||||
fun playVoiceMessage() {
|
||||
binding.voiceMessageView.togglePlayback()
|
||||
binding.voiceMessageView.root.togglePlayback()
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
@ -8,14 +8,12 @@ import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.os.bundleOf
|
||||
@ -23,6 +21,7 @@ import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
@ -31,7 +30,6 @@ import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
@ -43,7 +41,6 @@ import org.thoughtcrime.securesms.util.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.toDp
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
@ -54,13 +51,12 @@ import kotlin.math.sqrt
|
||||
class VisibleMessageView : LinearLayout {
|
||||
|
||||
@Inject lateinit var threadDb: ThreadDatabase
|
||||
@Inject lateinit var contactDb: SessionContactDatabase
|
||||
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
|
||||
@Inject lateinit var mmsSmsDb: MmsSmsDatabase
|
||||
@Inject lateinit var smsDb: SmsDatabase
|
||||
@Inject lateinit var mmsDb: MmsDatabase
|
||||
|
||||
private lateinit var binding: ViewVisibleMessageBinding
|
||||
private val binding by lazy { ViewVisibleMessageBinding.bind(this) }
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
||||
private val swipeToReplyIconRect = Rect()
|
||||
@ -75,7 +71,6 @@ class VisibleMessageView : LinearLayout {
|
||||
var snIsSelected = false
|
||||
set(value) {
|
||||
field = value
|
||||
binding.messageTimestampTextView.isVisible = isSelected
|
||||
handleIsSelectedChanged()
|
||||
}
|
||||
var onPress: ((event: MotionEvent) -> Unit)? = null
|
||||
@ -91,73 +86,84 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
initialize()
|
||||
}
|
||||
|
||||
private fun initialize() {
|
||||
binding = ViewVisibleMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
isHapticFeedbackEnabled = true
|
||||
setWillNotDraw(false)
|
||||
binding.expirationTimerViewContainer.disableClipping()
|
||||
binding.messageContentContainer.disableClipping()
|
||||
binding.messageContentView.disableClipping()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?, glide: GlideRequests, searchQuery: String?) {
|
||||
val sender = message.individualRecipient
|
||||
val senderSessionID = sender.address.serialize()
|
||||
fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?,
|
||||
glide: GlideRequests, searchQuery: String?, contact: Contact?, senderSessionID: String,
|
||||
) {
|
||||
val threadID = message.threadId
|
||||
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
||||
val contact = contactDb.getContactWithSessionID(senderSessionID)
|
||||
val isGroupThread = thread.isGroupRecipient
|
||||
val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread)
|
||||
val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread)
|
||||
// Show profile picture and sender name if this is a group thread AND
|
||||
// the message is incoming
|
||||
binding.moderatorIconImageView.isVisible = false
|
||||
binding.profilePictureView.root.visibility = when {
|
||||
thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE
|
||||
thread.isGroupRecipient -> View.INVISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
|
||||
val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing)
|
||||
else ViewUtil.dpToPx(context,2)
|
||||
|
||||
if (binding.profilePictureView.root.visibility == View.GONE) {
|
||||
val expirationParams = binding.expirationTimerViewContainer.layoutParams as MarginLayoutParams
|
||||
expirationParams.bottomMargin = bottomMargin
|
||||
binding.expirationTimerViewContainer.layoutParams = expirationParams
|
||||
} else {
|
||||
val avatarLayoutParams = binding.profilePictureView.root.layoutParams as MarginLayoutParams
|
||||
avatarLayoutParams.bottomMargin = bottomMargin
|
||||
binding.profilePictureView.root.layoutParams = avatarLayoutParams
|
||||
}
|
||||
|
||||
if (isGroupThread && !message.isOutgoing) {
|
||||
binding.profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
|
||||
binding.profilePictureView.publicKey = senderSessionID
|
||||
binding.profilePictureView.glide = glide
|
||||
binding.profilePictureView.update(message.individualRecipient)
|
||||
binding.profilePictureView.setOnClickListener {
|
||||
showUserDetails(senderSessionID, threadID)
|
||||
}
|
||||
if (thread.isOpenGroupRecipient) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||
val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server)
|
||||
binding.moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
|
||||
} else {
|
||||
binding.moderatorIconImageView.visibility = View.INVISIBLE
|
||||
if (isEndOfMessageCluster) {
|
||||
binding.profilePictureView.root.publicKey = senderSessionID
|
||||
binding.profilePictureView.root.glide = glide
|
||||
binding.profilePictureView.root.update(message.individualRecipient)
|
||||
binding.profilePictureView.root.setOnClickListener {
|
||||
showUserDetails(senderSessionID, threadID)
|
||||
}
|
||||
if (thread.isOpenGroupRecipient) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||
val isModerator = OpenGroupAPIV2.isUserModerator(
|
||||
senderSessionID,
|
||||
openGroup.room,
|
||||
openGroup.server
|
||||
)
|
||||
binding.moderatorIconImageView.isVisible = !message.isOutgoing && isModerator
|
||||
}
|
||||
}
|
||||
binding.senderNameTextView.isVisible = isStartOfMessageCluster
|
||||
val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||
val context =
|
||||
if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||
binding.senderNameTextView.text = contact?.displayName(context) ?: senderSessionID
|
||||
} else {
|
||||
binding.profilePictureContainer.visibility = View.GONE
|
||||
binding.senderNameTextView.visibility = View.GONE
|
||||
}
|
||||
// Date break
|
||||
binding.dateBreakTextView.showDateBreak(message, previous)
|
||||
// Timestamp
|
||||
binding.messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
|
||||
// Margins
|
||||
val startPadding = if (isGroupThread) {
|
||||
if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.very_large_spacing) else toPx(50,resources)
|
||||
} else {
|
||||
if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.very_large_spacing)
|
||||
else resources.getDimensionPixelSize(R.dimen.medium_spacing)
|
||||
}
|
||||
val endPadding = if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.medium_spacing)
|
||||
else resources.getDimensionPixelSize(R.dimen.very_large_spacing)
|
||||
binding.messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0)
|
||||
// binding.messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
|
||||
// Set inter-message spacing
|
||||
setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
// Gravity
|
||||
val gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
||||
binding.mainContainer.gravity = gravity or Gravity.BOTTOM
|
||||
// Message status indicator
|
||||
val (iconID, iconColor) = getMessageStatusImage(message)
|
||||
if (iconID != null) {
|
||||
@ -169,29 +175,29 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
if (message.isOutgoing) {
|
||||
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
||||
binding.messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
|
||||
binding.messageStatusImageView.isVisible =
|
||||
!message.isSent || message.id == lastMessageID
|
||||
} else {
|
||||
binding.messageStatusImageView.isVisible = false
|
||||
}
|
||||
// Expiration timer
|
||||
updateExpirationTimer(message)
|
||||
// Calculate max message bubble width
|
||||
var maxWidth = screenWidth - startPadding - endPadding
|
||||
if (binding.profilePictureContainer.visibility != View.GONE) { maxWidth -= binding.profilePictureContainer.width }
|
||||
// Populate content view
|
||||
binding.messageContentView.indexInAdapter = indexInAdapter
|
||||
binding.messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false))
|
||||
binding.messageContentView.bind(
|
||||
message,
|
||||
isStartOfMessageCluster,
|
||||
isEndOfMessageCluster,
|
||||
glide,
|
||||
thread,
|
||||
searchQuery,
|
||||
message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false)
|
||||
)
|
||||
binding.messageContentView.delegate = contentViewDelegate
|
||||
onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() }
|
||||
}
|
||||
|
||||
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||
val topPadding = if (isStartOfMessageCluster) R.dimen.conversation_vertical_message_spacing_default else R.dimen.conversation_vertical_message_spacing_collapse
|
||||
ViewUtil.setPaddingTop(this, resources.getDimension(topPadding).roundToInt())
|
||||
val bottomPadding = if (isEndOfMessageCluster) R.dimen.conversation_vertical_message_spacing_default else R.dimen.conversation_vertical_message_spacing_collapse
|
||||
ViewUtil.setPaddingBottom(this, resources.getDimension(bottomPadding).roundToInt())
|
||||
}
|
||||
|
||||
private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean {
|
||||
return if (isGroupThread) {
|
||||
previous == null || previous.isUpdate || !DateUtils.isSameHour(current.timestamp, previous.timestamp)
|
||||
@ -223,18 +229,17 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
private fun updateExpirationTimer(message: MessageRecord) {
|
||||
val expirationTimerViewLayoutParams = binding.expirationTimerView.layoutParams as MarginLayoutParams
|
||||
val container = binding.expirationTimerViewContainer
|
||||
val content = binding.messageContentView
|
||||
val expiration = binding.expirationTimerView
|
||||
val spacing = binding.messageContentSpacing
|
||||
container.removeAllViewsInLayout()
|
||||
container.addView(if (message.isOutgoing) expiration else content)
|
||||
container.addView(if (message.isOutgoing) content else expiration)
|
||||
val expirationTimerViewSize = toPx(12, resources)
|
||||
val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt()
|
||||
expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0
|
||||
expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize)
|
||||
binding.expirationTimerView.layoutParams = expirationTimerViewLayoutParams
|
||||
container.addView(spacing, if (message.isOutgoing) 0 else 2)
|
||||
val containerParams = container.layoutParams as ConstraintLayout.LayoutParams
|
||||
containerParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||
container.layoutParams = containerParams
|
||||
if (message.expiresIn > 0 && !message.isPending) {
|
||||
binding.expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
|
||||
binding.expirationTimerView.isVisible = true
|
||||
@ -279,9 +284,9 @@ class VisibleMessageView : LinearLayout {
|
||||
val threshold = swipeToReplyThreshold
|
||||
val iconSize = toPx(24, context.resources)
|
||||
val bottomVOffset = paddingBottom + binding.messageStatusImageView.height + (binding.messageContentView.height - iconSize) / 2
|
||||
swipeToReplyIconRect.left = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + spacing
|
||||
swipeToReplyIconRect.left = binding.messageContentView.right - binding.messageContentView.paddingEnd + spacing
|
||||
swipeToReplyIconRect.top = height - bottomVOffset - iconSize
|
||||
swipeToReplyIconRect.right = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + iconSize + spacing
|
||||
swipeToReplyIconRect.right = binding.messageContentView.right - binding.messageContentView.paddingEnd + iconSize + spacing
|
||||
swipeToReplyIconRect.bottom = height - bottomVOffset
|
||||
swipeToReplyIcon.bounds = swipeToReplyIconRect
|
||||
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
|
||||
@ -293,7 +298,7 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
binding.profilePictureView.recycle()
|
||||
binding.profilePictureView.root.recycle()
|
||||
binding.messageContentView.recycle()
|
||||
}
|
||||
// endregion
|
||||
|
@ -3,9 +3,7 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@ -21,12 +19,13 @@ import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
@AndroidEntryPoint
|
||||
class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener {
|
||||
|
||||
@Inject lateinit var attachmentDb: AttachmentDatabase
|
||||
|
||||
private lateinit var binding: ViewVoiceMessageBinding
|
||||
private val binding: ViewVoiceMessageBinding by lazy { ViewVoiceMessageBinding.bind(this) }
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var isPlaying = false
|
||||
set(value) {
|
||||
@ -40,16 +39,17 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
var indexInAdapter = -1
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
private fun initialize() {
|
||||
binding = ViewVoiceMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(0),
|
||||
TimeUnit.MILLISECONDS.toSeconds(0))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
|
@ -140,7 +140,7 @@ open class KThumbnailView: FrameLayout {
|
||||
val dimens = dimensDelegate.resourceSize()
|
||||
|
||||
val request = glide.load(DecryptableUri(slide.thumbnailUri!!))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.let { request ->
|
||||
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
|
||||
request.override(getDefaultWidth(), getDefaultHeight())
|
||||
|
@ -20,14 +20,26 @@ object MentionUtilities {
|
||||
|
||||
@JvmStatic
|
||||
fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String {
|
||||
return highlightMentions(text, false, threadID, context).toString() // isOutgoingMessage is irrelevant
|
||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
|
||||
return highlightMentions(text, false, isOpenGroup, context).toString() // isOutgoingMessage is irrelevant
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun highlightMentions(text:CharSequence, isOpenGroup: Boolean, context: Context): String {
|
||||
return highlightMentions(text, false, isOpenGroup, context).toString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString {
|
||||
@Suppress("NAME_SHADOWING") var text = text
|
||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
|
||||
return highlightMentions(text, isOutgoingMessage, isOpenGroup, context) // isOutgoingMessage is irrelevant
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, isOpenGroup: Boolean, context: Context): SpannableString {
|
||||
@Suppress("NAME_SHADOWING") var text = text
|
||||
val pattern = Pattern.compile("@[0-9a-fA-F]*")
|
||||
var matcher = pattern.matcher(text)
|
||||
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
|
||||
|
@ -349,7 +349,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(withCrossFade()), new CenterCrop());
|
||||
|
||||
if (slide.isInProgress()) return request;
|
||||
|
@ -0,0 +1,75 @@
|
||||
package org.thoughtcrime.securesms.crypto
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import org.session.libsession.utilities.Util
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.Signature
|
||||
|
||||
class BiometricSecretProvider {
|
||||
|
||||
companion object {
|
||||
private const val BIOMETRIC_ASYM_KEY_ALIAS = "Session-biometric-asym"
|
||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
private const val SIGNATURE_ALGORITHM = "SHA512withECDSA"
|
||||
}
|
||||
|
||||
fun getRandomData() = Util.getSecretBytes(32)
|
||||
|
||||
private fun createAsymmetricKey(context: Context) {
|
||||
val keyGenerator = KeyPairGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE
|
||||
)
|
||||
|
||||
val builder = KeyGenParameterSpec.Builder(BIOMETRIC_ASYM_KEY_ALIAS,
|
||||
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
||||
)
|
||||
.setDigests(
|
||||
KeyProperties.DIGEST_SHA256,
|
||||
KeyProperties.DIGEST_SHA512
|
||||
)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setUserAuthenticationValidityDurationSeconds(-1)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
builder.setUnlockedDeviceRequired(true)
|
||||
if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
|
||||
builder.setIsStrongBoxBacked(true)
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
builder.setInvalidatedByBiometricEnrollment(true)
|
||||
}
|
||||
keyGenerator.initialize(builder.build())
|
||||
keyGenerator.generateKeyPair()
|
||||
}
|
||||
|
||||
fun getOrCreateBiometricSignature(context: Context): Signature {
|
||||
val ks = KeyStore.getInstance(ANDROID_KEYSTORE)
|
||||
ks.load(null)
|
||||
if (!ks.containsAlias(BIOMETRIC_ASYM_KEY_ALIAS)) {
|
||||
createAsymmetricKey(context)
|
||||
}
|
||||
val key = ks.getKey(BIOMETRIC_ASYM_KEY_ALIAS, null) as PrivateKey
|
||||
val signature = Signature.getInstance(SIGNATURE_ALGORITHM)
|
||||
signature.initSign(key)
|
||||
return signature
|
||||
}
|
||||
|
||||
fun verifySignature(data: ByteArray, signedData: ByteArray): Boolean {
|
||||
val ks = KeyStore.getInstance(ANDROID_KEYSTORE)
|
||||
ks.load(null)
|
||||
val certificate = ks.getCertificate(BIOMETRIC_ASYM_KEY_ALIAS)
|
||||
val signature = Signature.getInstance(SIGNATURE_ALGORITHM)
|
||||
signature.initVerify(certificate)
|
||||
signature.update(data)
|
||||
return signature.verify(signedData)
|
||||
}
|
||||
}
|
@ -52,6 +52,7 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MmsAttachmentInfo;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
@ -67,6 +68,7 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
@ -266,6 +268,33 @@ public class AttachmentDatabase extends Database {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
void deleteAttachmentsForMessages(String[] messageIds) {
|
||||
StringBuilder queryBuilder = new StringBuilder();
|
||||
for (int i = 0; i < messageIds.length; i++) {
|
||||
queryBuilder.append(MMS_ID+" = ").append(messageIds[i]);
|
||||
if (i+1 < messageIds.length) {
|
||||
queryBuilder.append(" OR ");
|
||||
}
|
||||
}
|
||||
String idsAsString = queryBuilder.toString();
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
List<MmsAttachmentInfo> attachmentInfos = new ArrayList<>();
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, new String[] { DATA, THUMBNAIL, CONTENT_TYPE}, idsAsString, null, null, null, null);
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
attachmentInfos.add(new MmsAttachmentInfo(cursor.getString(0), cursor.getString(1), cursor.getString(2)));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
deleteAttachmentsOnDisk(attachmentInfos);
|
||||
database.delete(TABLE_NAME, idsAsString, null);
|
||||
notifyAttachmentListeners();
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
void deleteAttachmentsForMessage(long mmsId) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
@ -327,6 +356,30 @@ public class AttachmentDatabase extends Database {
|
||||
notifyAttachmentListeners();
|
||||
}
|
||||
|
||||
private void deleteAttachmentsOnDisk(List<MmsAttachmentInfo> mmsAttachmentInfos) {
|
||||
for (MmsAttachmentInfo info : mmsAttachmentInfos) {
|
||||
if (info.getDataFile() != null && !TextUtils.isEmpty(info.getDataFile())) {
|
||||
File data = new File(info.getDataFile());
|
||||
if (data.exists()) {
|
||||
data.delete();
|
||||
}
|
||||
}
|
||||
if (info.getThumbnailFile() != null && !TextUtils.isEmpty(info.getThumbnailFile())) {
|
||||
File thumbnail = new File(info.getThumbnailFile());
|
||||
if (thumbnail.exists()) {
|
||||
thumbnail.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean anyImageType = MmsAttachmentInfo.anyImages(mmsAttachmentInfos);
|
||||
boolean anyThumbnail = MmsAttachmentInfo.anyThumbnailNonNull(mmsAttachmentInfos);
|
||||
|
||||
if (anyImageType || anyThumbnail) {
|
||||
Glide.get(context).clearDiskCache();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private void deleteAttachmentOnDisk(@Nullable String data, @Nullable String thumbnail, @Nullable String contentType) {
|
||||
if (!TextUtils.isEmpty(data)) {
|
||||
|
@ -2,15 +2,13 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import org.session.libsession.utilities.Debouncer
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
|
||||
class ConversationNotificationDebouncer(private val context: Context) {
|
||||
private val threadIDs = mutableSetOf<Long>()
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val debouncer = Debouncer(handler, 250);
|
||||
private val handler = (context.applicationContext as ApplicationContext).conversationListNotificationHandler
|
||||
private val debouncer = Debouncer(handler, 1000)
|
||||
|
||||
companion object {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@ -29,7 +27,7 @@ class ConversationNotificationDebouncer(private val context: Context) {
|
||||
}
|
||||
|
||||
private fun publish() {
|
||||
for (threadID in threadIDs) {
|
||||
for (threadID in threadIDs.toList()) {
|
||||
context.contentResolver.notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadID), null)
|
||||
}
|
||||
threadIDs.clear()
|
||||
|
@ -23,7 +23,7 @@ import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.utilities.Debouncer;
|
||||
import org.session.libsession.utilities.WindowDebouncer;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
@ -32,16 +32,21 @@ import java.util.Set;
|
||||
public abstract class Database {
|
||||
|
||||
protected static final String ID_WHERE = "_id = ?";
|
||||
protected static final String ID_IN = "_id IN (?)";
|
||||
|
||||
protected SQLCipherOpenHelper databaseHelper;
|
||||
protected final Context context;
|
||||
private final Debouncer conversationListNotificationDebouncer;
|
||||
private final WindowDebouncer conversationListNotificationDebouncer;
|
||||
private final Runnable conversationListUpdater;
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
public Database(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
this.context = context;
|
||||
this.conversationListUpdater = () -> {
|
||||
context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
|
||||
};
|
||||
this.databaseHelper = databaseHelper;
|
||||
this.conversationListNotificationDebouncer = new Debouncer(ApplicationContext.getInstance(context).getConversationListNotificationHandler(), 250);
|
||||
this.conversationListNotificationDebouncer = ApplicationContext.getInstance(context).getConversationListDebouncer();
|
||||
}
|
||||
|
||||
protected void notifyConversationListeners(Set<Long> threadIds) {
|
||||
@ -54,7 +59,7 @@ public abstract class Database {
|
||||
}
|
||||
|
||||
protected void notifyConversationListListeners() {
|
||||
conversationListNotificationDebouncer.publish(()->context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null));
|
||||
conversationListNotificationDebouncer.publish(conversationListUpdater);
|
||||
}
|
||||
|
||||
protected void notifyStickerListeners() {
|
||||
|
@ -4,6 +4,7 @@ import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@ -41,7 +42,7 @@ public class DatabaseContentProviders {
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -36,11 +36,12 @@ fun <T> SQLiteDatabase.getAll(table: String, query: String?, arguments: Array<St
|
||||
return listOf()
|
||||
}
|
||||
|
||||
fun SQLiteDatabase.insertOrUpdate(table: String, values: ContentValues, query: String, arguments: Array<String>) {
|
||||
fun SQLiteDatabase.insertOrUpdate(table: String, values: ContentValues, query: String, arguments: Array<String>): Int {
|
||||
val id = insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE).toInt()
|
||||
if (id == -1) {
|
||||
update(table, values, query, arguments)
|
||||
return update(table, values, query, arguments)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
fun Cursor.getInt(columnName: String): Int {
|
||||
|
@ -243,6 +243,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
recipient.setParticipants(Stream.of(members).map(memberAddress -> Recipient.from(context, memberAddress, true)).toList());
|
||||
});
|
||||
|
||||
notifyConversationListeners(threadId);
|
||||
notifyConversationListListeners();
|
||||
return threadId;
|
||||
}
|
||||
@ -314,6 +315,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
new String[] {groupID});
|
||||
|
||||
Recipient.applyCached(Address.fromSerialized(groupID), recipient -> recipient.setGroupAvatarId(avatarId == 0 ? null : avatarId));
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public void updateMembers(String groupId, List<Address> members) {
|
||||
|
@ -4,13 +4,13 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@ -92,6 +92,19 @@ public class GroupReceiptDatabase extends Database {
|
||||
return results;
|
||||
}
|
||||
|
||||
void deleteRowsForMessages(String[] mmsIds) {
|
||||
StringBuilder queryBuilder = new StringBuilder();
|
||||
for (int i = 0; i < mmsIds.length; i++) {
|
||||
queryBuilder.append(MMS_ID+" = ").append(mmsIds[i]);
|
||||
if (i+1 < mmsIds.length) {
|
||||
queryBuilder.append(" OR ");
|
||||
}
|
||||
}
|
||||
String idsAsString = queryBuilder.toString();
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, idsAsString, null);
|
||||
}
|
||||
|
||||
void deleteRowsForMessage(long mmsId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});
|
||||
|
@ -265,7 +265,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
override fun getLastMessageHashValue(snode: Snode, publicKey: String, namespace: Int): String? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ? AND $lastMessageHashNamespace = ?"
|
||||
return database.get(lastMessageHashValueTable2, query, arrayOf( snode.toString(), publicKey, namespace.toString() )) { cursor ->
|
||||
return database.get(lastMessageHashValueTable2, query, arrayOf(snode.toString(), publicKey, namespace.toString())) { cursor ->
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(lastMessageHashValue))
|
||||
}
|
||||
}
|
||||
@ -279,7 +279,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
lastMessageHashNamespace to namespace.toString()
|
||||
))
|
||||
val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ? AND $lastMessageHashNamespace = ?"
|
||||
database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey, namespace.toString() ))
|
||||
val lastHash = database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey, namespace.toString() ))
|
||||
}
|
||||
|
||||
override fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set<String>? {
|
||||
|
@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.database
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
|
||||
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
||||
|
||||
@ -77,6 +77,9 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
||||
database.endTransaction()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return pair of sms or mms table-specific ID and whether it is in SMS table
|
||||
*/
|
||||
fun getMessageID(serverID: Long, threadID: Long): Pair<Long, Boolean>? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val mappingResult = database.get(messageThreadMappingTable, "${Companion.serverID} = ? AND ${Companion.threadID} = ?",
|
||||
|
File diff suppressed because it is too large
Load Diff
1607
app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
Normal file
1607
app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@ import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import net.sqlcipher.Cursor
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
||||
import org.session.libsession.messaging.jobs.Job
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
@ -135,6 +136,13 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
job.failureCount = cursor.getInt(failureCount)
|
||||
return job
|
||||
}
|
||||
|
||||
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(BackgroundGroupAddJob.KEY)) { cursor ->
|
||||
jobFromCursor(cursor) as? BackgroundGroupAddJob
|
||||
}.filterNotNull().any { it.joinUrl == groupJoinUrl }
|
||||
}
|
||||
}
|
||||
|
||||
object SessionJobHelper {
|
||||
|
@ -362,7 +362,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
return new Pair<>(messageId, threadId);
|
||||
}
|
||||
|
||||
protected Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp) {
|
||||
protected Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
|
||||
if (message.isSecureMessage()) {
|
||||
type |= Types.SECURE_MESSAGE_BIT;
|
||||
} else if (message.isGroup()) {
|
||||
@ -440,11 +440,13 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long messageId = db.insert(TABLE_NAME, null, values);
|
||||
|
||||
if (unread) {
|
||||
if (unread && runIncrement) {
|
||||
DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1);
|
||||
}
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
|
||||
if (runThreadUpdate) {
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
|
||||
}
|
||||
|
||||
if (message.getSubscriptionId() != -1) {
|
||||
DatabaseComponent.get(context).recipientDatabase().setDefaultSubscriptionId(recipient, message.getSubscriptionId());
|
||||
@ -456,23 +458,23 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0);
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, boolean runIncrement, boolean runThreadUpdate) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runIncrement, runThreadUpdate);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertCallMessage(IncomingTextMessage message) {
|
||||
return insertMessageInbox(message, 0, 0);
|
||||
return insertMessageInbox(message, 0, 0, true, true);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp);
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runIncrement, runThreadUpdate);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp) {
|
||||
public Optional<InsertResult> insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp, boolean runThreadUpdate) {
|
||||
if (threadId == -1) {
|
||||
threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(message.getRecipient());
|
||||
}
|
||||
long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, null);
|
||||
long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, null, runThreadUpdate);
|
||||
if (messageId == -1) {
|
||||
return Optional.absent();
|
||||
}
|
||||
@ -481,7 +483,8 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
|
||||
public long insertMessageOutbox(long threadId, OutgoingTextMessage message,
|
||||
boolean forceSms, long date, InsertListener insertListener)
|
||||
boolean forceSms, long date, InsertListener insertListener,
|
||||
boolean runThreadUpdate)
|
||||
{
|
||||
long type = Types.BASE_SENDING_TYPE;
|
||||
|
||||
@ -517,7 +520,9 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
insertListener.onComplete();
|
||||
}
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
|
||||
if (runThreadUpdate) {
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
|
||||
}
|
||||
DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId);
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true);
|
||||
|
@ -11,7 +11,6 @@ import org.session.libsession.messaging.jobs.Job
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.jobs.TrimThreadJob
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage
|
||||
@ -102,7 +101,29 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return database.getAttachmentsForMessage(messageID)
|
||||
}
|
||||
|
||||
override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? {
|
||||
override fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean) {
|
||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||
threadDb.setRead(threadId, updateLastSeen)
|
||||
}
|
||||
|
||||
override fun incrementUnread(threadId: Long, amount: Int) {
|
||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||
threadDb.incrementUnread(threadId, amount)
|
||||
}
|
||||
|
||||
override fun updateThread(threadId: Long, unarchive: Boolean) {
|
||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||
threadDb.update(threadId, unarchive)
|
||||
}
|
||||
|
||||
override fun persist(message: VisibleMessage,
|
||||
quotes: QuoteModel?,
|
||||
linkPreview: List<LinkPreview?>,
|
||||
groupPublicKey: String?,
|
||||
openGroupID: String?,
|
||||
attachments: List<Attachment>,
|
||||
runIncrement: Boolean,
|
||||
runThreadUpdate: Boolean): Long? {
|
||||
var messageID: Long? = null
|
||||
val senderAddress = Address.fromSerialized(message.sender!!)
|
||||
val isUserSender = (message.sender!! == getUserPublicKey())
|
||||
@ -139,14 +160,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
|
||||
val insertResult = if (message.sender == getUserPublicKey()) {
|
||||
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
|
||||
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
|
||||
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!, runThreadUpdate)
|
||||
} else {
|
||||
// It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment
|
||||
val signalServiceAttachments = attachments.mapNotNull {
|
||||
it.toSignalPointer()
|
||||
}
|
||||
val mediaMessage = IncomingMediaMessage.from(message, senderAddress, targetRecipient.expireMessages * 1000L, group, signalServiceAttachments, quote, linkPreviews)
|
||||
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.receivedTimestamp ?: 0)
|
||||
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate)
|
||||
}
|
||||
if (insertResult.isPresent) {
|
||||
messageID = insertResult.get().messageId
|
||||
@ -158,12 +179,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val insertResult = if (message.sender == getUserPublicKey()) {
|
||||
val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, targetRecipient, message.sentTimestamp)
|
||||
else OutgoingTextMessage.from(message, targetRecipient)
|
||||
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!)
|
||||
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!, runThreadUpdate)
|
||||
} else {
|
||||
val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, senderAddress, message.sentTimestamp)
|
||||
else IncomingTextMessage.from(message, senderAddress, group, targetRecipient.expireMessages * 1000L)
|
||||
val encrypted = IncomingEncryptedMessage(textMessage, textMessage.messageBody)
|
||||
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0)
|
||||
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate)
|
||||
}
|
||||
insertResult.orNull()?.let { result ->
|
||||
messageID = result.messageId
|
||||
@ -171,8 +192,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
}
|
||||
val threadID = message.threadID
|
||||
// open group trim thread job is scheduled after processing in OpenGroupPollerV2
|
||||
if (openGroupID.isNullOrEmpty() && threadID != null && threadID >= 0) {
|
||||
JobQueue.shared.add(TrimThreadJob(threadID))
|
||||
if (openGroupID.isNullOrEmpty() && threadID != null && threadID >= 0 && TextSecurePreferences.isThreadLengthTrimmingEnabled(context)) {
|
||||
JobQueue.shared.queueThreadForTrim(threadID)
|
||||
}
|
||||
message.serverHash?.let { serverHash ->
|
||||
messageID?.let { id ->
|
||||
@ -436,7 +457,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
|
||||
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
|
||||
val smsDB = DatabaseComponent.get(context).smsDatabase()
|
||||
smsDB.insertMessageInbox(infoMessage)
|
||||
smsDB.insertMessageInbox(infoMessage, true, true)
|
||||
}
|
||||
|
||||
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) {
|
||||
@ -448,7 +469,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val mmsDB = DatabaseComponent.get(context).mmsDatabase()
|
||||
val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||
if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true)
|
||||
mmsDB.markAsSent(infoMessageID, true)
|
||||
}
|
||||
|
||||
@ -519,6 +540,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
OpenGroupManager.addOpenGroup(urlAsString, context)
|
||||
}
|
||||
|
||||
override fun onOpenGroupAdded(urlAsString: String) {
|
||||
val server = OpenGroupV2.getServer(urlAsString)
|
||||
OpenGroupManager.restartPollerForServer(server.toString().removeSuffix("/"))
|
||||
}
|
||||
|
||||
override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean {
|
||||
val jobDb = DatabaseComponent.get(context).sessionJobDatabase()
|
||||
return jobDb.hasBackgroundGroupAddJob(groupJoinUrl)
|
||||
}
|
||||
|
||||
override fun setProfileSharing(address: Address, value: Boolean) {
|
||||
val recipient = Recipient.from(context, address, false)
|
||||
DatabaseComponent.get(context).recipientDatabase().setProfileSharing(recipient, value)
|
||||
@ -667,7 +698,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
Optional.of(message)
|
||||
)
|
||||
|
||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1)
|
||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1, runIncrement = true, runThreadUpdate = true)
|
||||
}
|
||||
|
||||
override fun insertMessageRequestResponse(response: MessageRequestResponse) {
|
||||
@ -705,7 +736,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
Optional.absent()
|
||||
)
|
||||
val threadId = getOrCreateThreadIdFor(senderAddress)
|
||||
mmsDb.insertSecureDecryptedMessageInbox(message, threadId)
|
||||
mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runIncrement = true, runThreadUpdate = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -574,6 +574,17 @@ public class ThreadDatabase extends Database {
|
||||
return getOrCreateThreadIdFor(recipient, DistributionTypes.DEFAULT);
|
||||
}
|
||||
|
||||
public void setThreadArchived(long threadId) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(ARCHIVED, 1);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE,
|
||||
new String[] {String.valueOf(threadId)});
|
||||
|
||||
notifyConversationListListeners();
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
public long getOrCreateThreadIdFor(Recipient recipient, int distributionType) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = ADDRESS + " = ?";
|
||||
|
@ -10,6 +10,7 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteDatabaseHook;
|
||||
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
@ -86,6 +87,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
public void postKey(SQLiteDatabase db) {
|
||||
db.rawExecSQL("PRAGMA kdf_iter = '1';");
|
||||
db.rawExecSQL("PRAGMA cipher_page_size = 4096;");
|
||||
// if not vacuumed in a while, perform that operation
|
||||
long currentTime = System.currentTimeMillis();
|
||||
// 7 days
|
||||
if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) {
|
||||
db.rawExecSQL("VACUUM;");
|
||||
TextSecurePreferences.setLastVacuumNow(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -144,7 +152,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(GroupDatabase.getCreateUpdatedTimestampCommand());
|
||||
db.execSQL(RecipientDatabase.getCreateApprovedCommand());
|
||||
db.execSQL(RecipientDatabase.getCreateApprovedMeCommand());
|
||||
db.execSQL(MmsDatabase.getCreateMessageRequestResponseCommand());
|
||||
db.execSQL(MmsDatabase.createMessageRequestResponseCommand);
|
||||
db.execSQL(LokiAPIDatabase.CREATE_FORK_INFO_TABLE_COMMAND);
|
||||
db.execSQL(LokiAPIDatabase.CREATE_DEFAULT_FORK_INFO_COMMAND);
|
||||
db.execSQL(LokiAPIDatabase.UPDATE_HASHES_INCLUDE_NAMESPACE_COMMAND);
|
||||
@ -339,7 +347,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(RecipientDatabase.getCreateApprovedCommand());
|
||||
db.execSQL(RecipientDatabase.getCreateApprovedMeCommand());
|
||||
db.execSQL(RecipientDatabase.getUpdateApprovedCommand());
|
||||
db.execSQL(MmsDatabase.getCreateMessageRequestResponseCommand());
|
||||
db.execSQL(MmsDatabase.createMessageRequestResponseCommand);
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV32) {
|
||||
|
@ -46,8 +46,10 @@ public class PagingMediaLoader extends AsyncLoader<Pair<Cursor, Integer>> {
|
||||
return new Pair<>(cursor, leftIsRecent ? cursor.getPosition() : cursor.getCount() - 1 - cursor.getPosition());
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
|
||||
data class MmsAttachmentInfo(val dataFile: String?, val thumbnailFile: String?, val contentType: String?) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun List<MmsAttachmentInfo>.anyImages() = any {
|
||||
MediaUtil.isImageType(it.contentType)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun List<MmsAttachmentInfo>.anyThumbnailNonNull() = any {
|
||||
it.thumbnailFile?.isNotEmpty() == true
|
||||
}
|
||||
}
|
||||
}
|
@ -27,8 +27,8 @@ import android.text.style.StyleSpan;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.ExpirationUtil;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
|
||||
@ -74,7 +74,6 @@ public class ThreadRecord extends DisplayRecord {
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
Recipient recipient = getRecipient();
|
||||
if (isGroupUpdateMessage()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
||||
} else if (isOpenGroupInvitation()) {
|
||||
|
@ -4,18 +4,15 @@ package org.thoughtcrime.securesms.giph.ui;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
|
||||
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
@ -26,19 +23,20 @@ import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.bumptech.glide.util.ByteBufferUtil;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import org.session.libsession.utilities.MaterialColor;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.ViewUtil;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
|
||||
class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
||||
|
||||
@ -154,12 +152,12 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
||||
|
||||
RequestBuilder<Drawable> thumbnailRequest = GlideApp.with(context)
|
||||
.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL);
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE);
|
||||
|
||||
if (org.thoughtcrime.securesms.util.Util.isLowMemory(context)) {
|
||||
glideRequests.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
|
||||
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.listener(holder)
|
||||
.into(holder.thumbnail);
|
||||
@ -169,7 +167,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
||||
glideRequests.load(new ChunkedImageUrl(image.getGifUrl(), image.getGifSize()))
|
||||
.thumbnail(thumbnailRequest)
|
||||
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.listener(holder)
|
||||
.into(holder.thumbnail);
|
||||
|
@ -0,0 +1,34 @@
|
||||
package org.thoughtcrime.securesms.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
|
||||
|
||||
class PlaceholderAvatarFetcher(private val context: Context,
|
||||
private val photo: PlaceholderAvatarPhoto): DataFetcher<BitmapDrawable> {
|
||||
|
||||
override fun loadData(priority: Priority,callback: DataFetcher.DataCallback<in BitmapDrawable>) {
|
||||
try {
|
||||
val avatar = AvatarPlaceholderGenerator.generate(context, 128, photo.hashString, photo.displayName)
|
||||
callback.onDataReady(avatar)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Error in fetching avatar")
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {}
|
||||
|
||||
override fun cancel() {}
|
||||
|
||||
override fun getDataClass(): Class<BitmapDrawable> {
|
||||
return BitmapDrawable::class.java
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource = DataSource.LOCAL
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.ModelLoader.LoadData
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
||||
|
||||
class PlaceholderAvatarLoader(private val context: Context): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
|
||||
override fun buildLoadData(
|
||||
model: PlaceholderAvatarPhoto,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): LoadData<BitmapDrawable> {
|
||||
return LoadData(model, PlaceholderAvatarFetcher(context, model))
|
||||
}
|
||||
|
||||
override fun handles(model: PlaceholderAvatarPhoto): Boolean = true
|
||||
|
||||
class Factory(private val context: Context) : ModelLoaderFactory<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
return PlaceholderAvatarLoader(context)
|
||||
}
|
||||
override fun teardown() {}
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ public class GroupManager {
|
||||
}
|
||||
|
||||
public static long getThreadIDFromGroupID(String groupID, @NonNull Context context) {
|
||||
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupID), false);
|
||||
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupID), true);
|
||||
return DatabaseComponent.get(context).threadDatabase().getThreadIdIfExistsFor(groupRecipient);
|
||||
}
|
||||
|
||||
@ -59,6 +59,7 @@ public class GroupManager {
|
||||
|
||||
long threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(
|
||||
groupRecipient, DistributionTypes.CONVERSATION);
|
||||
DatabaseComponent.get(context).threadDatabase().setThreadArchived(threadID);
|
||||
return new GroupActionResult(groupRecipient, threadID);
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
|
||||
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
@ -101,6 +102,7 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
||||
val sanitizedServer = server.toString().removeSuffix("/")
|
||||
val openGroupID = "$sanitizedServer.${room!!}"
|
||||
OpenGroupManager.add(sanitizedServer, room, publicKey!!, this@JoinPublicChatActivity)
|
||||
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(stringWithExplicitScheme)
|
||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, this@JoinPublicChatActivity)
|
||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||
threadID to groupID
|
||||
|
@ -7,16 +7,15 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
object OpenGroupManager {
|
||||
private val executorService = Executors.newScheduledThreadPool(4)
|
||||
private var pollers = mutableMapOf<String, OpenGroupPollerV2>() // One for each server
|
||||
private var isPolling = false
|
||||
private val pollUpdaterLock = Any()
|
||||
|
||||
val isAllCaughtUp: Boolean
|
||||
get() {
|
||||
@ -49,8 +48,11 @@ object OpenGroupManager {
|
||||
}
|
||||
|
||||
fun stopPolling() {
|
||||
pollers.forEach { it.value.stop() }
|
||||
pollers.clear()
|
||||
synchronized(pollUpdaterLock) {
|
||||
pollers.forEach { it.value.stop() }
|
||||
pollers.clear()
|
||||
isPolling = false
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@ -67,7 +69,7 @@ object OpenGroupManager {
|
||||
storage.removeLastMessageServerID(room, server)
|
||||
// Store the public key
|
||||
storage.setOpenGroupPublicKey(server,publicKey)
|
||||
// Get an auth token
|
||||
// Get group info
|
||||
OpenGroupAPIV2.getAuthToken(room, server).get()
|
||||
// Get group info
|
||||
val info = OpenGroupAPIV2.getInfo(room, server).get()
|
||||
@ -77,11 +79,17 @@ object OpenGroupManager {
|
||||
}
|
||||
val openGroup = OpenGroupV2(server, room, info.name, publicKey)
|
||||
threadDB.setOpenGroupChat(openGroup, threadID)
|
||||
}
|
||||
|
||||
fun restartPollerForServer(server: String) {
|
||||
// Start the poller if needed
|
||||
pollers[server]?.startIfNeeded() ?: run {
|
||||
val poller = OpenGroupPollerV2(server, executorService)
|
||||
Util.runOnMain { poller.startIfNeeded() }
|
||||
pollers[server] = poller
|
||||
synchronized(pollUpdaterLock) {
|
||||
pollers[server]?.stop()
|
||||
pollers[server]?.startIfNeeded() ?: run {
|
||||
val poller = OpenGroupPollerV2(server, executorService)
|
||||
pollers[server] = poller
|
||||
poller.startIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,13 +99,16 @@ object OpenGroupManager {
|
||||
val openGroupID = "$server.$room"
|
||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
||||
val recipient = threadDB.getRecipientForThreadId(threadID) ?: return
|
||||
threadDB.setThreadArchived(threadID)
|
||||
val groupID = recipient.address.serialize()
|
||||
// Stop the poller if needed
|
||||
val openGroups = storage.getAllV2OpenGroups().filter { it.value.server == server }
|
||||
if (openGroups.count() == 1) {
|
||||
val poller = pollers[server]
|
||||
poller?.stop()
|
||||
pollers.remove(server)
|
||||
synchronized(pollUpdaterLock) {
|
||||
val poller = pollers[server]
|
||||
poller?.stop()
|
||||
pollers.remove(server)
|
||||
}
|
||||
}
|
||||
// Delete
|
||||
storage.removeLastDeletionServerID(room, server)
|
||||
@ -112,12 +123,7 @@ object OpenGroupManager {
|
||||
|
||||
fun addOpenGroup(urlAsString: String, context: Context) {
|
||||
val url = HttpUrl.parse(urlAsString) ?: return
|
||||
val builder = HttpUrl.Builder().scheme(url.scheme()).host(url.host())
|
||||
if (url.port() != 80 || url.port() != 443) {
|
||||
// Non-standard port; add to server
|
||||
builder.port(url.port())
|
||||
}
|
||||
val server = builder.build()
|
||||
val server = OpenGroupV2.getServer(urlAsString)
|
||||
val room = url.pathSegments().firstOrNull() ?: return
|
||||
val publicKey = url.queryParameter("public_key") ?: return
|
||||
add(server.toString().removeSuffix("/"), room, publicKey, context)
|
||||
|
@ -15,7 +15,8 @@ import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewConversationBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_ALL
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_NONE
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
@ -47,7 +48,7 @@ class ConversationView : LinearLayout {
|
||||
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
||||
}
|
||||
binding.profilePictureView.glide = glide
|
||||
binding.profilePictureView.root.glide = glide
|
||||
val unreadCount = thread.unreadCount
|
||||
if (thread.recipient.isBlocked) {
|
||||
binding.accentView.setBackgroundResource(R.color.destructive)
|
||||
@ -73,15 +74,15 @@ class ConversationView : LinearLayout {
|
||||
binding.conversationViewDisplayNameTextView.text = senderDisplayName
|
||||
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||
val recipient = thread.recipient
|
||||
binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL
|
||||
val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
|
||||
binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != NOTIFY_TYPE_ALL
|
||||
val drawableRes = if (recipient.isMuted || recipient.notifyType == NOTIFY_TYPE_NONE) {
|
||||
R.drawable.ic_outline_notifications_off_24
|
||||
} else {
|
||||
R.drawable.ic_notifications_mentions
|
||||
}
|
||||
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
||||
val rawSnippet = thread.getDisplayBody(context)
|
||||
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
|
||||
val snippet = highlightMentions(rawSnippet, recipient.isOpenGroupRecipient, context)
|
||||
binding.snippetTextView.text = snippet
|
||||
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||
@ -103,13 +104,11 @@ class ConversationView : LinearLayout {
|
||||
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
||||
}
|
||||
post {
|
||||
binding.profilePictureView.update(thread.recipient)
|
||||
}
|
||||
binding.profilePictureView.root.update(thread.recipient)
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
binding.profilePictureView.recycle()
|
||||
binding.profilePictureView.root.recycle()
|
||||
}
|
||||
|
||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
||||
|
@ -5,18 +5,15 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.database.Cursor
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -73,7 +70,6 @@ import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.show
|
||||
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
@ -83,7 +79,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
ConversationClickListener,
|
||||
SeedReminderViewDelegate,
|
||||
NewConversationButtonSetViewDelegate,
|
||||
LoaderManager.LoaderCallbacks<Cursor>,
|
||||
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
||||
|
||||
private lateinit var binding: ActivityHomeBinding
|
||||
@ -97,12 +92,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
@Inject lateinit var textSecurePreferences: TextSecurePreferences
|
||||
|
||||
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
|
||||
private val homeViewModel by viewModels<HomeViewModel>()
|
||||
|
||||
private val publicKey: String
|
||||
get() = textSecurePreferences.getLocalNumber()!!
|
||||
|
||||
private val homeAdapter: HomeAdapter by lazy {
|
||||
HomeAdapter(context = this, cursor = threadDb.approvedConversationList, listener = this)
|
||||
private val homeAdapter: NewHomeAdapter by lazy {
|
||||
NewHomeAdapter(context = this, listener = this)
|
||||
}
|
||||
|
||||
private val globalSearchAdapter = GlobalSearchAdapter { model ->
|
||||
@ -156,8 +152,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
// Set up Glide
|
||||
glide = GlideApp.with(this)
|
||||
// Set up toolbar buttons
|
||||
binding.profileButton.glide = glide
|
||||
binding.profileButton.setOnClickListener { openSettings() }
|
||||
binding.profileButton.root.glide = glide
|
||||
binding.profileButton.root.setOnClickListener { openSettings() }
|
||||
binding.searchViewContainer.setOnClickListener {
|
||||
binding.globalSearchInputLayout.requestFocus()
|
||||
}
|
||||
@ -184,8 +180,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
// Set up empty state view
|
||||
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
homeViewModel.getObservable(this).observe(this) { newData ->
|
||||
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
|
||||
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
|
||||
val offsetTop = if(firstPos >= 0) {
|
||||
manager.findViewByPosition(firstPos)?.let { view ->
|
||||
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
|
||||
} ?: 0
|
||||
} else 0
|
||||
homeAdapter.data = newData
|
||||
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
|
||||
setupMessageRequestsBanner()
|
||||
updateEmptyState()
|
||||
}
|
||||
homeViewModel.tryUpdateChannel()
|
||||
// Set up new conversation button set
|
||||
binding.newConversationButtonSet.delegate = this
|
||||
// Observe blocked contacts changed events
|
||||
@ -202,17 +210,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
// Double check that the long poller is up
|
||||
(applicationContext as ApplicationContext).startPollingIfNeeded()
|
||||
// update things based on TextSecurePrefs (profile info etc)
|
||||
// Set up typing observer
|
||||
withContext(Dispatchers.Main) {
|
||||
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this@HomeActivity, Observer<Set<Long>> { threadIDs ->
|
||||
val adapter = binding.recyclerView.adapter as HomeAdapter
|
||||
adapter.typingThreadIDs = threadIDs ?: setOf()
|
||||
})
|
||||
updateProfileButton()
|
||||
TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect {
|
||||
updateProfileButton()
|
||||
}
|
||||
}
|
||||
// Set up remaining components if needed
|
||||
val application = ApplicationContext.getInstance(this@HomeActivity)
|
||||
application.registerForFCMIfNeeded(false)
|
||||
@ -220,6 +217,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
OpenGroupManager.startPolling()
|
||||
JobQueue.shared.resumePendingJobs()
|
||||
}
|
||||
// Set up typing observer
|
||||
withContext(Dispatchers.Main) {
|
||||
updateProfileButton()
|
||||
TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect {
|
||||
updateProfileButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
// monitor the global search VM query
|
||||
launch {
|
||||
@ -293,7 +297,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
binding.searchToolbar.isVisible = isShown
|
||||
binding.sessionToolbar.isVisible = !isShown
|
||||
binding.recyclerView.isVisible = !isShown
|
||||
binding.emptyStateContainer.isVisible = (binding.recyclerView.adapter as HomeAdapter).itemCount == 0 && binding.recyclerView.isVisible
|
||||
binding.emptyStateContainer.isVisible = (binding.recyclerView.adapter as NewHomeAdapter).itemCount == 0 && binding.recyclerView.isVisible
|
||||
binding.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown
|
||||
binding.gradientView.isVisible = !isShown
|
||||
binding.globalSearchRecycler.isVisible = isShown
|
||||
@ -314,35 +318,27 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
root.setOnClickListener { showMessageRequests() }
|
||||
root.setOnLongClickListener { hideMessageRequests(); true }
|
||||
root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
homeAdapter.headerView = root
|
||||
homeAdapter.notifyItemChanged(0)
|
||||
val hadHeader = homeAdapter.hasHeaderView()
|
||||
homeAdapter.header = root
|
||||
if (hadHeader) homeAdapter.notifyItemChanged(0)
|
||||
else homeAdapter.notifyItemInserted(0)
|
||||
}
|
||||
} else {
|
||||
homeAdapter.headerView = null
|
||||
val hadHeader = homeAdapter.hasHeaderView()
|
||||
homeAdapter.header = null
|
||||
if (hadHeader) {
|
||||
homeAdapter.notifyItemRemoved(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
|
||||
return HomeLoader(this@HomeActivity)
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
|
||||
homeAdapter.changeCursor(cursor)
|
||||
setupMessageRequestsBanner()
|
||||
updateEmptyState()
|
||||
}
|
||||
|
||||
override fun onLoaderReset(cursor: Loader<Cursor>) {
|
||||
homeAdapter.changeCursor(null)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
|
||||
if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared
|
||||
IdentityKeyUtil.checkUpdate(this)
|
||||
binding.profileButton.recycle() // clear cached image before update tje profilePictureView
|
||||
binding.profileButton.update()
|
||||
binding.profileButton.root.recycle() // clear cached image before update tje profilePictureView
|
||||
binding.profileButton.root.update()
|
||||
if (textSecurePreferences.getHasViewedSeed()) {
|
||||
binding.seedReminderView.isVisible = false
|
||||
}
|
||||
@ -377,7 +373,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
|
||||
// region Updating
|
||||
private fun updateEmptyState() {
|
||||
val threadCount = (binding.recyclerView.adapter as HomeAdapter).itemCount
|
||||
val threadCount = (binding.recyclerView.adapter)!!.itemCount
|
||||
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
||||
}
|
||||
|
||||
@ -385,14 +381,16 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
fun onUpdateProfileEvent(event: ProfilePictureModifiedEvent) {
|
||||
if (event.recipient.isLocalNumber) {
|
||||
updateProfileButton()
|
||||
} else {
|
||||
homeViewModel.tryUpdateChannel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfileButton() {
|
||||
binding.profileButton.publicKey = publicKey
|
||||
binding.profileButton.displayName = textSecurePreferences.getProfileName()
|
||||
binding.profileButton.recycle()
|
||||
binding.profileButton.update()
|
||||
binding.profileButton.root.publicKey = publicKey
|
||||
binding.profileButton.root.displayName = textSecurePreferences.getProfileName()
|
||||
binding.profileButton.root.recycle()
|
||||
binding.profileButton.root.update()
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -534,9 +532,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
threadDb.setPinned(threadId, pinned)
|
||||
withContext(Dispatchers.Main) {
|
||||
LoaderManager.getInstance(this@HomeActivity).restartLoader(0, null, this@HomeActivity)
|
||||
}
|
||||
homeViewModel.tryUpdateChannel()
|
||||
}
|
||||
}
|
||||
|
||||
@ -608,11 +604,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
show(intent, isForResult = true)
|
||||
}
|
||||
|
||||
private fun showPath() {
|
||||
val intent = Intent(this, PathActivity::class.java)
|
||||
show(intent)
|
||||
}
|
||||
|
||||
private fun showMessageRequests() {
|
||||
val intent = Intent(this, MessageRequestsActivity::class.java)
|
||||
push(intent)
|
||||
@ -624,7 +615,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
textSecurePreferences.setHasHiddenMessageRequests()
|
||||
setupMessageRequestsBanner()
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
homeViewModel.tryUpdateChannel()
|
||||
}
|
||||
.setNegativeButton(R.string.no) { _, _ ->
|
||||
// Do nothing
|
||||
|
@ -1,51 +1,6 @@
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class HomeAdapter(
|
||||
context: Context,
|
||||
cursor: Cursor?,
|
||||
val listener: ConversationClickListener
|
||||
) : CursorRecyclerViewAdapter<HomeAdapter.ViewHolder>(context, cursor) {
|
||||
private val threadDatabase = DatabaseComponent.get(context).threadDatabase()
|
||||
lateinit var glide: GlideRequests
|
||||
var typingThreadIDs = setOf<Long>()
|
||||
set(value) { field = value; notifyDataSetChanged() }
|
||||
|
||||
class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = ConversationView(context)
|
||||
view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } }
|
||||
view.setOnLongClickListener {
|
||||
view.thread?.let { listener.onLongConversationClick(it) }
|
||||
true
|
||||
}
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindItemViewHolder(viewHolder: ViewHolder, cursor: Cursor) {
|
||||
val thread = getThread(cursor)!!
|
||||
val isTyping = typingThreadIDs.contains(thread.threadId)
|
||||
viewHolder.view.bind(thread, isTyping, glide)
|
||||
}
|
||||
|
||||
override fun onItemViewRecycled(holder: ViewHolder?) {
|
||||
super.onItemViewRecycled(holder)
|
||||
holder?.view?.recycle()
|
||||
}
|
||||
|
||||
private fun getThread(cursor: Cursor): ThreadRecord? {
|
||||
return threadDatabase.readerFor(cursor).current
|
||||
}
|
||||
}
|
||||
|
||||
interface ConversationClickListener {
|
||||
fun onConversationClick(thread: ThreadRecord)
|
||||
|
@ -0,0 +1,42 @@
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
|
||||
class HomeDiffUtil(
|
||||
private val old: List<ThreadRecord>,
|
||||
private val new: List<ThreadRecord>,
|
||||
private val context: Context
|
||||
): DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int = old.size
|
||||
|
||||
override fun getNewListSize(): Int = new.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
old[oldItemPosition].threadId == new[newItemPosition].threadId
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = old[oldItemPosition]
|
||||
val newItem = new[newItemPosition]
|
||||
|
||||
// return early to save getDisplayBody or expensive calls
|
||||
val sameCount = oldItem.count == newItem.count
|
||||
if (!sameCount) return false
|
||||
val sameUnreads = oldItem.unreadCount == newItem.unreadCount
|
||||
if (!sameUnreads) return false
|
||||
val samePinned = oldItem.isPinned == newItem.isPinned
|
||||
if (!samePinned) return false
|
||||
val sameAvatar = oldItem.recipient.profileAvatar == newItem.recipient.profileAvatar
|
||||
if (!sameAvatar) return false
|
||||
val sameUsername = oldItem.recipient.name == newItem.recipient.name
|
||||
if (!sameUsername) return false
|
||||
val sameSnippet = oldItem.getDisplayBody(context) == newItem.getDisplayBody(context)
|
||||
if (!sameSnippet) return false
|
||||
|
||||
// all same
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
@ -5,9 +5,14 @@ import android.database.Cursor
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader
|
||||
|
||||
class HomeLoader(context: Context) : AbstractCursorLoader(context) {
|
||||
class HomeLoader(context: Context, val onNewCursor: (Cursor?) -> Unit) : AbstractCursorLoader(context) {
|
||||
|
||||
override fun getCursor(): Cursor {
|
||||
return DatabaseComponent.get(context).threadDatabase().approvedConversationList
|
||||
}
|
||||
|
||||
override fun deliverResult(newCursor: Cursor?) {
|
||||
super.deliverResult(newCursor)
|
||||
onNewCursor(newCursor)
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.cash.copper.flow.observeQuery
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
|
||||
|
||||
private val executor = viewModelScope + SupervisorJob()
|
||||
|
||||
private val _conversations = MutableLiveData<List<ThreadRecord>>()
|
||||
val conversations: LiveData<List<ThreadRecord>> = _conversations
|
||||
|
||||
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED)
|
||||
|
||||
fun tryUpdateChannel() = listUpdateChannel.trySend(Unit)
|
||||
|
||||
fun getObservable(context: Context): LiveData<List<ThreadRecord>> {
|
||||
executor.launch(Dispatchers.IO) {
|
||||
context.contentResolver
|
||||
.observeQuery(DatabaseContentProviders.ConversationList.CONTENT_URI)
|
||||
.onEach { listUpdateChannel.trySend(Unit) }
|
||||
.collect()
|
||||
}
|
||||
executor.launch(Dispatchers.IO) {
|
||||
for (update in listUpdateChannel) {
|
||||
threadDb.approvedConversationList.use { openCursor ->
|
||||
val reader = threadDb.readerFor(openCursor)
|
||||
val threads = mutableListOf<ThreadRecord>()
|
||||
while (true) {
|
||||
threads += reader.next ?: break
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_conversations.value = threads
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return conversations
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class NewHomeAdapter(private val context: Context, private val listener: ConversationClickListener):
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>(),
|
||||
ListUpdateCallback {
|
||||
|
||||
companion object {
|
||||
private const val HEADER = 0
|
||||
private const val ITEM = 1
|
||||
}
|
||||
|
||||
var header: View? = null
|
||||
|
||||
private var _data: List<ThreadRecord> = emptyList()
|
||||
var data: List<ThreadRecord>
|
||||
get() = _data.toList()
|
||||
set(newData) {
|
||||
val previousData = _data.toList()
|
||||
val diff = HomeDiffUtil(previousData, newData, context)
|
||||
val diffResult = DiffUtil.calculateDiff(diff)
|
||||
_data = newData
|
||||
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
|
||||
}
|
||||
|
||||
fun hasHeaderView(): Boolean = header != null
|
||||
|
||||
private val headerCount: Int
|
||||
get() = if (header == null) 0 else 1
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
notifyItemRangeInserted(position + headerCount, count)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
notifyItemRangeRemoved(position + headerCount, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
notifyItemMoved(fromPosition + headerCount, toPosition + headerCount)
|
||||
}
|
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
notifyItemRangeChanged(position + headerCount, count, payload)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
if (hasHeaderView() && position == 0) return NO_ID
|
||||
val offsetPosition = if (hasHeaderView()) position-1 else position
|
||||
return _data[offsetPosition].threadId
|
||||
}
|
||||
|
||||
lateinit var glide: GlideRequests
|
||||
var typingThreadIDs = setOf<Long>()
|
||||
set(value) {
|
||||
field = value
|
||||
// TODO: replace this with a diffed update or a partial change set with payloads
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
||||
when (viewType) {
|
||||
HEADER -> {
|
||||
HeaderFooterViewHolder(header!!)
|
||||
}
|
||||
ITEM -> {
|
||||
val view = ConversationView(context)
|
||||
view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } }
|
||||
view.setOnLongClickListener {
|
||||
view.thread?.let { listener.onLongConversationClick(it) }
|
||||
true
|
||||
}
|
||||
ViewHolder(view)
|
||||
}
|
||||
else -> throw Exception("viewType $viewType isn't valid")
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is ViewHolder) {
|
||||
val offset = if (hasHeaderView()) position - 1 else position
|
||||
val thread = data[offset]
|
||||
val isTyping = typingThreadIDs.contains(thread.threadId)
|
||||
holder.view.bind(thread, isTyping, glide)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is ViewHolder) {
|
||||
holder.view.recycle()
|
||||
} else {
|
||||
super.onViewRecycled(holder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int =
|
||||
if (hasHeaderView() && position == 0) HEADER
|
||||
else ITEM
|
||||
|
||||
override fun getItemCount(): Int = data.size + if (hasHeaderView()) 1 else 0
|
||||
|
||||
class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
class HeaderFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
}
|
@ -39,7 +39,7 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
||||
const val ARGUMENT_THREAD_ID = "threadId"
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentUserDetailsBottomSheetBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@ -51,10 +51,10 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
|
||||
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
||||
with(binding) {
|
||||
profilePictureView.publicKey = publicKey
|
||||
profilePictureView.glide = GlideApp.with(this@UserDetailsBottomSheet)
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update(recipient)
|
||||
profilePictureView.root.publicKey = publicKey
|
||||
profilePictureView.root.glide = GlideApp.with(this@UserDetailsBottomSheet)
|
||||
profilePictureView.root.isLarge = true
|
||||
profilePictureView.root.update(recipient)
|
||||
nameTextViewContainer.visibility = View.VISIBLE
|
||||
nameTextViewContainer.setOnClickListener {
|
||||
nameTextViewContainer.visibility = View.INVISIBLE
|
||||
|
@ -11,7 +11,6 @@ import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
|
||||
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||
import java.security.InvalidParameterException
|
||||
@ -84,14 +83,14 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is ContentView) {
|
||||
holder.binding.searchResultProfilePicture.recycle()
|
||||
holder.binding.searchResultProfilePicture.root.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
val binding = ViewGlobalSearchResultBinding.bind(view).apply {
|
||||
searchResultProfilePicture.glide = GlideApp.with(root)
|
||||
searchResultProfilePicture.root.glide = GlideApp.with(root)
|
||||
}
|
||||
|
||||
fun bindPayload(newQuery: String, model: Model) {
|
||||
@ -99,7 +98,7 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
||||
}
|
||||
|
||||
fun bind(query: String, model: Model) {
|
||||
binding.searchResultProfilePicture.recycle()
|
||||
binding.searchResultProfilePicture.root.recycle()
|
||||
when (model) {
|
||||
is Model.GroupConversation -> bindModel(query, model)
|
||||
is Model.Contact -> bindModel(query, model)
|
||||
|
@ -3,9 +3,7 @@ package org.thoughtcrime.securesms.home.search
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.TypedValue
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import network.loki.messenger.R
|
||||
@ -86,12 +84,12 @@ private fun getHighlight(query: String?, toSearch: String): Spannable? {
|
||||
}
|
||||
|
||||
fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
||||
binding.searchResultProfilePicture.isVisible = true
|
||||
binding.searchResultProfilePicture.root.isVisible = true
|
||||
binding.searchResultSavedMessages.isVisible = false
|
||||
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
|
||||
binding.searchResultTimestamp.isVisible = false
|
||||
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false)
|
||||
binding.searchResultProfilePicture.update(threadRecipient)
|
||||
binding.searchResultProfilePicture.root.update(threadRecipient)
|
||||
val nameString = model.groupRecord.title
|
||||
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||
|
||||
@ -107,14 +105,14 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
||||
}
|
||||
|
||||
fun ContentView.bindModel(query: String?, model: ContactModel) {
|
||||
binding.searchResultProfilePicture.isVisible = true
|
||||
binding.searchResultProfilePicture.root.isVisible = true
|
||||
binding.searchResultSavedMessages.isVisible = false
|
||||
binding.searchResultSubtitle.isVisible = false
|
||||
binding.searchResultTimestamp.isVisible = false
|
||||
binding.searchResultSubtitle.text = null
|
||||
val recipient =
|
||||
Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false)
|
||||
binding.searchResultProfilePicture.update(recipient)
|
||||
binding.searchResultProfilePicture.root.update(recipient)
|
||||
val nameString = model.contact.getSearchName()
|
||||
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||
}
|
||||
@ -123,12 +121,12 @@ fun ContentView.bindModel(model: SavedMessages) {
|
||||
binding.searchResultSubtitle.isVisible = false
|
||||
binding.searchResultTimestamp.isVisible = false
|
||||
binding.searchResultTitle.setText(R.string.note_to_self)
|
||||
binding.searchResultProfilePicture.isVisible = false
|
||||
binding.searchResultProfilePicture.root.isVisible = false
|
||||
binding.searchResultSavedMessages.isVisible = true
|
||||
}
|
||||
|
||||
fun ContentView.bindModel(query: String?, model: Message) {
|
||||
binding.searchResultProfilePicture.isVisible = true
|
||||
binding.searchResultProfilePicture.root.isVisible = true
|
||||
binding.searchResultSavedMessages.isVisible = false
|
||||
binding.searchResultTimestamp.isVisible = true
|
||||
// val hasUnreads = model.unread > 0
|
||||
@ -137,7 +135,7 @@ fun ContentView.bindModel(query: String?, model: Message) {
|
||||
// binding.unreadCountTextView.text = model.unread.toString()
|
||||
// }
|
||||
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.receivedTimestampMs)
|
||||
binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient)
|
||||
binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient)
|
||||
val textSpannable = SpannableStringBuilder()
|
||||
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
||||
// group chat, bind
|
||||
|
@ -48,7 +48,8 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
||||
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
||||
.setMaxAttempts(10)
|
||||
.setMaxAttempts(2)
|
||||
.setMaxInstances(1)
|
||||
.build(),
|
||||
recipient,
|
||||
profileAvatar);
|
||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.messagerequests
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Typeface
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
@ -35,7 +34,7 @@ class MessageRequestView : LinearLayout {
|
||||
// region Updating
|
||||
fun bind(thread: ThreadRecord, glide: GlideRequests) {
|
||||
this.thread = thread
|
||||
binding.profilePictureView.glide = glide
|
||||
binding.profilePictureView.root.glide = glide
|
||||
val senderDisplayName = getUserDisplayName(thread.recipient)
|
||||
?: thread.recipient.address.toString()
|
||||
binding.displayNameTextView.text = senderDisplayName
|
||||
@ -45,12 +44,12 @@ class MessageRequestView : LinearLayout {
|
||||
binding.snippetTextView.text = snippet
|
||||
|
||||
post {
|
||||
binding.profilePictureView.update(thread.recipient)
|
||||
binding.profilePictureView.root.update(thread.recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
binding.profilePictureView.recycle()
|
||||
binding.profilePictureView.root.recycle()
|
||||
}
|
||||
|
||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
||||
|
@ -2,9 +2,11 @@ package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.GlideBuilder;
|
||||
import com.bumptech.glide.Registry;
|
||||
@ -21,12 +23,14 @@ import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
import org.session.libsession.avatars.ContactPhoto;
|
||||
import org.session.libsession.avatars.PlaceholderAvatarPhoto;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader;
|
||||
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
|
||||
import org.thoughtcrime.securesms.glide.OkHttpUrlLoader;
|
||||
import org.thoughtcrime.securesms.glide.PlaceholderAvatarLoader;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapCacheDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
|
||||
@ -69,6 +73,7 @@ public class SignalGlideModule extends AppGlideModule {
|
||||
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
||||
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
||||
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory(context));
|
||||
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
|
||||
}
|
||||
|
||||
|
@ -89,14 +89,14 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver {
|
||||
Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message");
|
||||
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
|
||||
try {
|
||||
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, replyThreadId, false, null);
|
||||
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, replyThreadId, false, null, true);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
} else {
|
||||
Log.w("AndroidAutoReplyReceiver", "Sending regular message ");
|
||||
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
|
||||
DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, System.currentTimeMillis(), null);
|
||||
DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, System.currentTimeMillis(), null, true);
|
||||
}
|
||||
|
||||
List<MarkedMessageInfo> messageIds = DatabaseComponent.get(context).threadDatabase().setRead(replyThreadId, true);
|
||||
|
@ -229,7 +229,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
ThreadDatabase threads = DatabaseComponent.get(context).threadDatabase();
|
||||
Recipient recipient = threads.getRecipientForThreadId(threadId);
|
||||
|
||||
if (!recipient.isGroupRecipient() && threads.getMessageCount(threadId) == 1 &&
|
||||
if (recipient != null && !recipient.isGroupRecipient() && threads.getMessageCount(threadId) == 1 &&
|
||||
!(recipient.isApproved() || threads.getLastSeenAndHasSent(threadId).second())) {
|
||||
TextSecurePreferences.removeHasHiddenMessageRequests(context);
|
||||
}
|
||||
@ -278,10 +278,10 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
|
||||
try {
|
||||
if (notificationState.hasMultipleThreads()) {
|
||||
sendMultipleThreadNotification(context, notificationState, signal);
|
||||
for (long threadId : notificationState.getThreads()) {
|
||||
sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true);
|
||||
}
|
||||
sendMultipleThreadNotification(context, notificationState, signal);
|
||||
} else if (notificationState.getMessageCount() > 0){
|
||||
sendSingleThreadNotification(context, notificationState, signal, false);
|
||||
} else {
|
||||
|
@ -83,7 +83,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
||||
case GroupMessage: {
|
||||
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
|
||||
try {
|
||||
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, threadId, false, null);
|
||||
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, threadId, false, null, true);
|
||||
MessageSender.send(message, address);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
@ -92,7 +92,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
||||
}
|
||||
case SecureMessage: {
|
||||
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
|
||||
DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), null);
|
||||
DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), null, true);
|
||||
MessageSender.send(message, address);
|
||||
break;
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
||||
Bitmap iconBitmap = GlideApp.with(context.getApplicationContext())
|
||||
.asBitmap()
|
||||
.load(contactPhoto)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.circleCrop()
|
||||
.submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
|
||||
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))
|
||||
|
@ -20,7 +20,7 @@ class LandingActivity : BaseActionBarActivity() {
|
||||
with(binding) {
|
||||
fakeChatView.startAnimating()
|
||||
registerButton.setOnClickListener { register() }
|
||||
restoreButton.setOnClickListener { restore() }
|
||||
restoreButton.setOnClickListener { link() }
|
||||
linkButton.setOnClickListener { link() }
|
||||
}
|
||||
IdentityKeyUtil.generateIdentityKeyPair(this)
|
||||
@ -34,11 +34,6 @@ class LandingActivity : BaseActionBarActivity() {
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun restore() {
|
||||
val intent = Intent(this, RecoveryPhraseRestoreActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun link() {
|
||||
val intent = Intent(this, LinkDeviceActivity::class.java)
|
||||
push(intent)
|
||||
|
@ -77,12 +77,12 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey
|
||||
glide = GlideApp.with(this)
|
||||
with(binding) {
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.publicKey = hexEncodedPublicKey
|
||||
profilePictureView.displayName = displayName
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update()
|
||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
||||
profilePictureView.root.glide = glide
|
||||
profilePictureView.root.publicKey = hexEncodedPublicKey
|
||||
profilePictureView.root.displayName = displayName
|
||||
profilePictureView.root.isLarge = true
|
||||
profilePictureView.root.update()
|
||||
profilePictureView.root.setOnClickListener { showEditProfilePictureUI() }
|
||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||
btnGroupNameDisplay.text = displayName
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
@ -214,8 +214,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
binding.btnGroupNameDisplay.text = displayName
|
||||
}
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
binding.profilePictureView.recycle() // Clear the cached image before updating
|
||||
binding.profilePictureView.update()
|
||||
binding.profilePictureView.root.recycle() // Clear the cached image before updating
|
||||
binding.profilePictureView.root.update()
|
||||
}
|
||||
displayNameToBeUploaded = null
|
||||
profilePictureToBeUploaded = null
|
||||
|
@ -22,8 +22,10 @@ import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogShareLogsBinding
|
||||
import org.session.libsignal.utilities.ExternalStorageUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.util.FileProviderUtil
|
||||
import org.thoughtcrime.securesms.util.StreamUtil
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@ -84,18 +86,26 @@ class ShareLogsDialog : BaseDialog() {
|
||||
requireContext().contentResolver.update(mediaUri, updateValues, null, null)
|
||||
}
|
||||
|
||||
val shareIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_STREAM, mediaUri)
|
||||
data = mediaUri
|
||||
type = "text/plain"
|
||||
val shareUri = if (mediaUri.scheme == ContentResolver.SCHEME_FILE) {
|
||||
FileProviderUtil.getUriFor(context, File(mediaUri.path!!))
|
||||
} else {
|
||||
mediaUri
|
||||
}
|
||||
|
||||
withContext(Main) {
|
||||
val shareIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_STREAM, shareUri)
|
||||
type = "text/plain"
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
|
||||
}
|
||||
|
||||
dismiss()
|
||||
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
|
||||
} catch (e: Exception) {
|
||||
withContext(Main) {
|
||||
Log.e("Loki", "Error saving logs", e)
|
||||
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
dismiss()
|
||||
|
@ -33,7 +33,7 @@ import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
interface ConversationRepository {
|
||||
fun isOxenHostedOpenGroup(threadId: Long): Boolean
|
||||
fun getRecipientForThreadId(threadId: Long): Recipient
|
||||
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
||||
fun saveDraft(threadId: Long, text: String)
|
||||
fun getDraft(threadId: Long): String?
|
||||
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
||||
@ -86,12 +86,11 @@ class DefaultConversationRepository @Inject constructor(
|
||||
|
||||
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||
return openGroup?.room == "session" || openGroup?.room == "oxen"
|
||||
|| openGroup?.room == "lokinet" || openGroup?.room == "crypto"
|
||||
return openGroup?.publicKey == OpenGroupAPIV2.defaultServerPublicKey
|
||||
}
|
||||
|
||||
override fun getRecipientForThreadId(threadId: Long): Recipient {
|
||||
return threadDb.getRecipientForThreadId(threadId)!!
|
||||
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
|
||||
return threadDb.getRecipientForThreadId(threadId)
|
||||
}
|
||||
|
||||
override fun saveDraft(threadId: Long, text: String) {
|
||||
@ -121,7 +120,7 @@ class DefaultConversationRepository @Inject constructor(
|
||||
contact,
|
||||
message.sentTimestamp
|
||||
)
|
||||
smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!)
|
||||
smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!, true)
|
||||
MessageSender.send(message, contact.address)
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
//insert the timer update message
|
||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1, true, true);
|
||||
|
||||
//set the timer to the conversation
|
||||
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
|
||||
@ -141,7 +141,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
|
||||
try {
|
||||
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId);
|
||||
database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp);
|
||||
database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp, true);
|
||||
|
||||
if (groupId != null) {
|
||||
// we need the group ID as recipient for setExpireMessages below
|
||||
|
@ -41,7 +41,8 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol {
|
||||
|
||||
override fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String) {
|
||||
val job = RetrieveProfileAvatarJob(recipient, profilePictureURL)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
val jobManager = ApplicationContext.getInstance(context).jobManager
|
||||
jobManager.add(job)
|
||||
val sessionID = recipient.address.serialize()
|
||||
val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()
|
||||
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
||||
|
@ -8,7 +8,7 @@
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/medium_profile_picture_size"
|
||||
android:layout_height="@dimen/medium_profile_picture_size" />
|
||||
|
@ -27,7 +27,7 @@
|
||||
android:layout_marginLeft="20dp"
|
||||
android:layout_marginRight="20dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profileButton"
|
||||
android:layout_width="@dimen/small_profile_picture_size"
|
||||
android:layout_height="@dimen/small_profile_picture_size"
|
||||
|
@ -20,7 +20,7 @@
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/large_profile_picture_size"
|
||||
android:layout_height="@dimen/large_profile_picture_size"
|
||||
|
@ -13,7 +13,7 @@
|
||||
app:behavior_hideable="true"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/large_profile_picture_size"
|
||||
android:layout_height="@dimen/large_profile_picture_size"
|
||||
|
@ -13,7 +13,7 @@
|
||||
app:behavior_hideable="true"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/large_profile_picture_size"
|
||||
android:layout_height="@dimen/large_profile_picture_size"
|
||||
|
@ -23,6 +23,7 @@
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
tools:visibility="visible"
|
||||
android:src="@drawable/ic_download_circle_filled_48"
|
||||
android:id="@+id/thumbnail_download_icon"
|
||||
android:layout_width="@dimen/medium_button_height"
|
||||
|
@ -14,7 +14,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/accent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/medium_profile_picture_size"
|
||||
android:layout_height="@dimen/medium_profile_picture_size"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.DeletedMessageView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
@ -29,4 +29,4 @@
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end" />
|
||||
|
||||
</LinearLayout>
|
||||
</org.thoughtcrime.securesms.conversation.v2.messages.DeletedMessageView>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.DocumentView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
@ -27,4 +27,4 @@
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end" />
|
||||
|
||||
</LinearLayout>
|
||||
</org.thoughtcrime.securesms.conversation.v2.messages.DocumentView>
|
@ -18,7 +18,7 @@
|
||||
android:id="@+id/search_result_profile_picture_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:visibility="gone"
|
||||
android:id="@+id/search_result_profile_picture"
|
||||
android:layout_width="@dimen/medium_profile_picture_size"
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout android:id="@+id/mainLinkPreviewContainer"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
|
@ -13,7 +13,7 @@
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="32dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/very_small_profile_picture_size"
|
||||
android:layout_height="@dimen/very_small_profile_picture_size"
|
||||
|
@ -17,7 +17,7 @@
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="32dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/very_small_profile_picture_size"
|
||||
android:layout_height="@dimen/very_small_profile_picture_size"
|
||||
|
@ -6,7 +6,7 @@
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/medium_profile_picture_size"
|
||||
android:layout_height="@dimen/medium_profile_picture_size"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.OpenGroupInvitationView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@ -66,4 +66,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</org.thoughtcrime.securesms.conversation.v2.messages.OpenGroupInvitationView>
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/doubleModeImageViewContainer"
|
||||
@ -39,4 +39,4 @@
|
||||
android:layout_height="@dimen/large_profile_picture_size"
|
||||
android:background="@drawable/profile_picture_view_large_background" />
|
||||
|
||||
</RelativeLayout>
|
||||
</org.thoughtcrime.securesms.components.ProfilePictureView>
|
@ -1,38 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.QuoteView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/mainQuoteViewContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/input_bar_background"
|
||||
android:paddingHorizontal="@dimen/medium_spacing"
|
||||
android:paddingVertical="@dimen/small_spacing">
|
||||
android:minWidth="300dp"
|
||||
android:minHeight="52dp"
|
||||
android:paddingVertical="12dp"
|
||||
android:paddingHorizontal="12dp"
|
||||
app:quote_mode="regular">
|
||||
|
||||
<View
|
||||
android:id="@+id/quoteViewAccentLine"
|
||||
android:layout_width="@dimen/accent_line_thickness"
|
||||
android:layout_height="0dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginVertical="4dp"
|
||||
android:layout_marginVertical="2dp"
|
||||
android:background="@color/text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintEnd_toStartOf="@id/quoteStartBarrier"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<RelativeLayout
|
||||
tools:visibility="gone"
|
||||
android:id="@+id/quoteViewAttachmentPreviewContainer"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginVertical="@dimen/small_spacing"
|
||||
android:background="@drawable/view_quote_attachment_preview_background"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintEnd_toStartOf="@id/quoteStartBarrier"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
@ -61,44 +61,45 @@
|
||||
app:barrierDirection="end"
|
||||
app:constraint_referenced_ids="quoteViewAttachmentPreviewContainer,quoteViewAccentLine" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_marginVertical="@dimen/small_spacing"
|
||||
android:id="@+id/quoteTextParent"
|
||||
<TextView
|
||||
android:id="@+id/quoteViewAuthorTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:layout_marginEnd="@dimen/medium_spacing"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/quoteViewBodyTextView"
|
||||
app:layout_constraintEnd_toEndOf="@+id/quoteViewBodyTextView"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/quoteStartBarrier"
|
||||
app:layout_constraintEnd_toStartOf="@id/quoteViewCancelButton"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Spiderman" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/quoteViewBodyTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
tools:visibility="gone"
|
||||
android:id="@+id/quoteViewAuthorTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textStyle="bold"
|
||||
tools:text="Spiderman" />
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/quoteStartBarrier"
|
||||
app:layout_constraintTop_toBottomOf="@+id/quoteViewAuthorTextView"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
android:maxWidth="240dp"
|
||||
tools:maxLines="1"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/quoteViewBodyTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
tools:visibility="gone"
|
||||
<View
|
||||
android:id="@+id/quoteViewCancelButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
@ -109,6 +110,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="@color/text" />
|
||||
app:tint="@color/text"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</org.thoughtcrime.securesms.conversation.v2.messages.QuoteView>
|
114
app/src/main/res/layout/view_quote_draft.xml
Normal file
114
app/src/main/res/layout/view_quote_draft.xml
Normal file
@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.QuoteView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/mainQuoteViewContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/input_bar_background"
|
||||
android:paddingHorizontal="@dimen/medium_spacing"
|
||||
android:paddingVertical="@dimen/small_spacing"
|
||||
app:quote_mode="draft">
|
||||
|
||||
<View
|
||||
android:id="@+id/quoteViewAccentLine"
|
||||
android:layout_width="@dimen/accent_line_thickness"
|
||||
android:layout_height="0dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginVertical="4dp"
|
||||
android:background="@color/text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<RelativeLayout
|
||||
tools:visibility="visible"
|
||||
android:id="@+id/quoteViewAttachmentPreviewContainer"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginVertical="@dimen/small_spacing"
|
||||
android:background="@drawable/view_quote_attachment_preview_background"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/quoteViewAttachmentPreviewImageView"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_microphone" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
|
||||
android:id="@+id/quoteViewAttachmentThumbnailImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/quoteStartBarrier"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:barrierDirection="end"
|
||||
app:constraint_referenced_ids="quoteViewAttachmentPreviewContainer,quoteViewAccentLine" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_marginVertical="@dimen/small_spacing"
|
||||
android:id="@+id/quoteTextParent"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:layout_marginEnd="@dimen/medium_spacing"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/quoteStartBarrier"
|
||||
app:layout_constraintEnd_toStartOf="@id/quoteViewCancelButton"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
tools:visibility="gone"
|
||||
android:id="@+id/quoteViewAuthorTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textStyle="bold"
|
||||
tools:text="Spiderman" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/quoteViewBodyTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
tools:visibility="gone"
|
||||
android:id="@+id/quoteViewCancelButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="@dimen/small_spacing"
|
||||
android:layout_marginEnd="@dimen/small_spacing"
|
||||
android:padding="6dp"
|
||||
android:src="@drawable/ic_close_white_48dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="@color/text" />
|
||||
|
||||
</org.thoughtcrime.securesms.conversation.v2.messages.QuoteView>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.UntrustedAttachmentView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
@ -27,4 +27,4 @@
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end" />
|
||||
|
||||
</LinearLayout>
|
||||
</org.thoughtcrime.securesms.conversation.v2.messages.UntrustedAttachmentView>
|
@ -14,7 +14,7 @@
|
||||
android:gravity="center_vertical"
|
||||
android:padding="@dimen/medium_spacing">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/medium_profile_picture_size"
|
||||
android:layout_height="@dimen/medium_profile_picture_size" />
|
||||
|
@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/visibleMessageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
@ -8,108 +10,117 @@
|
||||
<TextView
|
||||
android:id="@+id/dateBreakTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_height="@dimen/large_spacing"
|
||||
tools:text="@tools:sample/date/hhmmss"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center" />
|
||||
android:textStyle="bold" />
|
||||
|
||||
<RelativeLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/mainContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="bottom">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_alignBottom="@+id/messageContentContainer"
|
||||
android:id="@+id/profilePictureContainer"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<View
|
||||
android:id="@+id/startSpacing"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="1dp"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/very_small_profile_picture_size"
|
||||
android:layout_height="@dimen/very_small_profile_picture_size"
|
||||
android:layout_gravity="center" />
|
||||
<include
|
||||
tools:visibility="gone"
|
||||
android:id="@+id/profilePictureView"
|
||||
layout="@layout/view_profile_picture"
|
||||
android:layout_marginBottom="@dimen/small_spacing"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/startSpacing"
|
||||
app:layout_constraintEnd_toStartOf="@+id/expirationTimerViewContainer"
|
||||
android:layout_marginEnd="@dimen/small_spacing"
|
||||
android:layout_width="@dimen/very_small_profile_picture_size"
|
||||
android:layout_height="@dimen/very_small_profile_picture_size"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/moderatorIconImageView"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginEnd="@dimen/small_spacing"
|
||||
android:src="@drawable/ic_crown"
|
||||
android:layout_gravity="bottom|end" />
|
||||
<ImageView
|
||||
android:visibility="gone"
|
||||
android:id="@+id/moderatorIconImageView"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePictureView"
|
||||
app:layout_constraintEnd_toEndOf="@+id/profilePictureView"
|
||||
android:layout_marginEnd="-4dp"
|
||||
android:layout_marginBottom="-4dp"
|
||||
android:src="@drawable/ic_crown" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/messageContentContainer"
|
||||
<TextView
|
||||
android:id="@+id/senderNameTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
tools:text="@tools:sample/full_names"
|
||||
android:paddingBottom="4dp"
|
||||
app:layout_constraintStart_toStartOf="@+id/expirationTimerViewContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/senderNameTextView"
|
||||
<LinearLayout
|
||||
android:id="@+id/expirationTimerViewContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePictureView"
|
||||
app:layout_constraintEnd_toStartOf="@+id/messageTimestampContainer"
|
||||
app:layout_constraintStart_toEndOf="@+id/profilePictureView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/senderNameTextView">
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView
|
||||
android:id="@+id/messageContentView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/expirationTimerViewContainer"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
|
||||
tools:visibility="visible"
|
||||
android:visibility="gone"
|
||||
android:id="@+id/expirationTimerView"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginHorizontal="@dimen/small_spacing" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView
|
||||
android:id="@+id/messageContentView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
|
||||
android:id="@+id/expirationTimerView"
|
||||
android:layout_marginHorizontal="@dimen/small_spacing"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_gravity="center_vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageTimestampTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="2dp"
|
||||
android:maxLines="1"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageStatusImageView"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:padding="2dp"
|
||||
android:src="@drawable/ic_delivery_status_sent" />
|
||||
|
||||
</RelativeLayout>
|
||||
<View
|
||||
android:id="@+id/messageContentSpacing"
|
||||
android:minWidth="@dimen/very_large_spacing"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
<RelativeLayout
|
||||
android:id="@+id/messageTimestampContainer"
|
||||
android:layout_width="@dimen/medium_spacing"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/expirationTimerViewContainer">
|
||||
|
||||
</LinearLayout>
|
||||
<ImageView
|
||||
android:id="@+id/messageStatusImageView"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:padding="2dp"
|
||||
android:src="@drawable/ic_delivery_status_sent" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user