mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Performance improvements and bug fixes (#869)
* refactor: fail on testSnode instead of recursively using up snode list. add call timeout on http client * refactor: refactoring batch message receives and pollers * refactor: reduce thread utils pool count to a 2 thread fixed pool. Do a check against pubkey instead of room names for oxenHostedOpenGroup * refactor: caching lib with potential loader fixes and no-cache for giphy * refactor: remove store and instead use ConcurrentHashMap with a backing update coroutine * refactor: queue trim thread jobs instead of add every message processed * fix: wrapping auth token and initial sync for open groups in a threadutils queued runnable, getting initial sync times down * fix: fixing the user contacts cache in ConversationAdapter.kt * refactor: improve polling and initial sync, move group joins from config messages into a background job fetching image. * refactor: improving the job queuing for open groups, replacing placeholder avatar generation with a custom glide loader and archiving initial sync of open groups * feat: add OpenGroupDeleteJob.kt * feat: add open group delete job to process deletions after batch adding * feat: add vacuum and fix job queue re-adding jobs forever, only try to set message hash values in DB if they have changed * refactor: remove redundant inflation for profile image views throughout app * refactor(wip): reducing layout inflation and starting to refactor the open group deletion issues taking a long time * refactor(wip): refactoring group deletion to not iterate through and delete messages individually * refactor(wip): refactoring group deletion to not iterate through and delete messages individually * fix: group deletion optimisation * build: bump build number * build: bump build number and fix batch message receive retry logic * fix: clear out open group deletes * fix: update visible ConversationAdapter.kt binding for initial contact fetching and better traces for debugging background jobs * fix: add in check for / force sync latest encryption key pair from linked devices if we already have that closed group * Rename .java to .kt * refactor: change MmsDatabase to kotlin to make list operations easier * fix: nullable type * fix: compilation issues and constants in .kt instead of .java * fix: bug fix expiration timer on closed group recipient * feat: use the job queue properly across executors * feat: start on open group dispatcher-specific logic, probably a queue factory based on openGroupId if that is the same across new message and deletion jobs to ensure consistent entry and removal * refactor: removing redundant code and fixing jobqueue per opengroup * fix: allow attachments in note to self * fix: make the minWidth in quote view bind max of text / title and body, wrapped ? * fix: fixing up layouts and code view layouts * fix: remove TODO, remove timestamp binding * feat: fix view logic, avatars and padding, downloading attachments lazily (on bind), fixing potential crash, add WindowDebouncer.kt * fix: NPE on viewModel recipient from removed thread while tearing down the Recipient observer in ConversationActivityV2.kt * refactor: replace conversation notification debouncer handler with handlerthread, same as conversation list debouncer * refactor: UI for groups and poller improvements * fix: revert some changes in poller * feat: add header back in for message requests * refactor: remove Trace calls, add more conditions to the HomeDiffUtil for updating more efficiently * feat: try update the home adapter if we get a profile picture modified event * feat: bump build numbers * fix: try to start with list in homeViewModel if we don't have already, render quotes to be width of attachment slide view instead of fixed * fix: set channel to be conflated instead of no buffer * fix: set unreads based off last local user message vs incrementing unreads to be all amount * feat: add profile update flag, update build number * fix: link preview thumbnails download on bind * fix: centercrop placeholder in glide request * feat: recycle the contact selection list and profile image in unbind * fix: try to prevent user KP crash at weird times * fix: remove additional log, improve attachment download success rate, fix share logs dialog issue
This commit is contained in:
parent
db92034a8a
commit
6ddefb7a2e
@ -110,6 +110,7 @@ dependencies {
|
|||||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
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.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||||
@ -158,8 +159,8 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 279
|
def canonicalVersionCode = 282
|
||||||
def canonicalVersionName = "1.13.1"
|
def canonicalVersionName = "1.13.4"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
@ -24,7 +24,7 @@ import android.content.Intent;
|
|||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.HandlerThread;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
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.SSKEnvironment;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsession.utilities.Util;
|
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.DynamicLanguageContextWrapper;
|
||||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
@ -93,6 +94,7 @@ import java.security.Security;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.Timer;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
@ -127,7 +129,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
public Poller poller = null;
|
public Poller poller = null;
|
||||||
public Broadcaster broadcaster = null;
|
public Broadcaster broadcaster = null;
|
||||||
private Job firebaseInstanceIdJob;
|
private Job firebaseInstanceIdJob;
|
||||||
private Handler conversationListNotificationHandler;
|
private WindowDebouncer conversationListDebouncer;
|
||||||
|
private HandlerThread conversationListHandlerThread;
|
||||||
|
private Handler conversationListHandler;
|
||||||
private PersistentLogger persistentLogger;
|
private PersistentLogger persistentLogger;
|
||||||
|
|
||||||
@Inject LokiAPIDatabase lokiAPIDatabase;
|
@Inject LokiAPIDatabase lokiAPIDatabase;
|
||||||
@ -136,9 +140,18 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
@Inject JobDatabase jobDatabase;
|
@Inject JobDatabase jobDatabase;
|
||||||
@Inject TextSecurePreferences textSecurePreferences;
|
@Inject TextSecurePreferences textSecurePreferences;
|
||||||
CallMessageProcessor callMessageProcessor;
|
CallMessageProcessor callMessageProcessor;
|
||||||
|
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||||
|
|
||||||
private volatile boolean isAppVisible;
|
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) {
|
public static ApplicationContext getInstance(Context context) {
|
||||||
return (ApplicationContext) context.getApplicationContext();
|
return (ApplicationContext) context.getApplicationContext();
|
||||||
}
|
}
|
||||||
@ -148,10 +161,21 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Handler getConversationListNotificationHandler() {
|
public Handler getConversationListNotificationHandler() {
|
||||||
if (this.conversationListNotificationHandler == null) {
|
if (this.conversationListHandlerThread == null) {
|
||||||
conversationListNotificationHandler = new Handler(Looper.getMainLooper());
|
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() {
|
public PersistentLogger getPersistentLogger() {
|
||||||
@ -161,7 +185,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
DatabaseModule.init(this);
|
DatabaseModule.init(this);
|
||||||
|
MessagingModuleConfiguration.configure(this);
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
|
||||||
|
storage,
|
||||||
|
messageDataProvider,
|
||||||
|
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
|
||||||
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
||||||
Log.i(TAG, "onCreate()");
|
Log.i(TAG, "onCreate()");
|
||||||
startKovenant();
|
startKovenant();
|
||||||
@ -174,11 +203,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
|
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
|
||||||
broadcaster = new Broadcaster(this);
|
broadcaster = new Broadcaster(this);
|
||||||
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
|
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
|
||||||
MessagingModuleConfiguration.Companion.configure(this,
|
|
||||||
storage,
|
|
||||||
messageDataProvider,
|
|
||||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this)
|
|
||||||
);
|
|
||||||
SnodeModule.Companion.configure(apiDB, broadcaster);
|
SnodeModule.Companion.configure(apiDB, broadcaster);
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||||
if (userPublicKey != null) {
|
if (userPublicKey != null) {
|
||||||
|
@ -5,7 +5,14 @@ import android.text.TextUtils
|
|||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.session.libsession.database.MessageDataProvider
|
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.Address
|
||||||
import org.session.libsession.utilities.UploadResult
|
import org.session.libsession.utilities.UploadResult
|
||||||
import org.session.libsession.utilities.Util
|
import org.session.libsession.utilities.Util
|
||||||
@ -126,7 +133,7 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
|
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
|
||||||
return mmsDb.getMessage(mmsMessageId).use { cursor ->
|
return mmsDb.getMessage(mmsMessageId).use { cursor ->
|
||||||
mmsDb.readerFor(cursor).next
|
mmsDb.readerFor(cursor).next
|
||||||
}.isOutgoing
|
}?.isOutgoing ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isOutgoingMessage(timestamp: Long): Boolean {
|
override fun isOutgoingMessage(timestamp: Long): Boolean {
|
||||||
|
@ -10,29 +10,51 @@ import com.annimon.stream.function.Predicate
|
|||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import net.sqlcipher.database.SQLiteDatabase
|
import net.sqlcipher.database.SQLiteDatabase
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
|
||||||
import org.session.libsession.avatars.AvatarHelper
|
import org.session.libsession.avatars.AvatarHelper
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||||
import org.session.libsession.utilities.Conversions
|
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.libsession.utilities.Util
|
||||||
import org.session.libsignal.crypto.kdf.HKDFv3
|
import org.session.libsignal.crypto.kdf.HKDFv3
|
||||||
import org.session.libsignal.utilities.ByteUtil
|
import org.session.libsignal.utilities.ByteUtil
|
||||||
import java.io.*
|
import org.session.libsignal.utilities.Log
|
||||||
import java.lang.Exception
|
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.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.*
|
import java.util.LinkedList
|
||||||
import javax.crypto.*
|
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.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
@ -245,8 +267,8 @@ object FullBackupExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
|
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
|
||||||
val columns = arrayOf(MmsDatabase.EXPIRES_IN)
|
val columns = arrayOf(MmsSmsColumns.EXPIRES_IN)
|
||||||
val where = MmsDatabase.ID + " = ?"
|
val where = MmsSmsColumns.ID + " = ?"
|
||||||
val args = arrayOf(mmsId.toString())
|
val args = arrayOf(mmsId.toString())
|
||||||
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
|
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
|
||||||
if (mmsCursor != null && mmsCursor.moveToFirst()) {
|
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.crypto.kdf.HKDFv3
|
||||||
import org.session.libsignal.utilities.ByteUtil
|
import org.session.libsignal.utilities.ByteUtil
|
||||||
import org.session.libsignal.utilities.Log
|
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.AttachmentSecret
|
||||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
|
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.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil
|
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.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.*
|
import java.util.LinkedList
|
||||||
import javax.crypto.*
|
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.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
@ -172,7 +192,7 @@ object FullBackupImporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun trimEntriesForExpiredMessages(context: Context, db: SQLiteDatabase) {
|
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)
|
db.delete(GroupReceiptDatabase.TABLE_NAME, GroupReceiptDatabase.MMS_ID + trimmedCondition, null)
|
||||||
val columns = arrayOf(AttachmentDatabase.ROW_ID, AttachmentDatabase.UNIQUE_ID)
|
val columns = arrayOf(AttachmentDatabase.ROW_ID, AttachmentDatabase.UNIQUE_ID)
|
||||||
val where = AttachmentDatabase.MMS_ID + trimmedCondition
|
val where = AttachmentDatabase.MMS_ID + trimmedCondition
|
||||||
|
@ -8,28 +8,26 @@ import android.graphics.Outline;
|
|||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.widget.AppCompatImageView;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewOutlineProvider;
|
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 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.ContactColors;
|
||||||
import org.session.libsession.avatars.ContactPhoto;
|
import org.session.libsession.avatars.ContactPhoto;
|
||||||
import org.session.libsession.avatars.ResourceContactPhoto;
|
import org.session.libsession.avatars.ResourceContactPhoto;
|
||||||
import org.session.libsession.utilities.Address;
|
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.Recipient;
|
||||||
import org.session.libsession.utilities.recipients.RecipientExporter;
|
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;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -139,7 +137,7 @@ public class AvatarImageView extends AppCompatImageView {
|
|||||||
requestManager.load(photo.contactPhoto)
|
requestManager.load(photo.contactPhoto)
|
||||||
.fallback(photoPlaceholderDrawable)
|
.fallback(photoPlaceholderDrawable)
|
||||||
.error(photoPlaceholderDrawable)
|
.error(photoPlaceholderDrawable)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.into(this);
|
.into(this);
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
@ -10,16 +9,20 @@ import androidx.annotation.DimenRes
|
|||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
||||||
|
import org.session.libsession.avatars.ContactColors
|
||||||
|
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
||||||
import org.session.libsession.avatars.ProfileContactPhoto
|
import org.session.libsession.avatars.ProfileContactPhoto
|
||||||
|
import org.session.libsession.avatars.ResourceContactPhoto
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
|
|
||||||
|
|
||||||
class ProfilePictureView : RelativeLayout {
|
class ProfilePictureView @JvmOverloads constructor(
|
||||||
private lateinit var binding: ViewProfilePictureBinding
|
context: Context, attrs: AttributeSet? = null
|
||||||
|
) : RelativeLayout(context, attrs) {
|
||||||
|
private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) }
|
||||||
lateinit var glide: GlideRequests
|
lateinit var glide: GlideRequests
|
||||||
var publicKey: String? = null
|
var publicKey: String? = null
|
||||||
var displayName: String? = null
|
var displayName: String? = null
|
||||||
@ -28,16 +31,9 @@ class ProfilePictureView : RelativeLayout {
|
|||||||
var isLarge = false
|
var isLarge = false
|
||||||
|
|
||||||
private val profilePicturesCache = mutableMapOf<String, String?>()
|
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
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
@ -105,21 +101,24 @@ class ProfilePictureView : RelativeLayout {
|
|||||||
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
|
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
|
||||||
val signalProfilePicture = recipient.contactPhoto
|
val signalProfilePicture = recipient.contactPhoto
|
||||||
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
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 != "") {
|
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
glide.load(signalProfilePicture)
|
glide.load(signalProfilePicture)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
.placeholder(unknownRecipientDrawable)
|
||||||
.circleCrop()
|
.centerCrop()
|
||||||
.error(AvatarPlaceholderGenerator.generate(context,sizeInPX, publicKey, displayName))
|
.error(unknownRecipientDrawable)
|
||||||
.into(imageView)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
profilePicturesCache[publicKey] = recipient.profileAvatar
|
.circleCrop()
|
||||||
|
.into(imageView)
|
||||||
} else {
|
} else {
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
glide.load(AvatarPlaceholderGenerator.generate(context, sizeInPX, publicKey, displayName))
|
glide.load(placeholder)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
|
.placeholder(unknownRecipientDrawable)
|
||||||
profilePicturesCache[publicKey] = recipient.profileAvatar
|
.centerCrop()
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
|
||||||
}
|
}
|
||||||
|
profilePicturesCache[publicKey] = recipient.profileAvatar
|
||||||
} else {
|
} else {
|
||||||
imageView.setImageDrawable(null)
|
imageView.setImageDrawable(null)
|
||||||
}
|
}
|
||||||
|
@ -264,7 +264,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
|
|||||||
}
|
}
|
||||||
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
|
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.into(thumbnailView);
|
.into(thumbnailView);
|
||||||
} else if (!documentSlides.isEmpty()){
|
} else if (!documentSlides.isEmpty()){
|
||||||
thumbnailView.setVisibility(GONE);
|
thumbnailView.setVisibility(GONE);
|
||||||
|
@ -35,6 +35,13 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
|
|||||||
return items.size
|
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 {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return when (items[position]) {
|
return when (items[position]) {
|
||||||
is ContactSelectionListItem.Header -> ViewType.Divider
|
is ContactSelectionListItem.Header -> ViewType.Divider
|
||||||
|
@ -54,8 +54,8 @@ class UserView : LinearLayout {
|
|||||||
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
|
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
|
||||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
|
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
|
||||||
val address = user.address.serialize()
|
val address = user.address.serialize()
|
||||||
binding.profilePictureView.glide = glide
|
binding.profilePictureView.root.glide = glide
|
||||||
binding.profilePictureView.update(user)
|
binding.profilePictureView.root.update(user)
|
||||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||||
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
||||||
when (actionIndicator) {
|
when (actionIndicator) {
|
||||||
@ -83,7 +83,7 @@ class UserView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun unbind() {
|
fun unbind() {
|
||||||
|
binding.profilePictureView.root.recycle()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ import kotlinx.coroutines.launch
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding
|
import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding
|
||||||
import network.loki.messenger.databinding.ActivityConversationV2Binding
|
import network.loki.messenger.databinding.ActivityConversationV2Binding
|
||||||
|
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||||
import nl.komponents.kovenant.ui.successUi
|
import nl.komponents.kovenant.ui.successUi
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.mentions.Mention
|
import org.session.libsession.messaging.mentions.Mention
|
||||||
@ -241,7 +242,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
actionMode?.let {
|
actionMode?.let {
|
||||||
onDeselect(message, position, it)
|
onDeselect(message, position, it)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
lifecycleCoroutineScope = lifecycleScope
|
||||||
)
|
)
|
||||||
adapter.visibleMessageContentViewDelegate = this
|
adapter.visibleMessageContentViewDelegate = this
|
||||||
adapter
|
adapter
|
||||||
@ -314,11 +316,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
scrollToFirstUnreadMessageIfNeeded()
|
scrollToFirstUnreadMessageIfNeeded()
|
||||||
showOrHideInputIfNeeded()
|
showOrHideInputIfNeeded()
|
||||||
setUpMessageRequestsBar()
|
setUpMessageRequestsBar()
|
||||||
if (viewModel.recipient.isOpenGroupRecipient) {
|
viewModel.recipient?.let { recipient ->
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
|
if (recipient.isOpenGroupRecipient) {
|
||||||
if (openGroup == null) {
|
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
|
||||||
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
|
if (openGroup == null) {
|
||||||
return finish()
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId)
|
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() {
|
override fun onPause() {
|
||||||
@ -391,17 +396,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
actionBar.title = ""
|
actionBar.title = ""
|
||||||
actionBar.customView = actionBarBinding!!.root
|
actionBar.customView = actionBarBinding!!.root
|
||||||
actionBar.setDisplayShowCustomEnabled(true)
|
actionBar.setDisplayShowCustomEnabled(true)
|
||||||
actionBarBinding!!.conversationTitleView.text = viewModel.recipient.toShortString()
|
actionBarBinding!!.conversationTitleView.text = viewModel.recipient?.toShortString()
|
||||||
@DimenRes val sizeID: Int = if (viewModel.recipient.isClosedGroupRecipient) {
|
@DimenRes val sizeID: Int = if (viewModel.recipient?.isClosedGroupRecipient == true) {
|
||||||
R.dimen.medium_profile_picture_size
|
R.dimen.medium_profile_picture_size
|
||||||
} else {
|
} else {
|
||||||
R.dimen.small_profile_picture_size
|
R.dimen.small_profile_picture_size
|
||||||
}
|
}
|
||||||
val size = resources.getDimension(sizeID).roundToInt()
|
val size = resources.getDimension(sizeID).roundToInt()
|
||||||
actionBarBinding!!.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
|
actionBarBinding!!.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
|
||||||
actionBarBinding!!.profilePictureView.glide = glide
|
actionBarBinding!!.profilePictureView.root.glide = glide
|
||||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
|
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
|
// called from onCreate
|
||||||
@ -437,7 +445,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
if (mediaURI != null && mediaType != null) {
|
if (mediaURI != null && mediaType != null) {
|
||||||
if (AttachmentManager.MediaType.IMAGE == mediaType || AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType) {
|
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())
|
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
|
return
|
||||||
} else {
|
} else {
|
||||||
prepMediaForSending(mediaURI, mediaType).addListener(object : ListenableFuture.Listener<Boolean> {
|
prepMediaForSending(mediaURI, mediaType).addListener(object : ListenableFuture.Listener<Boolean> {
|
||||||
@ -491,11 +499,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpRecipientObserver() {
|
private fun setUpRecipientObserver() {
|
||||||
viewModel.recipient.addListener(this)
|
viewModel.recipient?.addListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tearDownRecipientObserver() {
|
private fun tearDownRecipientObserver() {
|
||||||
viewModel.recipient.removeListener(this)
|
viewModel.recipient?.removeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLatestOpenGroupInfoIfNeeded() {
|
private fun getLatestOpenGroupInfoIfNeeded() {
|
||||||
@ -505,12 +513,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
// called from onCreate
|
// called from onCreate
|
||||||
private fun setUpBlockedBanner() {
|
private fun setUpBlockedBanner() {
|
||||||
if (viewModel.recipient.isGroupRecipient) { return }
|
val recipient = viewModel.recipient ?: return
|
||||||
val sessionID = viewModel.recipient.address.toString()
|
if (recipient.isGroupRecipient) { return }
|
||||||
|
val sessionID = recipient.address.toString()
|
||||||
val contact = sessionContactDb.getContactWithSessionID(sessionID)
|
val contact = sessionContactDb.getContactWithSessionID(sessionID)
|
||||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||||
binding?.blockedBannerTextView?.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
|
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() }
|
binding?.blockedBanner?.setOnClickListener { viewModel.unblock() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -558,13 +567,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||||
if (!isMessageRequestThread()) {
|
if (!isMessageRequestThread()) {
|
||||||
ConversationMenuHelper.onPrepareOptionsMenu(
|
val recipient = viewModel.recipient
|
||||||
menu,
|
if (recipient != null) {
|
||||||
menuInflater,
|
ConversationMenuHelper.onPrepareOptionsMenu(
|
||||||
viewModel.recipient,
|
menu,
|
||||||
viewModel.threadId,
|
menuInflater,
|
||||||
this
|
recipient,
|
||||||
) { onOptionsItemSelected(it) }
|
viewModel.threadId,
|
||||||
|
this
|
||||||
|
) { onOptionsItemSelected(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
super.onPrepareOptionsMenu(menu)
|
super.onPrepareOptionsMenu(menu)
|
||||||
return true
|
return true
|
||||||
@ -582,21 +594,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
// region Animation & Updating
|
// region Animation & Updating
|
||||||
override fun onModified(recipient: Recipient) {
|
override fun onModified(recipient: Recipient) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (viewModel.recipient.isContactRecipient) {
|
val recipient = viewModel.recipient
|
||||||
binding?.blockedBanner?.isVisible = viewModel.recipient.isBlocked
|
if (recipient != null && recipient.isContactRecipient) {
|
||||||
|
binding?.blockedBanner?.isVisible = recipient.isBlocked
|
||||||
}
|
}
|
||||||
setUpMessageRequestsBar()
|
setUpMessageRequestsBar()
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
updateSubtitle()
|
updateSubtitle()
|
||||||
showOrHideInputIfNeeded()
|
showOrHideInputIfNeeded()
|
||||||
actionBarBinding?.profilePictureView?.update(recipient)
|
if (recipient != null) {
|
||||||
actionBarBinding?.conversationTitleView?.text = recipient.toShortString()
|
actionBarBinding?.profilePictureView?.root?.update(recipient)
|
||||||
|
}
|
||||||
|
actionBarBinding?.conversationTitleView?.text = recipient?.toShortString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showOrHideInputIfNeeded() {
|
private fun showOrHideInputIfNeeded() {
|
||||||
if (viewModel.recipient.isClosedGroupRecipient) {
|
val recipient = viewModel.recipient
|
||||||
val group = groupDb.getGroup(viewModel.recipient.address.toGroupString()).orNull()
|
if (recipient != null && recipient.isClosedGroupRecipient) {
|
||||||
|
val group = groupDb.getGroup(recipient.address.toGroupString()).orNull()
|
||||||
val isActive = (group?.isActive == true)
|
val isActive = (group?.isActive == true)
|
||||||
binding?.inputBar?.showInput = isActive
|
binding?.inputBar?.showInput = isActive
|
||||||
} else {
|
} else {
|
||||||
@ -632,17 +648,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isMessageRequestThread(): Boolean {
|
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 {
|
private fun isOutgoingMessageRequestThread(): Boolean {
|
||||||
return !viewModel.recipient.isGroupRecipient &&
|
val recipient = viewModel.recipient ?: return false
|
||||||
!(viewModel.recipient.hasApprovedMe() || viewModel.hasReceived())
|
return !recipient.isGroupRecipient &&
|
||||||
|
!recipient.isLocalNumber &&
|
||||||
|
!(recipient.hasApprovedMe() || viewModel.hasReceived())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isIncomingMessageRequestThread(): Boolean {
|
private fun isIncomingMessageRequestThread(): Boolean {
|
||||||
return !viewModel.recipient.isGroupRecipient &&
|
val recipient = viewModel.recipient ?: return false
|
||||||
!viewModel.recipient.isApproved &&
|
return !recipient.isGroupRecipient &&
|
||||||
|
!recipient.isApproved &&
|
||||||
|
!recipient.isLocalNumber &&
|
||||||
!threadDb.getLastSeenAndHasSent(viewModel.threadId).second() &&
|
!threadDb.getLastSeenAndHasSent(viewModel.threadId).second() &&
|
||||||
threadDb.getMessageCount(viewModel.threadId) > 0
|
threadDb.getMessageCount(viewModel.threadId) > 0
|
||||||
}
|
}
|
||||||
@ -701,17 +722,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
private fun showOrUpdateMentionCandidatesIfNeeded(query: String = "") {
|
private fun showOrUpdateMentionCandidatesIfNeeded(query: String = "") {
|
||||||
val additionalContentContainer = binding?.additionalContentContainer ?: return
|
val additionalContentContainer = binding?.additionalContentContainer ?: return
|
||||||
|
val recipient = viewModel.recipient ?: return
|
||||||
if (!isShowingMentionCandidatesView) {
|
if (!isShowingMentionCandidatesView) {
|
||||||
additionalContentContainer.removeAllViews()
|
additionalContentContainer.removeAllViews()
|
||||||
val view = MentionCandidatesView(this)
|
val view = MentionCandidatesView(this)
|
||||||
view.glide = glide
|
view.glide = glide
|
||||||
view.onCandidateSelected = { handleMentionSelected(it) }
|
view.onCandidateSelected = { handleMentionSelected(it) }
|
||||||
additionalContentContainer.addView(view)
|
additionalContentContainer.addView(view)
|
||||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
|
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
|
||||||
this.mentionCandidatesView = view
|
this.mentionCandidatesView = view
|
||||||
view.show(candidates, viewModel.threadId)
|
view.show(candidates, viewModel.threadId)
|
||||||
} else {
|
} else {
|
||||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
|
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
|
||||||
this.mentionCandidatesView!!.setMentionCandidates(candidates)
|
this.mentionCandidatesView!!.setMentionCandidates(candidates)
|
||||||
}
|
}
|
||||||
isShowingMentionCandidatesView = true
|
isShowingMentionCandidatesView = true
|
||||||
@ -839,15 +861,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
private fun updateSubtitle() {
|
private fun updateSubtitle() {
|
||||||
val actionBarBinding = actionBarBinding ?: return
|
val actionBarBinding = actionBarBinding ?: return
|
||||||
actionBarBinding.muteIconImageView.isVisible = viewModel.recipient.isMuted
|
val recipient = viewModel.recipient ?: return
|
||||||
|
actionBarBinding.muteIconImageView.isVisible = recipient.isMuted
|
||||||
actionBarBinding.conversationSubtitleView.isVisible = true
|
actionBarBinding.conversationSubtitleView.isVisible = true
|
||||||
if (viewModel.recipient.isMuted) {
|
if (recipient.isMuted) {
|
||||||
if (viewModel.recipient.mutedUntil != Long.MAX_VALUE) {
|
if (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()))
|
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
|
||||||
} else {
|
} else {
|
||||||
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever)
|
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever)
|
||||||
}
|
}
|
||||||
} else if (viewModel.recipient.isGroupRecipient) {
|
} else if (recipient.isGroupRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
|
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
|
||||||
if (openGroup != null) {
|
if (openGroup != null) {
|
||||||
val userCount = lokiApiDb.getUserCount(openGroup.room, openGroup.server) ?: 0
|
val userCount = lokiApiDb.getUserCount(openGroup.room, openGroup.server) ?: 0
|
||||||
@ -866,7 +889,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
if (item.itemId == android.R.id.home) {
|
if (item.itemId == android.R.id.home) {
|
||||||
return false
|
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
|
// `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
|
// `position` is the adapter position; not the visual position
|
||||||
private fun handleSwipeToReply(message: MessageRecord, position: Int) {
|
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
|
// `position` is the adapter position; not the visual position
|
||||||
@ -1002,12 +1028,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
override fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) {
|
override fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) {
|
||||||
if (indexInAdapter < 0 || indexInAdapter >= adapter.itemCount) { return }
|
if (indexInAdapter < 0 || indexInAdapter >= adapter.itemCount) { return }
|
||||||
val viewHolder = binding?.conversationRecyclerView?.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder ?: 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() {
|
override fun sendMessage() {
|
||||||
if (viewModel.recipient.isContactRecipient && viewModel.recipient.isBlocked) {
|
val recipient = viewModel.recipient ?: return
|
||||||
BlockedDialog(viewModel.recipient).show(supportFragmentManager, "Blocked Dialog")
|
if (recipient.isContactRecipient && recipient.isBlocked) {
|
||||||
|
BlockedDialog(recipient).show(supportFragmentManager, "Blocked Dialog")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val binding = binding ?: return
|
val binding = binding ?: return
|
||||||
@ -1019,24 +1047,26 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun commitInputContent(contentUri: Uri) {
|
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())
|
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() {
|
private fun processMessageRequestApproval() {
|
||||||
if (isIncomingMessageRequestThread()) {
|
if (isIncomingMessageRequestThread()) {
|
||||||
acceptMessageRequest()
|
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
|
// edge case for new outgoing thread on new recipient without sending approval messages
|
||||||
viewModel.setRecipientApproved()
|
viewModel.setRecipientApproved()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false) {
|
private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false) {
|
||||||
|
val recipient = viewModel.recipient ?: return
|
||||||
processMessageRequestApproval()
|
processMessageRequestApproval()
|
||||||
val text = getMessageBody()
|
val text = getMessageBody()
|
||||||
val userPublicKey = textSecurePreferences.getLocalNumber()
|
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) {
|
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
|
||||||
val dialog = SendSeedDialog { sendTextOnlyMessage(true) }
|
val dialog = SendSeedDialog { sendTextOnlyMessage(true) }
|
||||||
return dialog.show(supportFragmentManager, "Send Seed Dialog")
|
return dialog.show(supportFragmentManager, "Send Seed Dialog")
|
||||||
@ -1045,7 +1075,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val message = VisibleMessage()
|
val message = VisibleMessage()
|
||||||
message.sentTimestamp = System.currentTimeMillis()
|
message.sentTimestamp = System.currentTimeMillis()
|
||||||
message.text = text
|
message.text = text
|
||||||
val outgoingTextMessage = OutgoingTextMessage.from(message, viewModel.recipient)
|
val outgoingTextMessage = OutgoingTextMessage.from(message, recipient)
|
||||||
// Clear the input bar
|
// Clear the input bar
|
||||||
binding?.inputBar?.text = ""
|
binding?.inputBar?.text = ""
|
||||||
binding?.inputBar?.cancelQuoteDraft()
|
binding?.inputBar?.cancelQuoteDraft()
|
||||||
@ -1055,14 +1085,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
currentMentionStartIndex = -1
|
currentMentionStartIndex = -1
|
||||||
mentions.clear()
|
mentions.clear()
|
||||||
// Put the message in the database
|
// 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
|
// Send it
|
||||||
MessageSender.send(message, viewModel.recipient.address)
|
MessageSender.send(message, recipient.address)
|
||||||
// Send a typing stopped message
|
// Send a typing stopped message
|
||||||
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
|
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendAttachments(attachments: List<Attachment>, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
|
private fun sendAttachments(attachments: List<Attachment>, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
|
||||||
|
val recipient = viewModel.recipient ?: return
|
||||||
processMessageRequestApproval()
|
processMessageRequestApproval()
|
||||||
// Create the message
|
// Create the message
|
||||||
val message = VisibleMessage()
|
val message = VisibleMessage()
|
||||||
@ -1073,7 +1104,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val sender = if (it.isOutgoing) fromSerialized(textSecurePreferences.getLocalNumber()!!) else it.individualRecipient.address
|
val sender = if (it.isOutgoing) fromSerialized(textSecurePreferences.getLocalNumber()!!) else it.individualRecipient.address
|
||||||
QuoteModel(it.dateSent, sender, it.body, false, quotedAttachments)
|
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
|
// Clear the input bar
|
||||||
binding?.inputBar?.text = ""
|
binding?.inputBar?.text = ""
|
||||||
binding?.inputBar?.cancelQuoteDraft()
|
binding?.inputBar?.cancelQuoteDraft()
|
||||||
@ -1087,9 +1118,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
// Reset attachments button if needed
|
// Reset attachments button if needed
|
||||||
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
|
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
|
||||||
// Put the message in the database
|
// 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
|
// Send it
|
||||||
MessageSender.send(message, viewModel.recipient.address, attachments, quote, linkPreview)
|
MessageSender.send(message, recipient.address, attachments, quote, linkPreview)
|
||||||
// Send a typing stopped message
|
// Send a typing stopped message
|
||||||
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
|
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
|
||||||
}
|
}
|
||||||
@ -1119,8 +1150,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun pickFromLibrary() {
|
private fun pickFromLibrary() {
|
||||||
|
val recipient = viewModel.recipient ?: return
|
||||||
binding?.inputBar?.text?.trim()?.let { text ->
|
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)
|
sendAttachments(slideDeck.asAttachments(), body)
|
||||||
}
|
}
|
||||||
INVITE_CONTACTS -> {
|
INVITE_CONTACTS -> {
|
||||||
if (!viewModel.recipient.isOpenGroupRecipient) { return }
|
if (viewModel.recipient?.isOpenGroupRecipient != true) { return }
|
||||||
val extras = intent?.extras ?: return
|
val extras = intent?.extras ?: return
|
||||||
if (!intent.hasExtra(selectedContactsKey)) { return }
|
if (!intent.hasExtra(selectedContactsKey)) { return }
|
||||||
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
|
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
|
||||||
@ -1268,13 +1300,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessages(messages: Set<MessageRecord>) {
|
override fun deleteMessages(messages: Set<MessageRecord>) {
|
||||||
|
val recipient = viewModel.recipient ?: return
|
||||||
if (!IS_UNSEND_REQUESTS_ENABLED) {
|
if (!IS_UNSEND_REQUESTS_ENABLED) {
|
||||||
deleteMessagesWithoutUnsendRequest(messages)
|
deleteMessagesWithoutUnsendRequest(messages)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val allSentByCurrentUser = messages.all { it.isOutgoing }
|
val allSentByCurrentUser = messages.all { it.isOutgoing }
|
||||||
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id) != null }
|
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id) != null }
|
||||||
if (viewModel.recipient.isOpenGroupRecipient) {
|
if (recipient.isOpenGroupRecipient) {
|
||||||
val messageCount = messages.size
|
val messageCount = messages.size
|
||||||
val builder = AlertDialog.Builder(this)
|
val builder = AlertDialog.Builder(this)
|
||||||
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||||
@ -1293,7 +1326,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
builder.show()
|
builder.show()
|
||||||
} else if (allSentByCurrentUser && allHasHash) {
|
} else if (allSentByCurrentUser && allHasHash) {
|
||||||
val bottomSheet = DeleteOptionsBottomSheet()
|
val bottomSheet = DeleteOptionsBottomSheet()
|
||||||
bottomSheet.recipient = viewModel.recipient
|
bottomSheet.recipient = recipient
|
||||||
bottomSheet.onDeleteForMeTapped = {
|
bottomSheet.onDeleteForMeTapped = {
|
||||||
for (message in messages) {
|
for (message in messages) {
|
||||||
viewModel.deleteLocally(message)
|
viewModel.deleteLocally(message)
|
||||||
@ -1452,16 +1485,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun reply(messages: Set<MessageRecord>) {
|
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()
|
endActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendMediaSavedNotification() {
|
private fun sendMediaSavedNotification() {
|
||||||
if (viewModel.recipient.isGroupRecipient) { return }
|
val recipient = viewModel.recipient ?: return
|
||||||
|
if (recipient.isGroupRecipient) { return }
|
||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
val kind = DataExtractionNotification.Kind.MediaSaved(timestamp)
|
val kind = DataExtractionNotification.Kind.MediaSaved(timestamp)
|
||||||
val message = DataExtractionNotification(kind)
|
val message = DataExtractionNotification(kind)
|
||||||
MessageSender.send(message, viewModel.recipient.address)
|
MessageSender.send(message, recipient.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun endActionMode() {
|
private fun endActionMode() {
|
||||||
|
@ -4,10 +4,25 @@ import android.app.AlertDialog
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import android.util.SparseArray
|
||||||
|
import android.util.SparseBooleanArray
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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 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.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.ControlMessageView
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
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,
|
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 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) {
|
: 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>()
|
var selectedItems = mutableSetOf<MessageRecord>()
|
||||||
private var searchQuery: String? = null
|
private var searchQuery: String? = null
|
||||||
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = 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) {
|
sealed class ViewType(val rawValue: Int) {
|
||||||
object Visible : ViewType(0)
|
object Visible : ViewType(0)
|
||||||
object Control : ViewType(1)
|
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)
|
class ControlMessageViewHolder(val view: ControlMessageView) : ViewHolder(view)
|
||||||
|
|
||||||
override fun getItemViewType(cursor: Cursor): Int {
|
override fun getItemViewType(cursor: Cursor): Int {
|
||||||
@ -52,7 +87,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
|||||||
@Suppress("NAME_SHADOWING")
|
@Suppress("NAME_SHADOWING")
|
||||||
val viewType = ViewType.allValues[viewType]
|
val viewType = ViewType.allValues[viewType]
|
||||||
return when (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))
|
ViewType.Control -> ControlMessageViewHolder(ControlMessageView(context))
|
||||||
else -> throw IllegalStateException("Unexpected view type: $viewType.")
|
else -> throw IllegalStateException("Unexpected view type: $viewType.")
|
||||||
}
|
}
|
||||||
@ -65,20 +100,31 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
|||||||
when (viewHolder) {
|
when (viewHolder) {
|
||||||
is VisibleMessageViewHolder -> {
|
is VisibleMessageViewHolder -> {
|
||||||
val view = viewHolder.view
|
val view = viewHolder.view
|
||||||
|
val visibleMessageView = ViewVisibleMessageBinding.bind(viewHolder.view).visibleMessageView
|
||||||
val isSelected = selectedItems.contains(message)
|
val isSelected = selectedItems.contains(message)
|
||||||
view.snIsSelected = isSelected
|
visibleMessageView.snIsSelected = isSelected
|
||||||
view.indexInAdapter = position
|
visibleMessageView.indexInAdapter = position
|
||||||
view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery)
|
val senderId = message.individualRecipient.address.serialize()
|
||||||
if (!message.isDeleted) {
|
val senderIdHash = senderId.hashCode()
|
||||||
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
|
updateQueue.trySend(senderId)
|
||||||
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault(senderIdHash, false)) {
|
||||||
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
getSenderInfo(senderId)?.let { contact ->
|
||||||
} else {
|
contactCache[senderIdHash] = contact
|
||||||
view.onPress = null
|
}
|
||||||
view.onSwipeToReply = null
|
|
||||||
view.onLongPress = null
|
|
||||||
}
|
}
|
||||||
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 -> {
|
is ControlMessageViewHolder -> {
|
||||||
viewHolder.view.bind(message, messageBefore)
|
viewHolder.view.bind(message, messageBefore)
|
||||||
@ -105,7 +151,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
|||||||
|
|
||||||
override fun onItemViewRecycled(viewHolder: ViewHolder?) {
|
override fun onItemViewRecycled(viewHolder: ViewHolder?) {
|
||||||
when (viewHolder) {
|
when (viewHolder) {
|
||||||
is VisibleMessageViewHolder -> viewHolder.view.recycle()
|
is VisibleMessageViewHolder -> viewHolder.view.findViewById<VisibleMessageView>(R.id.visibleMessageView).recycle()
|
||||||
is ControlMessageViewHolder -> viewHolder.view.recycle()
|
is ControlMessageViewHolder -> viewHolder.view.recycle()
|
||||||
}
|
}
|
||||||
super.onItemViewRecycled(viewHolder)
|
super.onItemViewRecycled(viewHolder)
|
||||||
|
@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@ -22,8 +23,8 @@ class ConversationViewModel(
|
|||||||
private val _uiState = MutableStateFlow(ConversationUiState())
|
private val _uiState = MutableStateFlow(ConversationUiState())
|
||||||
val uiState: StateFlow<ConversationUiState> = _uiState
|
val uiState: StateFlow<ConversationUiState> = _uiState
|
||||||
|
|
||||||
val recipient: Recipient
|
val recipient: Recipient?
|
||||||
get() = repository.getRecipientForThreadId(threadId)
|
get() = repository.maybeGetRecipientForThreadId(threadId)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
@ -44,20 +45,24 @@ class ConversationViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun unblock() {
|
fun unblock() {
|
||||||
|
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for unblock action")
|
||||||
if (recipient.isContactRecipient) {
|
if (recipient.isContactRecipient) {
|
||||||
repository.unblock(recipient)
|
repository.unblock(recipient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteLocally(message: MessageRecord) {
|
fun deleteLocally(message: MessageRecord) {
|
||||||
|
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for delete locally action")
|
||||||
repository.deleteLocally(recipient, message)
|
repository.deleteLocally(recipient, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setRecipientApproved() {
|
fun setRecipientApproved() {
|
||||||
|
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for set approved action")
|
||||||
repository.setApproved(recipient, true)
|
repository.setApproved(recipient, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
|
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
|
||||||
|
val recipient = recipient ?: return@launch
|
||||||
repository.deleteForEveryone(threadId, recipient, message)
|
repository.deleteForEveryone(threadId, recipient, message)
|
||||||
.onFailure {
|
.onFailure {
|
||||||
showMessage("Couldn't delete message due to error: $it")
|
showMessage("Couldn't delete message due to error: $it")
|
||||||
@ -92,6 +97,7 @@ class ConversationViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun acceptMessageRequest() = viewModelScope.launch {
|
fun acceptMessageRequest() = viewModelScope.launch {
|
||||||
|
val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for accept message request action")
|
||||||
repository.acceptMessageRequest(threadId, recipient)
|
repository.acceptMessageRequest(threadId, recipient)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
@ -104,6 +110,7 @@ class ConversationViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun declineMessageRequest() {
|
fun declineMessageRequest() {
|
||||||
|
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for decline message request action")
|
||||||
repository.declineMessageRequest(threadId, recipient)
|
repository.declineMessageRequest(threadId, recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ class AlbumThumbnailView : FrameLayout {
|
|||||||
this.slideSize = slides.size
|
this.slideSize = slides.size
|
||||||
}
|
}
|
||||||
// iterate binding
|
// iterate binding
|
||||||
slides.take(5).forEachIndexed { position, slide ->
|
slides.take(MAX_ALBUM_DISPLAY_SIZE).forEachIndexed { position, slide ->
|
||||||
val thumbnailView = getThumbnailView(position)
|
val thumbnailView = getThumbnailView(position)
|
||||||
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
|
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
|
||||||
}
|
}
|
||||||
|
@ -28,11 +28,11 @@ class MentionCandidateView : LinearLayout {
|
|||||||
|
|
||||||
private fun update() = with(binding) {
|
private fun update() = with(binding) {
|
||||||
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
||||||
profilePictureView.publicKey = mentionCandidate.publicKey
|
profilePictureView.root.publicKey = mentionCandidate.publicKey
|
||||||
profilePictureView.displayName = mentionCandidate.displayName
|
profilePictureView.root.displayName = mentionCandidate.displayName
|
||||||
profilePictureView.additionalPublicKey = null
|
profilePictureView.root.additionalPublicKey = null
|
||||||
profilePictureView.glide = glide!!
|
profilePictureView.root.glide = glide!!
|
||||||
profilePictureView.update()
|
profilePictureView.root.update()
|
||||||
if (openGroupServer != null && openGroupRoom != null) {
|
if (openGroupServer != null && openGroupRoom != null) {
|
||||||
val isUserModerator = OpenGroupAPIV2.isUserModerator(mentionCandidate.publicKey, openGroupRoom!!, openGroupServer!!)
|
val isUserModerator = OpenGroupAPIV2.isUserModerator(mentionCandidate.publicKey, openGroupRoom!!, openGroupServer!!)
|
||||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||||
|
@ -9,6 +9,7 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.DialogJoinOpenGroupBinding
|
import network.loki.messenger.databinding.DialogJoinOpenGroupBinding
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
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
|
val activity = requireContext() as AppCompatActivity
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
|
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
|
||||||
|
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(url)
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
|
||||||
}
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
|
@ -122,9 +122,12 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
linkPreview = null
|
linkPreview = null
|
||||||
linkPreviewDraftView = null
|
linkPreviewDraftView = null
|
||||||
binding.inputBarAdditionalContentContainer.removeAllViews()
|
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
|
quoteView.delegate = this
|
||||||
binding.inputBarAdditionalContentContainer.addView(quoteView)
|
binding.inputBarAdditionalContentContainer.addView(layout)
|
||||||
val attachments = (message as? MmsMessageRecord)?.slideDeck
|
val attachments = (message as? MmsMessageRecord)?.slideDeck
|
||||||
val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize()
|
val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize()
|
||||||
quoteView.bind(sender, message.body, attachments,
|
quoteView.bind(sender, message.body, attachments,
|
||||||
|
@ -28,11 +28,11 @@ class MentionCandidateView : RelativeLayout {
|
|||||||
|
|
||||||
private fun update() = with(binding) {
|
private fun update() = with(binding) {
|
||||||
mentionCandidateNameTextView.text = candidate.displayName
|
mentionCandidateNameTextView.text = candidate.displayName
|
||||||
profilePictureView.publicKey = candidate.publicKey
|
profilePictureView.root.publicKey = candidate.publicKey
|
||||||
profilePictureView.displayName = candidate.displayName
|
profilePictureView.root.displayName = candidate.displayName
|
||||||
profilePictureView.additionalPublicKey = null
|
profilePictureView.root.additionalPublicKey = null
|
||||||
profilePictureView.glide = glide!!
|
profilePictureView.root.glide = glide!!
|
||||||
profilePictureView.update()
|
profilePictureView.root.update()
|
||||||
if (openGroupServer != null && openGroupRoom != null) {
|
if (openGroupServer != null && openGroupRoom != null) {
|
||||||
val isUserModerator = OpenGroupAPIV2.isUserModerator(candidate.publicKey, openGroupRoom!!, openGroupServer!!)
|
val isUserModerator = OpenGroupAPIV2.isUserModerator(candidate.publicKey, openGroupRoom!!, openGroupServer!!)
|
||||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
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.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
@ -11,15 +10,12 @@ import network.loki.messenger.databinding.ViewDeletedMessageBinding
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
|
||||||
class DeletedMessageView : LinearLayout {
|
class DeletedMessageView : LinearLayout {
|
||||||
private lateinit var binding: ViewDeletedMessageBinding
|
private val binding: ViewDeletedMessageBinding by lazy { ViewDeletedMessageBinding.bind(this) }
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
private fun initialize() {
|
|
||||||
binding = ViewDeletedMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
}
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
|
@ -3,22 +3,17 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import network.loki.messenger.databinding.ViewDocumentBinding
|
import network.loki.messenger.databinding.ViewDocumentBinding
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
|
|
||||||
class DocumentView : LinearLayout {
|
class DocumentView : LinearLayout {
|
||||||
private lateinit var binding: ViewDocumentBinding
|
private val binding: ViewDocumentBinding by lazy { ViewDocumentBinding.bind(this) }
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
private fun initialize() {
|
|
||||||
binding = ViewDocumentBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
}
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@ -14,16 +13,12 @@ import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
|
||||||
class OpenGroupInvitationView : LinearLayout {
|
class OpenGroupInvitationView : LinearLayout {
|
||||||
private lateinit var binding: ViewOpenGroupInvitationBinding
|
private val binding: ViewOpenGroupInvitationBinding by lazy { ViewOpenGroupInvitationBinding.bind(this) }
|
||||||
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
|
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
|
||||||
|
|
||||||
constructor(context: Context): super(context) { initialize() }
|
constructor(context: Context): super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet?): super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
private fun initialize() {
|
|
||||||
binding = ViewOpenGroupInvitationBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||||
// FIXME: This is a really weird approach...
|
// 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.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.text.StaticLayout
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.core.content.res.use
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
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.SessionContactDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.Quote
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import javax.inject.Inject
|
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
|
// 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:
|
// 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
|
// • Quoted voice messages and documents in both private chats and group chats
|
||||||
// • All of the above in both dark mode and light mode
|
// • All of the above in both dark mode and light mode
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class QuoteView : LinearLayout {
|
class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) {
|
||||||
|
|
||||||
@Inject lateinit var contactDb: SessionContactDatabase
|
@Inject lateinit var contactDb: SessionContactDatabase
|
||||||
|
|
||||||
private lateinit var binding: ViewQuoteBinding
|
private val binding: ViewQuoteBinding by lazy { ViewQuoteBinding.bind(this) }
|
||||||
private lateinit var mode: Mode
|
|
||||||
private val vPadding by lazy { toPx(6, resources) }
|
private val vPadding by lazy { toPx(6, resources) }
|
||||||
var delegate: QuoteViewDelegate? = null
|
var delegate: QuoteViewDelegate? = null
|
||||||
|
private val mode: Mode
|
||||||
|
|
||||||
enum class Mode { Regular, Draft }
|
enum class Mode { Regular, Draft }
|
||||||
|
|
||||||
// region Lifecycle
|
init {
|
||||||
constructor(context: Context) : this(context, Mode.Regular)
|
mode = attrs?.let { attrSet ->
|
||||||
constructor(context: Context, attrs: AttributeSet) : this(context, Mode.Regular, attrs)
|
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) {
|
// region Lifecycle
|
||||||
this.mode = mode
|
override fun onFinishInflate() {
|
||||||
binding = ViewQuoteBinding.inflate(LayoutInflater.from(context), this, true)
|
super.onFinishInflate()
|
||||||
// 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)
|
|
||||||
when (mode) {
|
when (mode) {
|
||||||
Mode.Draft -> binding.quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
|
Mode.Draft -> binding.quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
|
||||||
Mode.Regular -> {
|
Mode.Regular -> {
|
||||||
@ -66,44 +63,6 @@ class QuoteView : LinearLayout {
|
|||||||
}
|
}
|
||||||
// endregion
|
// 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
|
// region Updating
|
||||||
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
|
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
|
||||||
isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long,
|
isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long,
|
||||||
@ -115,7 +74,7 @@ class QuoteView : LinearLayout {
|
|||||||
// Author
|
// Author
|
||||||
if (thread.isGroupRecipient) {
|
if (thread.isGroupRecipient) {
|
||||||
val author = contactDb.getContactWithSessionID(authorPublicKey)
|
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.text = authorDisplayName
|
||||||
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
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
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@ -14,7 +13,7 @@ import org.thoughtcrime.securesms.util.ActivityDispatcher
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class UntrustedAttachmentView: LinearLayout {
|
class UntrustedAttachmentView: LinearLayout {
|
||||||
private lateinit var binding: ViewUntrustedAttachmentBinding
|
private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) }
|
||||||
enum class AttachmentType {
|
enum class AttachmentType {
|
||||||
AUDIO,
|
AUDIO,
|
||||||
DOCUMENT,
|
DOCUMENT,
|
||||||
@ -22,13 +21,10 @@ class UntrustedAttachmentView: LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
private fun initialize() {
|
|
||||||
binding = ViewUntrustedAttachmentBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
}
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
|
@ -5,7 +5,6 @@ import android.graphics.Color
|
|||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.StaticLayout
|
|
||||||
import android.text.style.BackgroundColorSpan
|
import android.text.style.BackgroundColorSpan
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
@ -28,6 +27,11 @@ import androidx.core.view.isVisible
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||||
import okhttp3.HttpUrl
|
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.ThemeUtil
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
@ -65,7 +69,7 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean,
|
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
|
// Background
|
||||||
val background = getBackground(message.isOutgoing, isStartOfMessageCluster, isEndOfMessageCluster)
|
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
|
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
|
onContentDoubleTap = null
|
||||||
|
|
||||||
if (message.isDeleted) {
|
if (message.isDeleted) {
|
||||||
binding.deletedMessageView.isVisible = true
|
binding.deletedMessageView.root.isVisible = true
|
||||||
binding.deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message))
|
binding.deletedMessageView.root.bind(message, VisibleMessageContentView.getTextColor(context,message))
|
||||||
return
|
return
|
||||||
} else {
|
} 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()
|
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
|
linkPreviewLayout.width = if (mediaThumbnailMessage) 0 else ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
binding.linkPreviewView.layoutParams = linkPreviewLayout
|
binding.linkPreviewView.layoutParams = linkPreviewLayout
|
||||||
|
|
||||||
binding.untrustedView.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty()
|
binding.untrustedView.root.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty()
|
||||||
binding.voiceMessageView.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
|
binding.voiceMessageView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
|
||||||
binding.documentView.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
|
binding.documentView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
|
||||||
binding.albumThumbnailView.isVisible = mediaThumbnailMessage
|
binding.albumThumbnailView.isVisible = mediaThumbnailMessage
|
||||||
binding.openGroupInvitationView.isVisible = message.isOpenGroupInvitation
|
binding.openGroupInvitationView.root.isVisible = message.isOpenGroupInvitation
|
||||||
|
|
||||||
var hideBody = false
|
var hideBody = false
|
||||||
|
|
||||||
if (message is MmsMessageRecord && message.quote != null) {
|
if (message is MmsMessageRecord && message.quote != null) {
|
||||||
binding.quoteView.isVisible = true
|
binding.quoteView.root.isVisible = true
|
||||||
val quote = message.quote!!
|
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) {
|
val quoteText = if (quote.isOriginalMissing) {
|
||||||
context.getString(R.string.QuoteView_original_missing)
|
context.getString(R.string.QuoteView_original_missing)
|
||||||
} else {
|
} else {
|
||||||
quote.text
|
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,
|
message.isOutgoing, message.isOpenGroupInvitation, message.threadId,
|
||||||
quote.isOriginalMissing, glide)
|
quote.isOriginalMissing, glide)
|
||||||
onContentClick.add { event ->
|
onContentClick.add { event ->
|
||||||
val r = Rect()
|
val r = Rect()
|
||||||
binding.quoteView.getGlobalVisibleRect(r)
|
binding.quoteView.root.getGlobalVisibleRect(r)
|
||||||
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
|
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
|
||||||
delegate?.scrollToMessageIfPossible(quote.id)
|
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()) {
|
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
||||||
@ -138,26 +164,26 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
hideBody = true
|
hideBody = true
|
||||||
// Audio attachment
|
// Audio attachment
|
||||||
if (contactIsTrusted || message.isOutgoing) {
|
if (contactIsTrusted || message.isOutgoing) {
|
||||||
binding.voiceMessageView.indexInAdapter = indexInAdapter
|
binding.voiceMessageView.root.indexInAdapter = indexInAdapter
|
||||||
binding.voiceMessageView.delegate = context as? ConversationActivityV2
|
binding.voiceMessageView.root.delegate = context as? ConversationActivityV2
|
||||||
binding.voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
|
binding.voiceMessageView.root.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||||
// We have to use onContentClick (rather than a click listener directly on the voice
|
// 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.
|
// message view) so as to not interfere with all the other gestures.
|
||||||
onContentClick.add { binding.voiceMessageView.togglePlayback() }
|
onContentClick.add { binding.voiceMessageView.root.togglePlayback() }
|
||||||
onContentDoubleTap = { binding.voiceMessageView.handleDoubleTap() }
|
onContentDoubleTap = { binding.voiceMessageView.root.handleDoubleTap() }
|
||||||
} else {
|
} else {
|
||||||
// TODO: move this out to its own area
|
// TODO: move this out to its own area
|
||||||
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
|
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
|
||||||
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
||||||
}
|
}
|
||||||
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
|
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
|
||||||
hideBody = true
|
hideBody = true
|
||||||
// Document attachment
|
// Document attachment
|
||||||
if (contactIsTrusted || message.isOutgoing) {
|
if (contactIsTrusted || message.isOutgoing) {
|
||||||
binding.documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
binding.documentView.root.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||||
} else {
|
} else {
|
||||||
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
|
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
|
||||||
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
||||||
}
|
}
|
||||||
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
|
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
|
||||||
/*
|
/*
|
||||||
@ -178,34 +204,21 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
} else {
|
} else {
|
||||||
hideBody = true
|
hideBody = true
|
||||||
binding.albumThumbnailView.clearViews()
|
binding.albumThumbnailView.clearViews()
|
||||||
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
||||||
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
||||||
}
|
}
|
||||||
} else if (message.isOpenGroupInvitation) {
|
} else if (message.isOpenGroupInvitation) {
|
||||||
hideBody = true
|
hideBody = true
|
||||||
binding.openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
binding.openGroupInvitationView.root.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||||
onContentClick.add { binding.openGroupInvitationView.joinOpenGroup() }
|
onContentClick.add { binding.openGroupInvitationView.root.joinOpenGroup() }
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.bodyTextView.isVisible = message.body.isNotEmpty() && !hideBody
|
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
|
// set it to use constraints if not only a text message, otherwise wrap content to whatever width it wants
|
||||||
val params = binding.bodyTextView.layoutParams
|
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.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) {
|
if (message.body.isNotEmpty() && !hideBody) {
|
||||||
val color = getTextColor(context, message)
|
val color = getTextColor(context, message)
|
||||||
@ -222,7 +235,7 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
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 {
|
private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable {
|
||||||
val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster)
|
val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster)
|
||||||
@ -245,20 +258,20 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
arrayOf(
|
arrayOf(
|
||||||
binding.deletedMessageView,
|
binding.deletedMessageView.root,
|
||||||
binding.untrustedView,
|
binding.untrustedView.root,
|
||||||
binding.voiceMessageView,
|
binding.voiceMessageView.root,
|
||||||
binding.openGroupInvitationView,
|
binding.openGroupInvitationView.root,
|
||||||
binding.documentView,
|
binding.documentView.root,
|
||||||
binding.quoteView,
|
binding.quoteView.root,
|
||||||
binding.linkPreviewView,
|
binding.linkPreviewView,
|
||||||
binding.albumThumbnailView,
|
binding.albumThumbnailView,
|
||||||
binding.bodyTextView
|
binding.bodyTextView
|
||||||
).forEach { view -> view.isVisible = false }
|
).forEach { view: View -> view.isVisible = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playVoiceMessage() {
|
fun playVoiceMessage() {
|
||||||
binding.voiceMessageView.togglePlayback()
|
binding.voiceMessageView.root.togglePlayback()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
@ -8,14 +8,12 @@ import android.graphics.drawable.ColorDrawable
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
@ -23,6 +21,7 @@ import androidx.core.view.isVisible
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||||
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.utilities.ViewUtil
|
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.LokiThreadDatabase
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
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.toDp
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@ -54,13 +51,12 @@ import kotlin.math.sqrt
|
|||||||
class VisibleMessageView : LinearLayout {
|
class VisibleMessageView : LinearLayout {
|
||||||
|
|
||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
@Inject lateinit var contactDb: SessionContactDatabase
|
|
||||||
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
|
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
|
||||||
@Inject lateinit var mmsSmsDb: MmsSmsDatabase
|
@Inject lateinit var mmsSmsDb: MmsSmsDatabase
|
||||||
@Inject lateinit var smsDb: SmsDatabase
|
@Inject lateinit var smsDb: SmsDatabase
|
||||||
@Inject lateinit var mmsDb: MmsDatabase
|
@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 screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||||
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
||||||
private val swipeToReplyIconRect = Rect()
|
private val swipeToReplyIconRect = Rect()
|
||||||
@ -75,7 +71,6 @@ class VisibleMessageView : LinearLayout {
|
|||||||
var snIsSelected = false
|
var snIsSelected = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
binding.messageTimestampTextView.isVisible = isSelected
|
|
||||||
handleIsSelectedChanged()
|
handleIsSelectedChanged()
|
||||||
}
|
}
|
||||||
var onPress: ((event: MotionEvent) -> Unit)? = null
|
var onPress: ((event: MotionEvent) -> Unit)? = null
|
||||||
@ -91,73 +86,84 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
|
override fun onFinishInflate() {
|
||||||
|
super.onFinishInflate()
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
private fun 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
|
isHapticFeedbackEnabled = true
|
||||||
setWillNotDraw(false)
|
setWillNotDraw(false)
|
||||||
binding.expirationTimerViewContainer.disableClipping()
|
binding.expirationTimerViewContainer.disableClipping()
|
||||||
binding.messageContentContainer.disableClipping()
|
binding.messageContentView.disableClipping()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?, glide: GlideRequests, searchQuery: String?) {
|
fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?,
|
||||||
val sender = message.individualRecipient
|
glide: GlideRequests, searchQuery: String?, contact: Contact?, senderSessionID: String,
|
||||||
val senderSessionID = sender.address.serialize()
|
) {
|
||||||
val threadID = message.threadId
|
val threadID = message.threadId
|
||||||
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
||||||
val contact = contactDb.getContactWithSessionID(senderSessionID)
|
|
||||||
val isGroupThread = thread.isGroupRecipient
|
val isGroupThread = thread.isGroupRecipient
|
||||||
val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread)
|
val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread)
|
||||||
val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread)
|
val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread)
|
||||||
// Show profile picture and sender name if this is a group thread AND
|
// Show profile picture and sender name if this is a group thread AND
|
||||||
// the message is incoming
|
// 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) {
|
if (isGroupThread && !message.isOutgoing) {
|
||||||
binding.profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
|
if (isEndOfMessageCluster) {
|
||||||
binding.profilePictureView.publicKey = senderSessionID
|
binding.profilePictureView.root.publicKey = senderSessionID
|
||||||
binding.profilePictureView.glide = glide
|
binding.profilePictureView.root.glide = glide
|
||||||
binding.profilePictureView.update(message.individualRecipient)
|
binding.profilePictureView.root.update(message.individualRecipient)
|
||||||
binding.profilePictureView.setOnClickListener {
|
binding.profilePictureView.root.setOnClickListener {
|
||||||
showUserDetails(senderSessionID, threadID)
|
showUserDetails(senderSessionID, threadID)
|
||||||
}
|
}
|
||||||
if (thread.isOpenGroupRecipient) {
|
if (thread.isOpenGroupRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||||
val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server)
|
val isModerator = OpenGroupAPIV2.isUserModerator(
|
||||||
binding.moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
|
senderSessionID,
|
||||||
} else {
|
openGroup.room,
|
||||||
binding.moderatorIconImageView.visibility = View.INVISIBLE
|
openGroup.server
|
||||||
|
)
|
||||||
|
binding.moderatorIconImageView.isVisible = !message.isOutgoing && isModerator
|
||||||
|
}
|
||||||
}
|
}
|
||||||
binding.senderNameTextView.isVisible = isStartOfMessageCluster
|
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
|
binding.senderNameTextView.text = contact?.displayName(context) ?: senderSessionID
|
||||||
} else {
|
} else {
|
||||||
binding.profilePictureContainer.visibility = View.GONE
|
|
||||||
binding.senderNameTextView.visibility = View.GONE
|
binding.senderNameTextView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
// Date break
|
// Date break
|
||||||
binding.dateBreakTextView.showDateBreak(message, previous)
|
binding.dateBreakTextView.showDateBreak(message, previous)
|
||||||
// Timestamp
|
// Timestamp
|
||||||
binding.messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.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)
|
|
||||||
// Set inter-message spacing
|
// 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
|
// Message status indicator
|
||||||
val (iconID, iconColor) = getMessageStatusImage(message)
|
val (iconID, iconColor) = getMessageStatusImage(message)
|
||||||
if (iconID != null) {
|
if (iconID != null) {
|
||||||
@ -169,29 +175,29 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
||||||
binding.messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
|
binding.messageStatusImageView.isVisible =
|
||||||
|
!message.isSent || message.id == lastMessageID
|
||||||
} else {
|
} else {
|
||||||
binding.messageStatusImageView.isVisible = false
|
binding.messageStatusImageView.isVisible = false
|
||||||
}
|
}
|
||||||
// Expiration timer
|
// Expiration timer
|
||||||
updateExpirationTimer(message)
|
updateExpirationTimer(message)
|
||||||
// Calculate max message bubble width
|
// Calculate max message bubble width
|
||||||
var maxWidth = screenWidth - startPadding - endPadding
|
|
||||||
if (binding.profilePictureContainer.visibility != View.GONE) { maxWidth -= binding.profilePictureContainer.width }
|
|
||||||
// Populate content view
|
// Populate content view
|
||||||
binding.messageContentView.indexInAdapter = indexInAdapter
|
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
|
binding.messageContentView.delegate = contentViewDelegate
|
||||||
onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() }
|
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 {
|
private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean {
|
||||||
return if (isGroupThread) {
|
return if (isGroupThread) {
|
||||||
previous == null || previous.isUpdate || !DateUtils.isSameHour(current.timestamp, previous.timestamp)
|
previous == null || previous.isUpdate || !DateUtils.isSameHour(current.timestamp, previous.timestamp)
|
||||||
@ -223,18 +229,17 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateExpirationTimer(message: MessageRecord) {
|
private fun updateExpirationTimer(message: MessageRecord) {
|
||||||
val expirationTimerViewLayoutParams = binding.expirationTimerView.layoutParams as MarginLayoutParams
|
|
||||||
val container = binding.expirationTimerViewContainer
|
val container = binding.expirationTimerViewContainer
|
||||||
val content = binding.messageContentView
|
val content = binding.messageContentView
|
||||||
val expiration = binding.expirationTimerView
|
val expiration = binding.expirationTimerView
|
||||||
|
val spacing = binding.messageContentSpacing
|
||||||
container.removeAllViewsInLayout()
|
container.removeAllViewsInLayout()
|
||||||
container.addView(if (message.isOutgoing) expiration else content)
|
container.addView(if (message.isOutgoing) expiration else content)
|
||||||
container.addView(if (message.isOutgoing) content else expiration)
|
container.addView(if (message.isOutgoing) content else expiration)
|
||||||
val expirationTimerViewSize = toPx(12, resources)
|
container.addView(spacing, if (message.isOutgoing) 0 else 2)
|
||||||
val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt()
|
val containerParams = container.layoutParams as ConstraintLayout.LayoutParams
|
||||||
expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0
|
containerParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||||
expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize)
|
container.layoutParams = containerParams
|
||||||
binding.expirationTimerView.layoutParams = expirationTimerViewLayoutParams
|
|
||||||
if (message.expiresIn > 0 && !message.isPending) {
|
if (message.expiresIn > 0 && !message.isPending) {
|
||||||
binding.expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
|
binding.expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
|
||||||
binding.expirationTimerView.isVisible = true
|
binding.expirationTimerView.isVisible = true
|
||||||
@ -279,9 +284,9 @@ class VisibleMessageView : LinearLayout {
|
|||||||
val threshold = swipeToReplyThreshold
|
val threshold = swipeToReplyThreshold
|
||||||
val iconSize = toPx(24, context.resources)
|
val iconSize = toPx(24, context.resources)
|
||||||
val bottomVOffset = paddingBottom + binding.messageStatusImageView.height + (binding.messageContentView.height - iconSize) / 2
|
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.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
|
swipeToReplyIconRect.bottom = height - bottomVOffset
|
||||||
swipeToReplyIcon.bounds = swipeToReplyIconRect
|
swipeToReplyIcon.bounds = swipeToReplyIconRect
|
||||||
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
|
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
|
||||||
@ -293,7 +298,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
binding.profilePictureView.recycle()
|
binding.profilePictureView.root.recycle()
|
||||||
binding.messageContentView.recycle()
|
binding.messageContentView.recycle()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
@ -3,9 +3,7 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@ -21,12 +19,13 @@ import java.util.concurrent.TimeUnit
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener {
|
||||||
|
|
||||||
@Inject lateinit var attachmentDb: AttachmentDatabase
|
@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 val cornerMask by lazy { CornerMask(this) }
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -40,16 +39,17 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
|||||||
var indexInAdapter = -1
|
var indexInAdapter = -1
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
private fun initialize() {
|
override fun onFinishInflate() {
|
||||||
binding = ViewVoiceMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
super.onFinishInflate()
|
||||||
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||||
TimeUnit.MILLISECONDS.toMinutes(0),
|
TimeUnit.MILLISECONDS.toMinutes(0),
|
||||||
TimeUnit.MILLISECONDS.toSeconds(0))
|
TimeUnit.MILLISECONDS.toSeconds(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
|
@ -140,7 +140,7 @@ open class KThumbnailView: FrameLayout {
|
|||||||
val dimens = dimensDelegate.resourceSize()
|
val dimens = dimensDelegate.resourceSize()
|
||||||
|
|
||||||
val request = glide.load(DecryptableUri(slide.thumbnailUri!!))
|
val request = glide.load(DecryptableUri(slide.thumbnailUri!!))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.let { request ->
|
.let { request ->
|
||||||
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
|
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
|
||||||
request.override(getDefaultWidth(), getDefaultHeight())
|
request.override(getDefaultWidth(), getDefaultHeight())
|
||||||
|
@ -20,14 +20,26 @@ object MentionUtilities {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String {
|
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
|
@JvmStatic
|
||||||
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString {
|
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString {
|
||||||
@Suppress("NAME_SHADOWING") var text = text
|
|
||||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||||
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
|
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]*")
|
val pattern = Pattern.compile("@[0-9a-fA-F]*")
|
||||||
var matcher = pattern.matcher(text)
|
var matcher = pattern.matcher(text)
|
||||||
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
|
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) {
|
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||||
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
|
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.transition(withCrossFade()), new CenterCrop());
|
.transition(withCrossFade()), new CenterCrop());
|
||||||
|
|
||||||
if (slide.isInProgress()) return request;
|
if (slide.isInProgress()) return request;
|
||||||
|
@ -52,6 +52,7 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
|||||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
import org.thoughtcrime.securesms.database.model.MmsAttachmentInfo;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
@ -67,6 +68,7 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@ -266,6 +268,33 @@ public class AttachmentDatabase extends Database {
|
|||||||
return attachments;
|
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")
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
void deleteAttachmentsForMessage(long mmsId) {
|
void deleteAttachmentsForMessage(long mmsId) {
|
||||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||||
@ -327,6 +356,30 @@ public class AttachmentDatabase extends Database {
|
|||||||
notifyAttachmentListeners();
|
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")
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
private void deleteAttachmentOnDisk(@Nullable String data, @Nullable String thumbnail, @Nullable String contentType) {
|
private void deleteAttachmentOnDisk(@Nullable String data, @Nullable String thumbnail, @Nullable String contentType) {
|
||||||
if (!TextUtils.isEmpty(data)) {
|
if (!TextUtils.isEmpty(data)) {
|
||||||
|
@ -2,15 +2,13 @@ package org.thoughtcrime.securesms.database
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import org.session.libsession.utilities.Debouncer
|
import org.session.libsession.utilities.Debouncer
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
|
||||||
class ConversationNotificationDebouncer(private val context: Context) {
|
class ConversationNotificationDebouncer(private val context: Context) {
|
||||||
private val threadIDs = mutableSetOf<Long>()
|
private val threadIDs = mutableSetOf<Long>()
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = (context.applicationContext as ApplicationContext).conversationListNotificationHandler
|
||||||
private val debouncer = Debouncer(handler, 250);
|
private val debouncer = Debouncer(handler, 1000)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
@ -29,7 +27,7 @@ class ConversationNotificationDebouncer(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun publish() {
|
private fun publish() {
|
||||||
for (threadID in threadIDs) {
|
for (threadID in threadIDs.toList()) {
|
||||||
context.contentResolver.notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadID), null)
|
context.contentResolver.notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadID), null)
|
||||||
}
|
}
|
||||||
threadIDs.clear()
|
threadIDs.clear()
|
||||||
|
@ -23,7 +23,7 @@ import android.database.Cursor;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
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.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
@ -32,16 +32,21 @@ import java.util.Set;
|
|||||||
public abstract class Database {
|
public abstract class Database {
|
||||||
|
|
||||||
protected static final String ID_WHERE = "_id = ?";
|
protected static final String ID_WHERE = "_id = ?";
|
||||||
|
protected static final String ID_IN = "_id IN (?)";
|
||||||
|
|
||||||
protected SQLCipherOpenHelper databaseHelper;
|
protected SQLCipherOpenHelper databaseHelper;
|
||||||
protected final Context context;
|
protected final Context context;
|
||||||
private final Debouncer conversationListNotificationDebouncer;
|
private final WindowDebouncer conversationListNotificationDebouncer;
|
||||||
|
private final Runnable conversationListUpdater;
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
public Database(Context context, SQLCipherOpenHelper databaseHelper) {
|
public Database(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.conversationListUpdater = () -> {
|
||||||
|
context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
|
||||||
|
};
|
||||||
this.databaseHelper = databaseHelper;
|
this.databaseHelper = databaseHelper;
|
||||||
this.conversationListNotificationDebouncer = new Debouncer(ApplicationContext.getInstance(context).getConversationListNotificationHandler(), 250);
|
this.conversationListNotificationDebouncer = ApplicationContext.getInstance(context).getConversationListDebouncer();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyConversationListeners(Set<Long> threadIds) {
|
protected void notifyConversationListeners(Set<Long> threadIds) {
|
||||||
@ -54,7 +59,7 @@ public abstract class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyConversationListListeners() {
|
protected void notifyConversationListListeners() {
|
||||||
conversationListNotificationDebouncer.publish(()->context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null));
|
conversationListNotificationDebouncer.publish(conversationListUpdater);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyStickerListeners() {
|
protected void notifyStickerListeners() {
|
||||||
|
@ -4,6 +4,7 @@ import android.content.ContentProvider;
|
|||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ public class DatabaseContentProviders {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreate() {
|
public boolean onCreate() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -36,11 +36,12 @@ fun <T> SQLiteDatabase.getAll(table: String, query: String?, arguments: Array<St
|
|||||||
return listOf()
|
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()
|
val id = insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE).toInt()
|
||||||
if (id == -1) {
|
if (id == -1) {
|
||||||
update(table, values, query, arguments)
|
return update(table, values, query, arguments)
|
||||||
}
|
}
|
||||||
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Cursor.getInt(columnName: String): Int {
|
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());
|
recipient.setParticipants(Stream.of(members).map(memberAddress -> Recipient.from(context, memberAddress, true)).toList());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
notifyConversationListeners(threadId);
|
||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
return threadId;
|
return threadId;
|
||||||
}
|
}
|
||||||
@ -314,6 +315,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||||||
new String[] {groupID});
|
new String[] {groupID});
|
||||||
|
|
||||||
Recipient.applyCached(Address.fromSerialized(groupID), recipient -> recipient.setGroupAvatarId(avatarId == 0 ? null : avatarId));
|
Recipient.applyCached(Address.fromSerialized(groupID), recipient -> recipient.setGroupAvatarId(avatarId == 0 ? null : avatarId));
|
||||||
|
notifyConversationListListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateMembers(String groupId, List<Address> members) {
|
public void updateMembers(String groupId, List<Address> members) {
|
||||||
|
@ -4,13 +4,13 @@ package org.thoughtcrime.securesms.database;
|
|||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -92,6 +92,19 @@ public class GroupReceiptDatabase extends Database {
|
|||||||
return results;
|
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) {
|
void deleteRowsForMessage(long mmsId) {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});
|
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? {
|
override fun getLastMessageHashValue(snode: Snode, publicKey: String, namespace: Int): String? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ? AND $lastMessageHashNamespace = ?"
|
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))
|
cursor.getString(cursor.getColumnIndexOrThrow(lastMessageHashValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -279,7 +279,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
lastMessageHashNamespace to namespace.toString()
|
lastMessageHashNamespace to namespace.toString()
|
||||||
))
|
))
|
||||||
val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ? AND $lastMessageHashNamespace = ?"
|
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>? {
|
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.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
|
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
|
||||||
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
|
|
||||||
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
||||||
|
|
||||||
@ -77,6 +77,9 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
database.endTransaction()
|
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>? {
|
fun getMessageID(serverID: Long, threadID: Long): Pair<Long, Boolean>? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
val mappingResult = database.get(messageThreadMappingTable, "${Companion.serverID} = ? AND ${Companion.threadID} = ?",
|
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 android.content.Context
|
||||||
import net.sqlcipher.Cursor
|
import net.sqlcipher.Cursor
|
||||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
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.GroupAvatarDownloadJob
|
||||||
import org.session.libsession.messaging.jobs.Job
|
import org.session.libsession.messaging.jobs.Job
|
||||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||||
@ -135,6 +136,13 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
job.failureCount = cursor.getInt(failureCount)
|
job.failureCount = cursor.getInt(failureCount)
|
||||||
return job
|
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 {
|
object SessionJobHelper {
|
||||||
|
@ -362,7 +362,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
return new Pair<>(messageId, threadId);
|
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()) {
|
if (message.isSecureMessage()) {
|
||||||
type |= Types.SECURE_MESSAGE_BIT;
|
type |= Types.SECURE_MESSAGE_BIT;
|
||||||
} else if (message.isGroup()) {
|
} else if (message.isGroup()) {
|
||||||
@ -440,11 +440,13 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
long messageId = db.insert(TABLE_NAME, null, values);
|
long messageId = db.insert(TABLE_NAME, null, values);
|
||||||
|
|
||||||
if (unread) {
|
if (unread && runIncrement) {
|
||||||
DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1);
|
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) {
|
if (message.getSubscriptionId() != -1) {
|
||||||
DatabaseComponent.get(context).recipientDatabase().setDefaultSubscriptionId(recipient, message.getSubscriptionId());
|
DatabaseComponent.get(context).recipientDatabase().setDefaultSubscriptionId(recipient, message.getSubscriptionId());
|
||||||
@ -456,23 +458,23 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message) {
|
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, boolean runIncrement, boolean runThreadUpdate) {
|
||||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0);
|
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runIncrement, runThreadUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<InsertResult> insertCallMessage(IncomingTextMessage message) {
|
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) {
|
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
|
||||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp);
|
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) {
|
if (threadId == -1) {
|
||||||
threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(message.getRecipient());
|
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) {
|
if (messageId == -1) {
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
}
|
}
|
||||||
@ -481,7 +483,8 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public long insertMessageOutbox(long threadId, OutgoingTextMessage message,
|
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;
|
long type = Types.BASE_SENDING_TYPE;
|
||||||
|
|
||||||
@ -517,7 +520,9 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
insertListener.onComplete();
|
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().setLastSeen(threadId);
|
||||||
|
|
||||||
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true);
|
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.JobQueue
|
||||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
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.ConfigurationMessage
|
||||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage
|
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage
|
||||||
@ -102,7 +101,29 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return database.getAttachmentsForMessage(messageID)
|
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
|
var messageID: Long? = null
|
||||||
val senderAddress = Address.fromSerialized(message.sender!!)
|
val senderAddress = Address.fromSerialized(message.sender!!)
|
||||||
val isUserSender = (message.sender!! == getUserPublicKey())
|
val isUserSender = (message.sender!! == getUserPublicKey())
|
||||||
@ -139,14 +160,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
|
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
|
||||||
val insertResult = if (message.sender == getUserPublicKey()) {
|
val insertResult = if (message.sender == getUserPublicKey()) {
|
||||||
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
|
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 {
|
} else {
|
||||||
// It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment
|
// It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment
|
||||||
val signalServiceAttachments = attachments.mapNotNull {
|
val signalServiceAttachments = attachments.mapNotNull {
|
||||||
it.toSignalPointer()
|
it.toSignalPointer()
|
||||||
}
|
}
|
||||||
val mediaMessage = IncomingMediaMessage.from(message, senderAddress, targetRecipient.expireMessages * 1000L, group, signalServiceAttachments, quote, linkPreviews)
|
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) {
|
if (insertResult.isPresent) {
|
||||||
messageID = insertResult.get().messageId
|
messageID = insertResult.get().messageId
|
||||||
@ -158,12 +179,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
val insertResult = if (message.sender == getUserPublicKey()) {
|
val insertResult = if (message.sender == getUserPublicKey()) {
|
||||||
val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, targetRecipient, message.sentTimestamp)
|
val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, targetRecipient, message.sentTimestamp)
|
||||||
else OutgoingTextMessage.from(message, targetRecipient)
|
else OutgoingTextMessage.from(message, targetRecipient)
|
||||||
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!)
|
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!, runThreadUpdate)
|
||||||
} else {
|
} else {
|
||||||
val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, senderAddress, message.sentTimestamp)
|
val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, senderAddress, message.sentTimestamp)
|
||||||
else IncomingTextMessage.from(message, senderAddress, group, targetRecipient.expireMessages * 1000L)
|
else IncomingTextMessage.from(message, senderAddress, group, targetRecipient.expireMessages * 1000L)
|
||||||
val encrypted = IncomingEncryptedMessage(textMessage, textMessage.messageBody)
|
val encrypted = IncomingEncryptedMessage(textMessage, textMessage.messageBody)
|
||||||
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0)
|
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate)
|
||||||
}
|
}
|
||||||
insertResult.orNull()?.let { result ->
|
insertResult.orNull()?.let { result ->
|
||||||
messageID = result.messageId
|
messageID = result.messageId
|
||||||
@ -171,8 +192,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
val threadID = message.threadID
|
val threadID = message.threadID
|
||||||
// open group trim thread job is scheduled after processing in OpenGroupPollerV2
|
// open group trim thread job is scheduled after processing in OpenGroupPollerV2
|
||||||
if (openGroupID.isNullOrEmpty() && threadID != null && threadID >= 0) {
|
if (openGroupID.isNullOrEmpty() && threadID != null && threadID >= 0 && TextSecurePreferences.isThreadLengthTrimmingEnabled(context)) {
|
||||||
JobQueue.shared.add(TrimThreadJob(threadID))
|
JobQueue.shared.queueThreadForTrim(threadID)
|
||||||
}
|
}
|
||||||
message.serverHash?.let { serverHash ->
|
message.serverHash?.let { serverHash ->
|
||||||
messageID?.let { id ->
|
messageID?.let { id ->
|
||||||
@ -436,7 +457,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
|
val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
|
||||||
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
|
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
|
||||||
val smsDB = DatabaseComponent.get(context).smsDatabase()
|
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) {
|
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 mmsDB = DatabaseComponent.get(context).mmsDatabase()
|
||||||
val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return
|
if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return
|
||||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
|
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true)
|
||||||
mmsDB.markAsSent(infoMessageID, true)
|
mmsDB.markAsSent(infoMessageID, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,6 +540,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
OpenGroupManager.addOpenGroup(urlAsString, 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) {
|
override fun setProfileSharing(address: Address, value: Boolean) {
|
||||||
val recipient = Recipient.from(context, address, false)
|
val recipient = Recipient.from(context, address, false)
|
||||||
DatabaseComponent.get(context).recipientDatabase().setProfileSharing(recipient, value)
|
DatabaseComponent.get(context).recipientDatabase().setProfileSharing(recipient, value)
|
||||||
@ -667,7 +698,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
Optional.of(message)
|
Optional.of(message)
|
||||||
)
|
)
|
||||||
|
|
||||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1)
|
database.insertSecureDecryptedMessageInbox(mediaMessage, -1, runIncrement = true, runThreadUpdate = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun insertMessageRequestResponse(response: MessageRequestResponse) {
|
override fun insertMessageRequestResponse(response: MessageRequestResponse) {
|
||||||
@ -705,7 +736,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
Optional.absent()
|
Optional.absent()
|
||||||
)
|
)
|
||||||
val threadId = getOrCreateThreadIdFor(senderAddress)
|
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);
|
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) {
|
public long getOrCreateThreadIdFor(Recipient recipient, int distributionType) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
String where = ADDRESS + " = ?";
|
String where = ADDRESS + " = ?";
|
||||||
|
@ -10,6 +10,7 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||||||
import net.sqlcipher.database.SQLiteDatabaseHook;
|
import net.sqlcipher.database.SQLiteDatabaseHook;
|
||||||
import net.sqlcipher.database.SQLiteOpenHelper;
|
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
@ -86,6 +87,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
public void postKey(SQLiteDatabase db) {
|
public void postKey(SQLiteDatabase db) {
|
||||||
db.rawExecSQL("PRAGMA kdf_iter = '1';");
|
db.rawExecSQL("PRAGMA kdf_iter = '1';");
|
||||||
db.rawExecSQL("PRAGMA cipher_page_size = 4096;");
|
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(GroupDatabase.getCreateUpdatedTimestampCommand());
|
||||||
db.execSQL(RecipientDatabase.getCreateApprovedCommand());
|
db.execSQL(RecipientDatabase.getCreateApprovedCommand());
|
||||||
db.execSQL(RecipientDatabase.getCreateApprovedMeCommand());
|
db.execSQL(RecipientDatabase.getCreateApprovedMeCommand());
|
||||||
db.execSQL(MmsDatabase.getCreateMessageRequestResponseCommand());
|
db.execSQL(MmsDatabase.createMessageRequestResponseCommand);
|
||||||
db.execSQL(LokiAPIDatabase.CREATE_FORK_INFO_TABLE_COMMAND);
|
db.execSQL(LokiAPIDatabase.CREATE_FORK_INFO_TABLE_COMMAND);
|
||||||
db.execSQL(LokiAPIDatabase.CREATE_DEFAULT_FORK_INFO_COMMAND);
|
db.execSQL(LokiAPIDatabase.CREATE_DEFAULT_FORK_INFO_COMMAND);
|
||||||
db.execSQL(LokiAPIDatabase.UPDATE_HASHES_INCLUDE_NAMESPACE_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.getCreateApprovedCommand());
|
||||||
db.execSQL(RecipientDatabase.getCreateApprovedMeCommand());
|
db.execSQL(RecipientDatabase.getCreateApprovedMeCommand());
|
||||||
db.execSQL(RecipientDatabase.getUpdateApprovedCommand());
|
db.execSQL(RecipientDatabase.getUpdateApprovedCommand());
|
||||||
db.execSQL(MmsDatabase.getCreateMessageRequestResponseCommand());
|
db.execSQL(MmsDatabase.createMessageRequestResponseCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldVersion < lokiV32) {
|
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());
|
return new Pair<>(cursor, leftIsRecent ? cursor.getPosition() : cursor.getCount() - 1 - cursor.getPosition());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor.close();
|
|
||||||
|
|
||||||
|
if (cursor != null) {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
return null;
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.session.libsession.utilities.ExpirationUtil;
|
import org.session.libsession.utilities.ExpirationUtil;
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
|
|
||||||
@ -74,7 +74,6 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||||
Recipient recipient = getRecipient();
|
|
||||||
if (isGroupUpdateMessage()) {
|
if (isGroupUpdateMessage()) {
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
||||||
} else if (isOpenGroupInvitation()) {
|
} else if (isOpenGroupInvitation()) {
|
||||||
|
@ -4,18 +4,15 @@ package org.thoughtcrime.securesms.giph.ui;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
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.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ProgressBar;
|
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.RequestBuilder;
|
||||||
import com.bumptech.glide.load.DataSource;
|
import com.bumptech.glide.load.DataSource;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
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.request.target.Target;
|
||||||
import com.bumptech.glide.util.ByteBufferUtil;
|
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.MaterialColor;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
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.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
|
|
||||||
class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
||||||
|
|
||||||
@ -154,12 +152,12 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
|||||||
|
|
||||||
RequestBuilder<Drawable> thumbnailRequest = GlideApp.with(context)
|
RequestBuilder<Drawable> thumbnailRequest = GlideApp.with(context)
|
||||||
.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
|
.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL);
|
.diskCacheStrategy(DiskCacheStrategy.NONE);
|
||||||
|
|
||||||
if (org.thoughtcrime.securesms.util.Util.isLowMemory(context)) {
|
if (org.thoughtcrime.securesms.util.Util.isLowMemory(context)) {
|
||||||
glideRequests.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
|
glideRequests.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
|
||||||
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
|
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.transition(DrawableTransitionOptions.withCrossFade())
|
.transition(DrawableTransitionOptions.withCrossFade())
|
||||||
.listener(holder)
|
.listener(holder)
|
||||||
.into(holder.thumbnail);
|
.into(holder.thumbnail);
|
||||||
@ -169,7 +167,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
|||||||
glideRequests.load(new ChunkedImageUrl(image.getGifUrl(), image.getGifSize()))
|
glideRequests.load(new ChunkedImageUrl(image.getGifUrl(), image.getGifSize()))
|
||||||
.thumbnail(thumbnailRequest)
|
.thumbnail(thumbnailRequest)
|
||||||
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
|
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.transition(DrawableTransitionOptions.withCrossFade())
|
.transition(DrawableTransitionOptions.withCrossFade())
|
||||||
.listener(holder)
|
.listener(holder)
|
||||||
.into(holder.thumbnail);
|
.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) {
|
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);
|
return DatabaseComponent.get(context).threadDatabase().getThreadIdIfExistsFor(groupRecipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +59,7 @@ public class GroupManager {
|
|||||||
|
|
||||||
long threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(
|
long threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(
|
||||||
groupRecipient, DistributionTypes.CONVERSATION);
|
groupRecipient, DistributionTypes.CONVERSATION);
|
||||||
|
DatabaseComponent.get(context).threadDatabase().setThreadArchived(threadID);
|
||||||
return new GroupActionResult(groupRecipient, 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.ActivityJoinPublicChatBinding
|
||||||
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
@ -101,6 +102,7 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
|||||||
val sanitizedServer = server.toString().removeSuffix("/")
|
val sanitizedServer = server.toString().removeSuffix("/")
|
||||||
val openGroupID = "$sanitizedServer.${room!!}"
|
val openGroupID = "$sanitizedServer.${room!!}"
|
||||||
OpenGroupManager.add(sanitizedServer, room, publicKey!!, this@JoinPublicChatActivity)
|
OpenGroupManager.add(sanitizedServer, room, publicKey!!, this@JoinPublicChatActivity)
|
||||||
|
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(stringWithExplicitScheme)
|
||||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, this@JoinPublicChatActivity)
|
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, this@JoinPublicChatActivity)
|
||||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||||
threadID to groupID
|
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.OpenGroupAPIV2
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
|
||||||
import org.session.libsession.utilities.Util
|
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
object OpenGroupManager {
|
object OpenGroupManager {
|
||||||
private val executorService = Executors.newScheduledThreadPool(4)
|
private val executorService = Executors.newScheduledThreadPool(4)
|
||||||
private var pollers = mutableMapOf<String, OpenGroupPollerV2>() // One for each server
|
private var pollers = mutableMapOf<String, OpenGroupPollerV2>() // One for each server
|
||||||
private var isPolling = false
|
private var isPolling = false
|
||||||
|
private val pollUpdaterLock = Any()
|
||||||
|
|
||||||
val isAllCaughtUp: Boolean
|
val isAllCaughtUp: Boolean
|
||||||
get() {
|
get() {
|
||||||
@ -49,8 +48,11 @@ object OpenGroupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun stopPolling() {
|
fun stopPolling() {
|
||||||
pollers.forEach { it.value.stop() }
|
synchronized(pollUpdaterLock) {
|
||||||
pollers.clear()
|
pollers.forEach { it.value.stop() }
|
||||||
|
pollers.clear()
|
||||||
|
isPolling = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@ -67,7 +69,7 @@ object OpenGroupManager {
|
|||||||
storage.removeLastMessageServerID(room, server)
|
storage.removeLastMessageServerID(room, server)
|
||||||
// Store the public key
|
// Store the public key
|
||||||
storage.setOpenGroupPublicKey(server,publicKey)
|
storage.setOpenGroupPublicKey(server,publicKey)
|
||||||
// Get an auth token
|
// Get group info
|
||||||
OpenGroupAPIV2.getAuthToken(room, server).get()
|
OpenGroupAPIV2.getAuthToken(room, server).get()
|
||||||
// Get group info
|
// Get group info
|
||||||
val info = OpenGroupAPIV2.getInfo(room, server).get()
|
val info = OpenGroupAPIV2.getInfo(room, server).get()
|
||||||
@ -77,11 +79,17 @@ object OpenGroupManager {
|
|||||||
}
|
}
|
||||||
val openGroup = OpenGroupV2(server, room, info.name, publicKey)
|
val openGroup = OpenGroupV2(server, room, info.name, publicKey)
|
||||||
threadDB.setOpenGroupChat(openGroup, threadID)
|
threadDB.setOpenGroupChat(openGroup, threadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restartPollerForServer(server: String) {
|
||||||
// Start the poller if needed
|
// Start the poller if needed
|
||||||
pollers[server]?.startIfNeeded() ?: run {
|
synchronized(pollUpdaterLock) {
|
||||||
val poller = OpenGroupPollerV2(server, executorService)
|
pollers[server]?.stop()
|
||||||
Util.runOnMain { poller.startIfNeeded() }
|
pollers[server]?.startIfNeeded() ?: run {
|
||||||
pollers[server] = poller
|
val poller = OpenGroupPollerV2(server, executorService)
|
||||||
|
pollers[server] = poller
|
||||||
|
poller.startIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,13 +99,16 @@ object OpenGroupManager {
|
|||||||
val openGroupID = "$server.$room"
|
val openGroupID = "$server.$room"
|
||||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
||||||
val recipient = threadDB.getRecipientForThreadId(threadID) ?: return
|
val recipient = threadDB.getRecipientForThreadId(threadID) ?: return
|
||||||
|
threadDB.setThreadArchived(threadID)
|
||||||
val groupID = recipient.address.serialize()
|
val groupID = recipient.address.serialize()
|
||||||
// Stop the poller if needed
|
// Stop the poller if needed
|
||||||
val openGroups = storage.getAllV2OpenGroups().filter { it.value.server == server }
|
val openGroups = storage.getAllV2OpenGroups().filter { it.value.server == server }
|
||||||
if (openGroups.count() == 1) {
|
if (openGroups.count() == 1) {
|
||||||
val poller = pollers[server]
|
synchronized(pollUpdaterLock) {
|
||||||
poller?.stop()
|
val poller = pollers[server]
|
||||||
pollers.remove(server)
|
poller?.stop()
|
||||||
|
pollers.remove(server)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Delete
|
// Delete
|
||||||
storage.removeLastDeletionServerID(room, server)
|
storage.removeLastDeletionServerID(room, server)
|
||||||
@ -112,12 +123,7 @@ object OpenGroupManager {
|
|||||||
|
|
||||||
fun addOpenGroup(urlAsString: String, context: Context) {
|
fun addOpenGroup(urlAsString: String, context: Context) {
|
||||||
val url = HttpUrl.parse(urlAsString) ?: return
|
val url = HttpUrl.parse(urlAsString) ?: return
|
||||||
val builder = HttpUrl.Builder().scheme(url.scheme()).host(url.host())
|
val server = OpenGroupV2.getServer(urlAsString)
|
||||||
if (url.port() != 80 || url.port() != 443) {
|
|
||||||
// Non-standard port; add to server
|
|
||||||
builder.port(url.port())
|
|
||||||
}
|
|
||||||
val server = builder.build()
|
|
||||||
val room = url.pathSegments().firstOrNull() ?: return
|
val room = url.pathSegments().firstOrNull() ?: return
|
||||||
val publicKey = url.queryParameter("public_key") ?: return
|
val publicKey = url.queryParameter("public_key") ?: return
|
||||||
add(server.toString().removeSuffix("/"), room, publicKey, context)
|
add(server.toString().removeSuffix("/"), room, publicKey, context)
|
||||||
|
@ -15,7 +15,8 @@ import network.loki.messenger.R
|
|||||||
import network.loki.messenger.databinding.ViewConversationBinding
|
import network.loki.messenger.databinding.ViewConversationBinding
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
|
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.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
@ -47,7 +48,7 @@ class ConversationView : LinearLayout {
|
|||||||
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||||
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
||||||
}
|
}
|
||||||
binding.profilePictureView.glide = glide
|
binding.profilePictureView.root.glide = glide
|
||||||
val unreadCount = thread.unreadCount
|
val unreadCount = thread.unreadCount
|
||||||
if (thread.recipient.isBlocked) {
|
if (thread.recipient.isBlocked) {
|
||||||
binding.accentView.setBackgroundResource(R.color.destructive)
|
binding.accentView.setBackgroundResource(R.color.destructive)
|
||||||
@ -73,15 +74,15 @@ class ConversationView : LinearLayout {
|
|||||||
binding.conversationViewDisplayNameTextView.text = senderDisplayName
|
binding.conversationViewDisplayNameTextView.text = senderDisplayName
|
||||||
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||||
val recipient = thread.recipient
|
val recipient = thread.recipient
|
||||||
binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL
|
binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != NOTIFY_TYPE_ALL
|
||||||
val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
|
val drawableRes = if (recipient.isMuted || recipient.notifyType == NOTIFY_TYPE_NONE) {
|
||||||
R.drawable.ic_outline_notifications_off_24
|
R.drawable.ic_outline_notifications_off_24
|
||||||
} else {
|
} else {
|
||||||
R.drawable.ic_notifications_mentions
|
R.drawable.ic_notifications_mentions
|
||||||
}
|
}
|
||||||
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
||||||
val rawSnippet = thread.getDisplayBody(context)
|
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.text = snippet
|
||||||
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||||
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
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)
|
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||||
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
||||||
}
|
}
|
||||||
post {
|
binding.profilePictureView.root.update(thread.recipient)
|
||||||
binding.profilePictureView.update(thread.recipient)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
binding.profilePictureView.recycle()
|
binding.profilePictureView.root.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
private fun getUserDisplayName(recipient: Recipient): String? {
|
||||||
|
@ -5,18 +5,15 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.database.Cursor
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.loader.app.LoaderManager
|
|
||||||
import androidx.loader.content.Loader
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -83,7 +79,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
ConversationClickListener,
|
ConversationClickListener,
|
||||||
SeedReminderViewDelegate,
|
SeedReminderViewDelegate,
|
||||||
NewConversationButtonSetViewDelegate,
|
NewConversationButtonSetViewDelegate,
|
||||||
LoaderManager.LoaderCallbacks<Cursor>,
|
|
||||||
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
||||||
|
|
||||||
private lateinit var binding: ActivityHomeBinding
|
private lateinit var binding: ActivityHomeBinding
|
||||||
@ -97,12 +92,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
@Inject lateinit var textSecurePreferences: TextSecurePreferences
|
@Inject lateinit var textSecurePreferences: TextSecurePreferences
|
||||||
|
|
||||||
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
|
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
|
||||||
|
private val homeViewModel by viewModels<HomeViewModel>()
|
||||||
|
|
||||||
private val publicKey: String
|
private val publicKey: String
|
||||||
get() = textSecurePreferences.getLocalNumber()!!
|
get() = textSecurePreferences.getLocalNumber()!!
|
||||||
|
|
||||||
private val homeAdapter: HomeAdapter by lazy {
|
private val homeAdapter: NewHomeAdapter by lazy {
|
||||||
HomeAdapter(context = this, cursor = threadDb.approvedConversationList, listener = this)
|
NewHomeAdapter(context = this, listener = this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val globalSearchAdapter = GlobalSearchAdapter { model ->
|
private val globalSearchAdapter = GlobalSearchAdapter { model ->
|
||||||
@ -156,8 +152,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// Set up Glide
|
// Set up Glide
|
||||||
glide = GlideApp.with(this)
|
glide = GlideApp.with(this)
|
||||||
// Set up toolbar buttons
|
// Set up toolbar buttons
|
||||||
binding.profileButton.glide = glide
|
binding.profileButton.root.glide = glide
|
||||||
binding.profileButton.setOnClickListener { openSettings() }
|
binding.profileButton.root.setOnClickListener { openSettings() }
|
||||||
binding.searchViewContainer.setOnClickListener {
|
binding.searchViewContainer.setOnClickListener {
|
||||||
binding.globalSearchInputLayout.requestFocus()
|
binding.globalSearchInputLayout.requestFocus()
|
||||||
}
|
}
|
||||||
@ -184,8 +180,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// Set up empty state view
|
// Set up empty state view
|
||||||
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||||
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
|
homeViewModel.getObservable(this).observe(this) { newData ->
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
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
|
// Set up new conversation button set
|
||||||
binding.newConversationButtonSet.delegate = this
|
binding.newConversationButtonSet.delegate = this
|
||||||
// Observe blocked contacts changed events
|
// Observe blocked contacts changed events
|
||||||
@ -202,17 +210,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// Double check that the long poller is up
|
// Double check that the long poller is up
|
||||||
(applicationContext as ApplicationContext).startPollingIfNeeded()
|
(applicationContext as ApplicationContext).startPollingIfNeeded()
|
||||||
// update things based on TextSecurePrefs (profile info etc)
|
// 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
|
// Set up remaining components if needed
|
||||||
val application = ApplicationContext.getInstance(this@HomeActivity)
|
val application = ApplicationContext.getInstance(this@HomeActivity)
|
||||||
application.registerForFCMIfNeeded(false)
|
application.registerForFCMIfNeeded(false)
|
||||||
@ -220,6 +217,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
OpenGroupManager.startPolling()
|
OpenGroupManager.startPolling()
|
||||||
JobQueue.shared.resumePendingJobs()
|
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
|
// monitor the global search VM query
|
||||||
launch {
|
launch {
|
||||||
@ -293,7 +297,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
binding.searchToolbar.isVisible = isShown
|
binding.searchToolbar.isVisible = isShown
|
||||||
binding.sessionToolbar.isVisible = !isShown
|
binding.sessionToolbar.isVisible = !isShown
|
||||||
binding.recyclerView.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.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown
|
||||||
binding.gradientView.isVisible = !isShown
|
binding.gradientView.isVisible = !isShown
|
||||||
binding.globalSearchRecycler.isVisible = isShown
|
binding.globalSearchRecycler.isVisible = isShown
|
||||||
@ -314,35 +318,27 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
root.setOnClickListener { showMessageRequests() }
|
root.setOnClickListener { showMessageRequests() }
|
||||||
root.setOnLongClickListener { hideMessageRequests(); true }
|
root.setOnLongClickListener { hideMessageRequests(); true }
|
||||||
root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||||
homeAdapter.headerView = root
|
val hadHeader = homeAdapter.hasHeaderView()
|
||||||
homeAdapter.notifyItemChanged(0)
|
homeAdapter.header = root
|
||||||
|
if (hadHeader) homeAdapter.notifyItemChanged(0)
|
||||||
|
else homeAdapter.notifyItemInserted(0)
|
||||||
}
|
}
|
||||||
} else {
|
} 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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
|
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
|
||||||
if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared
|
if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared
|
||||||
IdentityKeyUtil.checkUpdate(this)
|
IdentityKeyUtil.checkUpdate(this)
|
||||||
binding.profileButton.recycle() // clear cached image before update tje profilePictureView
|
binding.profileButton.root.recycle() // clear cached image before update tje profilePictureView
|
||||||
binding.profileButton.update()
|
binding.profileButton.root.update()
|
||||||
if (textSecurePreferences.getHasViewedSeed()) {
|
if (textSecurePreferences.getHasViewedSeed()) {
|
||||||
binding.seedReminderView.isVisible = false
|
binding.seedReminderView.isVisible = false
|
||||||
}
|
}
|
||||||
@ -377,7 +373,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
private fun updateEmptyState() {
|
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
|
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,14 +381,16 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
fun onUpdateProfileEvent(event: ProfilePictureModifiedEvent) {
|
fun onUpdateProfileEvent(event: ProfilePictureModifiedEvent) {
|
||||||
if (event.recipient.isLocalNumber) {
|
if (event.recipient.isLocalNumber) {
|
||||||
updateProfileButton()
|
updateProfileButton()
|
||||||
|
} else {
|
||||||
|
homeViewModel.tryUpdateChannel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateProfileButton() {
|
private fun updateProfileButton() {
|
||||||
binding.profileButton.publicKey = publicKey
|
binding.profileButton.root.publicKey = publicKey
|
||||||
binding.profileButton.displayName = textSecurePreferences.getProfileName()
|
binding.profileButton.root.displayName = textSecurePreferences.getProfileName()
|
||||||
binding.profileButton.recycle()
|
binding.profileButton.root.recycle()
|
||||||
binding.profileButton.update()
|
binding.profileButton.root.update()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -534,9 +532,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
threadDb.setPinned(threadId, pinned)
|
threadDb.setPinned(threadId, pinned)
|
||||||
withContext(Dispatchers.Main) {
|
homeViewModel.tryUpdateChannel()
|
||||||
LoaderManager.getInstance(this@HomeActivity).restartLoader(0, null, this@HomeActivity)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -608,11 +604,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
show(intent, isForResult = true)
|
show(intent, isForResult = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showPath() {
|
|
||||||
val intent = Intent(this, PathActivity::class.java)
|
|
||||||
show(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showMessageRequests() {
|
private fun showMessageRequests() {
|
||||||
val intent = Intent(this, MessageRequestsActivity::class.java)
|
val intent = Intent(this, MessageRequestsActivity::class.java)
|
||||||
push(intent)
|
push(intent)
|
||||||
@ -624,7 +615,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
.setPositiveButton(R.string.yes) { _, _ ->
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
textSecurePreferences.setHasHiddenMessageRequests()
|
textSecurePreferences.setHasHiddenMessageRequests()
|
||||||
setupMessageRequestsBanner()
|
setupMessageRequestsBanner()
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
homeViewModel.tryUpdateChannel()
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.no) { _, _ ->
|
.setNegativeButton(R.string.no) { _, _ ->
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
@ -1,51 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.home
|
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.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 {
|
interface ConversationClickListener {
|
||||||
fun onConversationClick(thread: ThreadRecord)
|
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.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader
|
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 {
|
override fun getCursor(): Cursor {
|
||||||
return DatabaseComponent.get(context).threadDatabase().approvedConversationList
|
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"
|
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)
|
binding = FragmentUserDetailsBottomSheetBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@ -51,10 +51,10 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
|||||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
|
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
|
||||||
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
||||||
with(binding) {
|
with(binding) {
|
||||||
profilePictureView.publicKey = publicKey
|
profilePictureView.root.publicKey = publicKey
|
||||||
profilePictureView.glide = GlideApp.with(this@UserDetailsBottomSheet)
|
profilePictureView.root.glide = GlideApp.with(this@UserDetailsBottomSheet)
|
||||||
profilePictureView.isLarge = true
|
profilePictureView.root.isLarge = true
|
||||||
profilePictureView.update(recipient)
|
profilePictureView.root.update(recipient)
|
||||||
nameTextViewContainer.visibility = View.VISIBLE
|
nameTextViewContainer.visibility = View.VISIBLE
|
||||||
nameTextViewContainer.setOnClickListener {
|
nameTextViewContainer.setOnClickListener {
|
||||||
nameTextViewContainer.visibility = View.INVISIBLE
|
nameTextViewContainer.visibility = View.INVISIBLE
|
||||||
|
@ -11,7 +11,6 @@ import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
|
|||||||
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
|
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
|
||||||
import org.session.libsession.utilities.GroupRecord
|
import org.session.libsession.utilities.GroupRecord
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||||
import java.security.InvalidParameterException
|
import java.security.InvalidParameterException
|
||||||
@ -84,14 +83,14 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
|
|
||||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||||
if (holder is ContentView) {
|
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) {
|
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
val binding = ViewGlobalSearchResultBinding.bind(view).apply {
|
val binding = ViewGlobalSearchResultBinding.bind(view).apply {
|
||||||
searchResultProfilePicture.glide = GlideApp.with(root)
|
searchResultProfilePicture.root.glide = GlideApp.with(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bindPayload(newQuery: String, model: Model) {
|
fun bindPayload(newQuery: String, model: Model) {
|
||||||
@ -99,7 +98,7 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun bind(query: String, model: Model) {
|
fun bind(query: String, model: Model) {
|
||||||
binding.searchResultProfilePicture.recycle()
|
binding.searchResultProfilePicture.root.recycle()
|
||||||
when (model) {
|
when (model) {
|
||||||
is Model.GroupConversation -> bindModel(query, model)
|
is Model.GroupConversation -> bindModel(query, model)
|
||||||
is Model.Contact -> 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.graphics.Typeface
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.style.ForegroundColorSpan
|
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import android.util.TypedValue
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
@ -86,12 +84,12 @@ private fun getHighlight(query: String?, toSearch: String): Spannable? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
||||||
binding.searchResultProfilePicture.isVisible = true
|
binding.searchResultProfilePicture.root.isVisible = true
|
||||||
binding.searchResultSavedMessages.isVisible = false
|
binding.searchResultSavedMessages.isVisible = false
|
||||||
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
|
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
|
||||||
binding.searchResultTimestamp.isVisible = false
|
binding.searchResultTimestamp.isVisible = false
|
||||||
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), 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
|
val nameString = model.groupRecord.title
|
||||||
binding.searchResultTitle.text = getHighlight(query, nameString)
|
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||||
|
|
||||||
@ -107,14 +105,14 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ContentView.bindModel(query: String?, model: ContactModel) {
|
fun ContentView.bindModel(query: String?, model: ContactModel) {
|
||||||
binding.searchResultProfilePicture.isVisible = true
|
binding.searchResultProfilePicture.root.isVisible = true
|
||||||
binding.searchResultSavedMessages.isVisible = false
|
binding.searchResultSavedMessages.isVisible = false
|
||||||
binding.searchResultSubtitle.isVisible = false
|
binding.searchResultSubtitle.isVisible = false
|
||||||
binding.searchResultTimestamp.isVisible = false
|
binding.searchResultTimestamp.isVisible = false
|
||||||
binding.searchResultSubtitle.text = null
|
binding.searchResultSubtitle.text = null
|
||||||
val recipient =
|
val recipient =
|
||||||
Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false)
|
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()
|
val nameString = model.contact.getSearchName()
|
||||||
binding.searchResultTitle.text = getHighlight(query, nameString)
|
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||||
}
|
}
|
||||||
@ -123,12 +121,12 @@ fun ContentView.bindModel(model: SavedMessages) {
|
|||||||
binding.searchResultSubtitle.isVisible = false
|
binding.searchResultSubtitle.isVisible = false
|
||||||
binding.searchResultTimestamp.isVisible = false
|
binding.searchResultTimestamp.isVisible = false
|
||||||
binding.searchResultTitle.setText(R.string.note_to_self)
|
binding.searchResultTitle.setText(R.string.note_to_self)
|
||||||
binding.searchResultProfilePicture.isVisible = false
|
binding.searchResultProfilePicture.root.isVisible = false
|
||||||
binding.searchResultSavedMessages.isVisible = true
|
binding.searchResultSavedMessages.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ContentView.bindModel(query: String?, model: Message) {
|
fun ContentView.bindModel(query: String?, model: Message) {
|
||||||
binding.searchResultProfilePicture.isVisible = true
|
binding.searchResultProfilePicture.root.isVisible = true
|
||||||
binding.searchResultSavedMessages.isVisible = false
|
binding.searchResultSavedMessages.isVisible = false
|
||||||
binding.searchResultTimestamp.isVisible = true
|
binding.searchResultTimestamp.isVisible = true
|
||||||
// val hasUnreads = model.unread > 0
|
// val hasUnreads = model.unread > 0
|
||||||
@ -137,7 +135,7 @@ fun ContentView.bindModel(query: String?, model: Message) {
|
|||||||
// binding.unreadCountTextView.text = model.unread.toString()
|
// binding.unreadCountTextView.text = model.unread.toString()
|
||||||
// }
|
// }
|
||||||
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.receivedTimestampMs)
|
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()
|
val textSpannable = SpannableStringBuilder()
|
||||||
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
||||||
// group chat, bind
|
// group chat, bind
|
||||||
|
@ -48,7 +48,8 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
|||||||
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
|
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
||||||
.setMaxAttempts(10)
|
.setMaxAttempts(2)
|
||||||
|
.setMaxInstances(1)
|
||||||
.build(),
|
.build(),
|
||||||
recipient,
|
recipient,
|
||||||
profileAvatar);
|
profileAvatar);
|
||||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.messagerequests
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
@ -35,7 +34,7 @@ class MessageRequestView : LinearLayout {
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(thread: ThreadRecord, glide: GlideRequests) {
|
fun bind(thread: ThreadRecord, glide: GlideRequests) {
|
||||||
this.thread = thread
|
this.thread = thread
|
||||||
binding.profilePictureView.glide = glide
|
binding.profilePictureView.root.glide = glide
|
||||||
val senderDisplayName = getUserDisplayName(thread.recipient)
|
val senderDisplayName = getUserDisplayName(thread.recipient)
|
||||||
?: thread.recipient.address.toString()
|
?: thread.recipient.address.toString()
|
||||||
binding.displayNameTextView.text = senderDisplayName
|
binding.displayNameTextView.text = senderDisplayName
|
||||||
@ -45,12 +44,12 @@ class MessageRequestView : LinearLayout {
|
|||||||
binding.snippetTextView.text = snippet
|
binding.snippetTextView.text = snippet
|
||||||
|
|
||||||
post {
|
post {
|
||||||
binding.profilePictureView.update(thread.recipient)
|
binding.profilePictureView.root.update(thread.recipient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
binding.profilePictureView.recycle()
|
binding.profilePictureView.root.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
private fun getUserDisplayName(recipient: Recipient): String? {
|
||||||
|
@ -2,9 +2,11 @@ package org.thoughtcrime.securesms.mms;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import androidx.annotation.NonNull;
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.GlideBuilder;
|
import com.bumptech.glide.GlideBuilder;
|
||||||
import com.bumptech.glide.Registry;
|
import com.bumptech.glide.Registry;
|
||||||
@ -21,12 +23,14 @@ import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
|
|||||||
import com.bumptech.glide.module.AppGlideModule;
|
import com.bumptech.glide.module.AppGlideModule;
|
||||||
|
|
||||||
import org.session.libsession.avatars.ContactPhoto;
|
import org.session.libsession.avatars.ContactPhoto;
|
||||||
|
import org.session.libsession.avatars.PlaceholderAvatarPhoto;
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||||
import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader;
|
import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader;
|
||||||
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
|
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
|
||||||
import org.thoughtcrime.securesms.glide.OkHttpUrlLoader;
|
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.EncryptedBitmapCacheDecoder;
|
||||||
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
|
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
|
||||||
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
|
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(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||||
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
||||||
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.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());
|
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");
|
Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message");
|
||||||
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
|
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
|
||||||
try {
|
try {
|
||||||
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, replyThreadId, false, null);
|
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, replyThreadId, false, null, true);
|
||||||
} catch (MmsException e) {
|
} catch (MmsException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w("AndroidAutoReplyReceiver", "Sending regular message ");
|
Log.w("AndroidAutoReplyReceiver", "Sending regular message ");
|
||||||
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
|
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);
|
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();
|
ThreadDatabase threads = DatabaseComponent.get(context).threadDatabase();
|
||||||
Recipient recipient = threads.getRecipientForThreadId(threadId);
|
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())) {
|
!(recipient.isApproved() || threads.getLastSeenAndHasSent(threadId).second())) {
|
||||||
TextSecurePreferences.removeHasHiddenMessageRequests(context);
|
TextSecurePreferences.removeHasHiddenMessageRequests(context);
|
||||||
}
|
}
|
||||||
@ -278,10 +278,10 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (notificationState.hasMultipleThreads()) {
|
if (notificationState.hasMultipleThreads()) {
|
||||||
|
sendMultipleThreadNotification(context, notificationState, signal);
|
||||||
for (long threadId : notificationState.getThreads()) {
|
for (long threadId : notificationState.getThreads()) {
|
||||||
sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true);
|
sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true);
|
||||||
}
|
}
|
||||||
sendMultipleThreadNotification(context, notificationState, signal);
|
|
||||||
} else if (notificationState.getMessageCount() > 0){
|
} else if (notificationState.getMessageCount() > 0){
|
||||||
sendSingleThreadNotification(context, notificationState, signal, false);
|
sendSingleThreadNotification(context, notificationState, signal, false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -83,7 +83,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
|||||||
case GroupMessage: {
|
case GroupMessage: {
|
||||||
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
|
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
|
||||||
try {
|
try {
|
||||||
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, threadId, false, null);
|
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, threadId, false, null, true);
|
||||||
MessageSender.send(message, address);
|
MessageSender.send(message, address);
|
||||||
} catch (MmsException e) {
|
} catch (MmsException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
@ -92,7 +92,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
|||||||
}
|
}
|
||||||
case SecureMessage: {
|
case SecureMessage: {
|
||||||
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
|
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);
|
MessageSender.send(message, address);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||||||
Bitmap iconBitmap = GlideApp.with(context.getApplicationContext())
|
Bitmap iconBitmap = GlideApp.with(context.getApplicationContext())
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(contactPhoto)
|
.load(contactPhoto)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
|
.submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
|
||||||
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))
|
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))
|
||||||
|
@ -20,7 +20,7 @@ class LandingActivity : BaseActionBarActivity() {
|
|||||||
with(binding) {
|
with(binding) {
|
||||||
fakeChatView.startAnimating()
|
fakeChatView.startAnimating()
|
||||||
registerButton.setOnClickListener { register() }
|
registerButton.setOnClickListener { register() }
|
||||||
restoreButton.setOnClickListener { restore() }
|
restoreButton.setOnClickListener { link() }
|
||||||
linkButton.setOnClickListener { link() }
|
linkButton.setOnClickListener { link() }
|
||||||
}
|
}
|
||||||
IdentityKeyUtil.generateIdentityKeyPair(this)
|
IdentityKeyUtil.generateIdentityKeyPair(this)
|
||||||
@ -34,11 +34,6 @@ class LandingActivity : BaseActionBarActivity() {
|
|||||||
push(intent)
|
push(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restore() {
|
|
||||||
val intent = Intent(this, RecoveryPhraseRestoreActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun link() {
|
private fun link() {
|
||||||
val intent = Intent(this, LinkDeviceActivity::class.java)
|
val intent = Intent(this, LinkDeviceActivity::class.java)
|
||||||
push(intent)
|
push(intent)
|
||||||
|
@ -77,12 +77,12 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey
|
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey
|
||||||
glide = GlideApp.with(this)
|
glide = GlideApp.with(this)
|
||||||
with(binding) {
|
with(binding) {
|
||||||
profilePictureView.glide = glide
|
profilePictureView.root.glide = glide
|
||||||
profilePictureView.publicKey = hexEncodedPublicKey
|
profilePictureView.root.publicKey = hexEncodedPublicKey
|
||||||
profilePictureView.displayName = displayName
|
profilePictureView.root.displayName = displayName
|
||||||
profilePictureView.isLarge = true
|
profilePictureView.root.isLarge = true
|
||||||
profilePictureView.update()
|
profilePictureView.root.update()
|
||||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
profilePictureView.root.setOnClickListener { showEditProfilePictureUI() }
|
||||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||||
btnGroupNameDisplay.text = displayName
|
btnGroupNameDisplay.text = displayName
|
||||||
publicKeyTextView.text = hexEncodedPublicKey
|
publicKeyTextView.text = hexEncodedPublicKey
|
||||||
@ -214,8 +214,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
binding.btnGroupNameDisplay.text = displayName
|
binding.btnGroupNameDisplay.text = displayName
|
||||||
}
|
}
|
||||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||||
binding.profilePictureView.recycle() // Clear the cached image before updating
|
binding.profilePictureView.root.recycle() // Clear the cached image before updating
|
||||||
binding.profilePictureView.update()
|
binding.profilePictureView.root.update()
|
||||||
}
|
}
|
||||||
displayNameToBeUploaded = null
|
displayNameToBeUploaded = null
|
||||||
profilePictureToBeUploaded = null
|
profilePictureToBeUploaded = null
|
||||||
|
@ -22,8 +22,10 @@ import network.loki.messenger.BuildConfig
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.DialogShareLogsBinding
|
import network.loki.messenger.databinding.DialogShareLogsBinding
|
||||||
import org.session.libsignal.utilities.ExternalStorageUtil
|
import org.session.libsignal.utilities.ExternalStorageUtil
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||||
|
import org.thoughtcrime.securesms.util.FileProviderUtil
|
||||||
import org.thoughtcrime.securesms.util.StreamUtil
|
import org.thoughtcrime.securesms.util.StreamUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@ -84,18 +86,26 @@ class ShareLogsDialog : BaseDialog() {
|
|||||||
requireContext().contentResolver.update(mediaUri, updateValues, null, null)
|
requireContext().contentResolver.update(mediaUri, updateValues, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val shareIntent = Intent().apply {
|
val shareUri = if (mediaUri.scheme == ContentResolver.SCHEME_FILE) {
|
||||||
action = Intent.ACTION_SEND
|
FileProviderUtil.getUriFor(context, File(mediaUri.path!!))
|
||||||
putExtra(Intent.EXTRA_STREAM, mediaUri)
|
} else {
|
||||||
data = mediaUri
|
mediaUri
|
||||||
type = "text/plain"
|
}
|
||||||
|
|
||||||
|
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()
|
dismiss()
|
||||||
|
|
||||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
|
Log.e("Loki", "Error saving logs", e)
|
||||||
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
|
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
|
@ -33,7 +33,7 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
|
|
||||||
interface ConversationRepository {
|
interface ConversationRepository {
|
||||||
fun isOxenHostedOpenGroup(threadId: Long): Boolean
|
fun isOxenHostedOpenGroup(threadId: Long): Boolean
|
||||||
fun getRecipientForThreadId(threadId: Long): Recipient
|
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
||||||
fun saveDraft(threadId: Long, text: String)
|
fun saveDraft(threadId: Long, text: String)
|
||||||
fun getDraft(threadId: Long): String?
|
fun getDraft(threadId: Long): String?
|
||||||
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
||||||
@ -86,12 +86,11 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
|
|
||||||
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
|
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||||
return openGroup?.room == "session" || openGroup?.room == "oxen"
|
return openGroup?.publicKey == OpenGroupAPIV2.defaultServerPublicKey
|
||||||
|| openGroup?.room == "lokinet" || openGroup?.room == "crypto"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRecipientForThreadId(threadId: Long): Recipient {
|
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
|
||||||
return threadDb.getRecipientForThreadId(threadId)!!
|
return threadDb.getRecipientForThreadId(threadId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveDraft(threadId: Long, text: String) {
|
override fun saveDraft(threadId: Long, text: String) {
|
||||||
@ -121,7 +120,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
contact,
|
contact,
|
||||||
message.sentTimestamp
|
message.sentTimestamp
|
||||||
)
|
)
|
||||||
smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!)
|
smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!, true)
|
||||||
MessageSender.send(message, contact.address)
|
MessageSender.send(message, contact.address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
Optional.absent(),
|
Optional.absent(),
|
||||||
Optional.absent());
|
Optional.absent());
|
||||||
//insert the timer update message
|
//insert the timer update message
|
||||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
database.insertSecureDecryptedMessageInbox(mediaMessage, -1, true, true);
|
||||||
|
|
||||||
//set the timer to the conversation
|
//set the timer to the conversation
|
||||||
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
|
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
|
||||||
@ -141,7 +141,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId);
|
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId);
|
||||||
database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp);
|
database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp, true);
|
||||||
|
|
||||||
if (groupId != null) {
|
if (groupId != null) {
|
||||||
// we need the group ID as recipient for setExpireMessages below
|
// 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) {
|
override fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String) {
|
||||||
val job = RetrieveProfileAvatarJob(recipient, profilePictureURL)
|
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 sessionID = recipient.address.serialize()
|
||||||
val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()
|
val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()
|
||||||
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical">
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<include layout="@layout/view_profile_picture"
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/medium_profile_picture_size"
|
android:layout_width="@dimen/medium_profile_picture_size"
|
||||||
android:layout_height="@dimen/medium_profile_picture_size" />
|
android:layout_height="@dimen/medium_profile_picture_size" />
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
android:layout_marginLeft="20dp"
|
android:layout_marginLeft="20dp"
|
||||||
android:layout_marginRight="20dp">
|
android:layout_marginRight="20dp">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<include layout="@layout/view_profile_picture"
|
||||||
android:id="@+id/profileButton"
|
android:id="@+id/profileButton"
|
||||||
android:layout_width="@dimen/small_profile_picture_size"
|
android:layout_width="@dimen/small_profile_picture_size"
|
||||||
android:layout_height="@dimen/small_profile_picture_size"
|
android:layout_height="@dimen/small_profile_picture_size"
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:gravity="center_horizontal">
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<include layout="@layout/view_profile_picture"
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/large_profile_picture_size"
|
android:layout_width="@dimen/large_profile_picture_size"
|
||||||
android:layout_height="@dimen/large_profile_picture_size"
|
android:layout_height="@dimen/large_profile_picture_size"
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
app:behavior_hideable="true"
|
app:behavior_hideable="true"
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
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:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/large_profile_picture_size"
|
android:layout_width="@dimen/large_profile_picture_size"
|
||||||
android:layout_height="@dimen/large_profile_picture_size"
|
android:layout_height="@dimen/large_profile_picture_size"
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
app:behavior_hideable="true"
|
app:behavior_hideable="true"
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
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:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/large_profile_picture_size"
|
android:layout_width="@dimen/large_profile_picture_size"
|
||||||
android:layout_height="@dimen/large_profile_picture_size"
|
android:layout_height="@dimen/large_profile_picture_size"
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
tools:visibility="visible"
|
||||||
android:src="@drawable/ic_download_circle_filled_48"
|
android:src="@drawable/ic_download_circle_filled_48"
|
||||||
android:id="@+id/thumbnail_download_icon"
|
android:id="@+id/thumbnail_download_icon"
|
||||||
android:layout_width="@dimen/medium_button_height"
|
android:layout_width="@dimen/medium_button_height"
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/accent" />
|
android:background="@color/accent" />
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<include layout="@layout/view_profile_picture"
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/medium_profile_picture_size"
|
android:layout_width="@dimen/medium_profile_picture_size"
|
||||||
android:layout_height="@dimen/medium_profile_picture_size"
|
android:layout_height="@dimen/medium_profile_picture_size"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -29,4 +29,4 @@
|
|||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
android:ellipsize="end" />
|
android:ellipsize="end" />
|
||||||
|
|
||||||
</LinearLayout>
|
</org.thoughtcrime.securesms.conversation.v2.messages.DeletedMessageView>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -27,4 +27,4 @@
|
|||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
android:ellipsize="end" />
|
android:ellipsize="end" />
|
||||||
|
|
||||||
</LinearLayout>
|
</org.thoughtcrime.securesms.conversation.v2.messages.DocumentView>
|
@ -18,7 +18,7 @@
|
|||||||
android:id="@+id/search_result_profile_picture_parent"
|
android:id="@+id/search_result_profile_picture_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<include layout="@layout/view_profile_picture"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:id="@+id/search_result_profile_picture"
|
android:id="@+id/search_result_profile_picture"
|
||||||
android:layout_width="@dimen/medium_profile_picture_size"
|
android:layout_width="@dimen/medium_profile_picture_size"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout android:id="@+id/mainLinkPreviewContainer"
|
<LinearLayout android:id="@+id/mainLinkPreviewContainer"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="300dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
android:layout_width="26dp"
|
android:layout_width="26dp"
|
||||||
android:layout_height="32dp">
|
android:layout_height="32dp">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<include layout="@layout/view_profile_picture"
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/very_small_profile_picture_size"
|
android:layout_width="@dimen/very_small_profile_picture_size"
|
||||||
android:layout_height="@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_width="26dp"
|
||||||
android:layout_height="32dp">
|
android:layout_height="32dp">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<include layout="@layout/view_profile_picture"
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/very_small_profile_picture_size"
|
android:layout_width="@dimen/very_small_profile_picture_size"
|
||||||
android:layout_height="@dimen/very_small_profile_picture_size"
|
android:layout_height="@dimen/very_small_profile_picture_size"
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<include layout="@layout/view_profile_picture"
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/medium_profile_picture_size"
|
android:layout_width="@dimen/medium_profile_picture_size"
|
||||||
android:layout_height="@dimen/medium_profile_picture_size"
|
android:layout_height="@dimen/medium_profile_picture_size"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<org.thoughtcrime.securesms.conversation.v2.messages.OpenGroupInvitationView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -66,4 +66,4 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</org.thoughtcrime.securesms.conversation.v2.messages.OpenGroupInvitationView>
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/doubleModeImageViewContainer"
|
android:id="@+id/doubleModeImageViewContainer"
|
||||||
@ -39,4 +39,4 @@
|
|||||||
android:layout_height="@dimen/large_profile_picture_size"
|
android:layout_height="@dimen/large_profile_picture_size"
|
||||||
android:background="@drawable/profile_picture_view_large_background" />
|
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"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/mainQuoteViewContainer"
|
android:id="@+id/mainQuoteViewContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@color/input_bar_background"
|
android:background="@color/input_bar_background"
|
||||||
android:paddingHorizontal="@dimen/medium_spacing"
|
android:minWidth="300dp"
|
||||||
android:paddingVertical="@dimen/small_spacing">
|
android:minHeight="52dp"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
app:quote_mode="regular">
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/quoteViewAccentLine"
|
android:id="@+id/quoteViewAccentLine"
|
||||||
android:layout_width="@dimen/accent_line_thickness"
|
android:layout_width="@dimen/accent_line_thickness"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_centerVertical="true"
|
android:layout_marginVertical="2dp"
|
||||||
android:layout_marginVertical="4dp"
|
|
||||||
android:background="@color/text"
|
android:background="@color/text"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintEnd_toStartOf="@id/quoteStartBarrier"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
tools:visibility="gone"
|
|
||||||
android:id="@+id/quoteViewAttachmentPreviewContainer"
|
android:id="@+id/quoteViewAttachmentPreviewContainer"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:layout_centerVertical="true"
|
android:layout_centerVertical="true"
|
||||||
android:layout_marginVertical="@dimen/small_spacing"
|
android:layout_marginVertical="@dimen/small_spacing"
|
||||||
android:background="@drawable/view_quote_attachment_preview_background"
|
android:background="@drawable/view_quote_attachment_preview_background"
|
||||||
app:layout_constraintHorizontal_bias="0"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toStartOf="@id/quoteStartBarrier"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
@ -61,44 +61,45 @@
|
|||||||
app:barrierDirection="end"
|
app:barrierDirection="end"
|
||||||
app:constraint_referenced_ids="quoteViewAttachmentPreviewContainer,quoteViewAccentLine" />
|
app:constraint_referenced_ids="quoteViewAttachmentPreviewContainer,quoteViewAccentLine" />
|
||||||
|
|
||||||
<LinearLayout
|
<TextView
|
||||||
android:layout_marginVertical="@dimen/small_spacing"
|
android:id="@+id/quoteViewAuthorTextView"
|
||||||
android:id="@+id/quoteTextParent"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/medium_spacing"
|
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_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintStart_toEndOf="@+id/quoteStartBarrier"
|
app:layout_constraintStart_toEndOf="@+id/quoteStartBarrier"
|
||||||
app:layout_constraintEnd_toStartOf="@id/quoteViewCancelButton"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
android:orientation="vertical"
|
tools:text="Spiderman" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/quoteViewBodyTextView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
<TextView
|
android:layout_marginStart="@dimen/medium_spacing"
|
||||||
tools:visibility="gone"
|
android:ellipsize="end"
|
||||||
android:id="@+id/quoteViewAuthorTextView"
|
android:maxLines="3"
|
||||||
android:layout_width="match_parent"
|
android:textColor="@color/text"
|
||||||
android:layout_height="wrap_content"
|
android:textSize="@dimen/small_font_size"
|
||||||
android:ellipsize="end"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:maxLines="1"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:textColor="@color/text"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
android:textSize="@dimen/small_font_size"
|
app:layout_constraintStart_toEndOf="@+id/quoteStartBarrier"
|
||||||
android:textStyle="bold"
|
app:layout_constraintTop_toBottomOf="@+id/quoteViewAuthorTextView"
|
||||||
tools:text="Spiderman" />
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
android:maxWidth="240dp"
|
||||||
|
tools:maxLines="1"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
<TextView
|
<View
|
||||||
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:id="@+id/quoteViewCancelButton"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
@ -109,6 +110,7 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="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"?>
|
<?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:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -27,4 +27,4 @@
|
|||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
android:ellipsize="end" />
|
android:ellipsize="end" />
|
||||||
|
|
||||||
</LinearLayout>
|
</org.thoughtcrime.securesms.conversation.v2.messages.UntrustedAttachmentView>
|
@ -14,7 +14,7 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:padding="@dimen/medium_spacing">
|
android:padding="@dimen/medium_spacing">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<include layout="@layout/view_profile_picture"
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/medium_profile_picture_size"
|
android:layout_width="@dimen/medium_profile_picture_size"
|
||||||
android:layout_height="@dimen/medium_profile_picture_size" />
|
android:layout_height="@dimen/medium_profile_picture_size" />
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
@ -8,108 +10,117 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/dateBreakTextView"
|
android:id="@+id/dateBreakTextView"
|
||||||
android:layout_width="match_parent"
|
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:textColor="@color/text"
|
||||||
android:textSize="@dimen/very_small_font_size"
|
android:textSize="@dimen/very_small_font_size"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold" />
|
||||||
android:gravity="center" />
|
|
||||||
|
|
||||||
<RelativeLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/mainContainer"
|
android:id="@+id/mainContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="bottom">
|
android:gravity="bottom">
|
||||||
|
|
||||||
<FrameLayout
|
<View
|
||||||
android:layout_alignBottom="@+id/messageContentContainer"
|
android:id="@+id/startSpacing"
|
||||||
android:id="@+id/profilePictureContainer"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:layout_width="50dp"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="8dp"
|
||||||
android:orientation="horizontal">
|
android:layout_height="1dp"/>
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<include
|
||||||
android:id="@+id/profilePictureView"
|
tools:visibility="gone"
|
||||||
android:layout_width="@dimen/very_small_profile_picture_size"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_height="@dimen/very_small_profile_picture_size"
|
layout="@layout/view_profile_picture"
|
||||||
android:layout_gravity="center" />
|
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
|
<ImageView
|
||||||
android:id="@+id/moderatorIconImageView"
|
android:visibility="gone"
|
||||||
android:layout_width="16dp"
|
android:id="@+id/moderatorIconImageView"
|
||||||
android:layout_height="16dp"
|
android:layout_width="16dp"
|
||||||
android:layout_marginEnd="@dimen/small_spacing"
|
android:layout_height="16dp"
|
||||||
android:src="@drawable/ic_crown"
|
app:layout_constraintBottom_toBottomOf="@+id/profilePictureView"
|
||||||
android:layout_gravity="bottom|end" />
|
app:layout_constraintEnd_toEndOf="@+id/profilePictureView"
|
||||||
|
android:layout_marginEnd="-4dp"
|
||||||
|
android:layout_marginBottom="-4dp"
|
||||||
|
android:src="@drawable/ic_crown" />
|
||||||
|
|
||||||
</FrameLayout>
|
<TextView
|
||||||
|
android:id="@+id/senderNameTextView"
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/messageContentContainer"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="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
|
<LinearLayout
|
||||||
android:id="@+id/senderNameTextView"
|
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_width="wrap_content"
|
||||||
android:layout_height="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
|
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
|
||||||
android:id="@+id/expirationTimerViewContainer"
|
tools:visibility="visible"
|
||||||
android:orientation="horizontal"
|
android:visibility="gone"
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/expirationTimerView"
|
||||||
android:layout_height="wrap_content">
|
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
|
<View
|
||||||
android:id="@+id/messageContentView"
|
android:id="@+id/messageContentSpacing"
|
||||||
android:layout_width="wrap_content"
|
android:minWidth="@dimen/very_large_spacing"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_weight="1"
|
||||||
|
android:layout_width="0dp"
|
||||||
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
|
android:layout_height="0dp"/>
|
||||||
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>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</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>
|
@ -8,7 +8,7 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<!-- Content that will only show on its own -->
|
<!-- Content that will only show on its own -->
|
||||||
<org.thoughtcrime.securesms.conversation.v2.messages.DeletedMessageView
|
<include layout="@layout/view_deleted_message"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@ -16,8 +16,11 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:id="@+id/deletedMessageView"
|
android:id="@+id/deletedMessageView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"
|
||||||
<org.thoughtcrime.securesms.conversation.v2.messages.UntrustedAttachmentView
|
/>
|
||||||
|
|
||||||
|
<include layout="@layout/view_untrusted_attachment"
|
||||||
|
tools:visibility="gone"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@ -26,16 +29,19 @@
|
|||||||
android:id="@+id/untrustedView"
|
android:id="@+id/untrustedView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
<org.thoughtcrime.securesms.conversation.v2.messages.VoiceMessageView
|
|
||||||
|
<include layout="@layout/view_voice_message"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:id="@+id/voiceMessageView"
|
android:id="@+id/voiceMessageView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="160dp"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="36dp"/>
|
||||||
<org.thoughtcrime.securesms.conversation.v2.messages.OpenGroupInvitationView
|
|
||||||
|
<include layout="@layout/view_open_group_invitation"
|
||||||
|
tools:visibility="gone"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@ -44,11 +50,12 @@
|
|||||||
android:id="@+id/openGroupInvitationView"
|
android:id="@+id/openGroupInvitationView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
<org.thoughtcrime.securesms.conversation.v2.messages.DocumentView
|
|
||||||
|
<include layout="@layout/view_document"
|
||||||
|
tools:visibility="gone"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:visibility="visible"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:id="@+id/documentView"
|
android:id="@+id/documentView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -56,13 +63,13 @@
|
|||||||
|
|
||||||
<!-- Content that will show with other elements -->
|
<!-- Content that will show with other elements -->
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.conversation.v2.messages.QuoteView
|
<include layout="@layout/view_quote"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
tools:visibility="visible"
|
tools:visibility="visible"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:id="@+id/quoteView"
|
android:id="@+id/quoteView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
<org.thoughtcrime.securesms.conversation.v2.messages.LinkPreviewView
|
<org.thoughtcrime.securesms.conversation.v2.messages.LinkPreviewView
|
||||||
app:layout_constraintTop_toBottomOf="@+id/quoteView"
|
app:layout_constraintTop_toBottomOf="@+id/quoteView"
|
||||||
@ -98,5 +105,6 @@
|
|||||||
android:paddingVertical="@dimen/small_spacing"
|
android:paddingVertical="@dimen/small_spacing"
|
||||||
android:id="@+id/bodyTextView"
|
android:id="@+id/bodyTextView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
android:maxWidth="300dp"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout
|
<org.thoughtcrime.securesms.conversation.v2.messages.VoiceMessageView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/mainVoiceMessageViewContainer"
|
android:id="@+id/mainVoiceMessageViewContainer"
|
||||||
@ -67,4 +67,4 @@
|
|||||||
android:background="@drawable/view_voice_message_duration_text_view_background"
|
android:background="@drawable/view_voice_message_duration_text_view_background"
|
||||||
android:backgroundTint="@color/white" />
|
android:backgroundTint="@color/white" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</org.thoughtcrime.securesms.conversation.v2.messages.VoiceMessageView>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user