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:
Harris 2022-06-08 17:12:34 +10:00 committed by GitHub
parent db92034a8a
commit 6ddefb7a2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 3775 additions and 2357 deletions

View File

@ -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,

View File

@ -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) {

View File

@ -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 {

View File

@ -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()) {

View File

@ -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

View File

@ -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 {

View File

@ -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)
} }

View File

@ -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);

View File

@ -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

View File

@ -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
} }

View File

@ -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() {

View File

@ -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)

View File

@ -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)
} }

View File

@ -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)
} }

View File

@ -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

View File

@ -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()

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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...

View File

@ -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
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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>>()

View File

@ -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;

View File

@ -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)) {

View File

@ -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()

View File

@ -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() {

View File

@ -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

View File

@ -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 {

View File

@ -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) {

View File

@ -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)});

View File

@ -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>? {

View File

@ -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

View File

@ -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 {

View File

@ -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);

View File

@ -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)
} }
} }

View File

@ -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 + " = ?";

View File

@ -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) {

View File

@ -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;
} }
} }

View File

@ -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
}
}
}

View File

@ -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()) {

View File

@ -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);

View File

@ -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
}

View File

@ -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() {}
}
}

View File

@ -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);
} }

View File

@ -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

View File

@ -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)

View File

@ -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? {

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

View File

@ -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)
}
} }

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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);

View File

@ -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? {

View File

@ -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());
} }

View File

@ -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);

View File

@ -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 {

View File

@ -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;
} }

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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)
} }
} }

View File

@ -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

View File

@ -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)

View File

@ -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" />

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -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>

View File

@ -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