Merge branch 'dev'

This commit is contained in:
jubb 2022-06-08 17:13:38 +10:00
commit d0487c0eb8
135 changed files with 3884 additions and 2364 deletions

View File

@ -110,6 +110,7 @@ dependencies {
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation 'app.cash.copper:copper-flow:1.0.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
@ -158,8 +159,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.4'
}
def canonicalVersionCode = 279
def canonicalVersionName = "1.13.1"
def canonicalVersionCode = 282
def canonicalVersionName = "1.13.4"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -24,7 +24,7 @@ import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.HandlerThread;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
@ -44,6 +44,7 @@ import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.WindowDebouncer;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsignal.utilities.Log;
@ -93,6 +94,7 @@ import java.security.Security;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import javax.inject.Inject;
@ -127,7 +129,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
public Poller poller = null;
public Broadcaster broadcaster = null;
private Job firebaseInstanceIdJob;
private Handler conversationListNotificationHandler;
private WindowDebouncer conversationListDebouncer;
private HandlerThread conversationListHandlerThread;
private Handler conversationListHandler;
private PersistentLogger persistentLogger;
@Inject LokiAPIDatabase lokiAPIDatabase;
@ -136,9 +140,18 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject JobDatabase jobDatabase;
@Inject TextSecurePreferences textSecurePreferences;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
private volatile boolean isAppVisible;
@Override
public Object getSystemService(String name) {
if (MessagingModuleConfiguration.MESSAGING_MODULE_SERVICE.equals(name)) {
return messagingModuleConfiguration;
}
return super.getSystemService(name);
}
public static ApplicationContext getInstance(Context context) {
return (ApplicationContext) context.getApplicationContext();
}
@ -148,10 +161,21 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
public Handler getConversationListNotificationHandler() {
if (this.conversationListNotificationHandler == null) {
conversationListNotificationHandler = new Handler(Looper.getMainLooper());
if (this.conversationListHandlerThread == null) {
conversationListHandlerThread = new HandlerThread("ConversationListHandler");
conversationListHandlerThread.start();
}
return this.conversationListNotificationHandler;
if (this.conversationListHandler == null) {
conversationListHandler = new Handler(conversationListHandlerThread.getLooper());
}
return conversationListHandler;
}
public WindowDebouncer getConversationListDebouncer() {
if (conversationListDebouncer == null) {
conversationListDebouncer = new WindowDebouncer(1000, new Timer());
}
return conversationListDebouncer;
}
public PersistentLogger getPersistentLogger() {
@ -161,7 +185,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Override
public void onCreate() {
DatabaseModule.init(this);
MessagingModuleConfiguration.configure(this);
super.onCreate();
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
storage,
messageDataProvider,
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
Log.i(TAG, "onCreate()");
startKovenant();
@ -174,11 +203,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
broadcaster = new Broadcaster(this);
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
MessagingModuleConfiguration.Companion.configure(this,
storage,
messageDataProvider,
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this)
);
SnodeModule.Companion.configure(apiDB, broadcaster);
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey != null) {

View File

@ -39,11 +39,14 @@ import android.widget.ImageView;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal;
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.crypto.BiometricSecretProvider;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
import java.security.Signature;
import network.loki.messenger.R;
@ -61,6 +64,8 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
private CancellationSignal fingerprintCancellationSignal;
private FingerprintListener fingerprintListener;
private final BiometricSecretProvider biometricSecretProvider = new BiometricSecretProvider();
private boolean authenticated;
private boolean failure;
@ -200,7 +205,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
if (fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) {
Log.i(TAG, "Listening for fingerprints...");
fingerprintCancellationSignal = new CancellationSignal();
fingerprintManager.authenticate(null, 0, fingerprintCancellationSignal, fingerprintListener, null);
fingerprintManager.authenticate(new FingerprintManagerCompat.CryptoObject(biometricSecretProvider.getOrCreateBiometricSignature(this)), 0, fingerprintCancellationSignal, fingerprintListener, null);
} else {
Log.i(TAG, "firing intent...");
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock Session", "");
@ -224,6 +229,27 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
Log.i(TAG, "onAuthenticationSucceeded");
if (result.getCryptoObject() == null || result.getCryptoObject().getSignature() == null) {
// authentication failed
onAuthenticationFailed();
return;
}
// Signature object now successfully unlocked
boolean authenticationSucceeded = false;
try {
Signature signature = result.getCryptoObject().getSignature();
byte[] random = biometricSecretProvider.getRandomData();
signature.update(random);
byte[] signed = signature.sign();
authenticationSucceeded = biometricSecretProvider.verifySignature(random, signed);
} catch (Exception e) {
Log.e(TAG, "onAuthentication signature generation and verification failed", e);
}
if (!authenticationSucceeded) {
onAuthenticationFailed();
return;
}
fingerprintPrompt.setImageResource(R.drawable.ic_check_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
fingerprintPrompt.animate().setInterpolator(new BounceInterpolator()).scaleX(1.1f).scaleY(1.1f).setDuration(500).setListener(new AnimationCompleteListener() {
@ -239,7 +265,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
@Override
public void onAuthenticationFailed() {
Log.w(TAG, "onAuthenticatoinFailed()");
Log.w(TAG, "onAuthenticationFailed()");
fingerprintPrompt.setImageResource(R.drawable.ic_close_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);

View File

@ -5,7 +5,14 @@ import android.text.TextUtils
import com.google.protobuf.ByteString
import org.greenrobot.eventbus.EventBus
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.sending_receiving.attachments.*
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.UploadResult
import org.session.libsession.utilities.Util
@ -126,7 +133,7 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
return mmsDb.getMessage(mmsMessageId).use { cursor ->
mmsDb.readerFor(cursor).next
}.isOutgoing
}?.isOutgoing ?: false
}
override fun isOutgoingMessage(timestamp: Long): Boolean {

View File

@ -10,29 +10,51 @@ import com.annimon.stream.function.Predicate
import com.google.protobuf.ByteString
import net.sqlcipher.database.SQLiteDatabase
import org.greenrobot.eventbus.EventBus
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.utilities.Conversions
import org.thoughtcrime.securesms.backup.BackupProtos.*
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
import org.thoughtcrime.securesms.database.*
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
import org.thoughtcrime.securesms.util.BackupUtil
import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil
import java.io.*
import java.lang.Exception
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
import org.thoughtcrime.securesms.backup.BackupProtos.Header
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
import org.thoughtcrime.securesms.backup.BackupProtos.Sticker
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.JobDatabase
import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.PushDatabase
import org.thoughtcrime.securesms.database.SearchDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.util.BackupUtil
import java.io.Closeable
import java.io.File
import java.io.FileInputStream
import java.io.Flushable
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.*
import java.util.LinkedList
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.Mac
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ -245,8 +267,8 @@ object FullBackupExporter {
}
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
val columns = arrayOf(MmsDatabase.EXPIRES_IN)
val where = MmsDatabase.ID + " = ?"
val columns = arrayOf(MmsSmsColumns.EXPIRES_IN)
val where = MmsSmsColumns.ID + " = ?"
val args = arrayOf(mmsId.toString())
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
if (mmsCursor != null && mmsCursor.moveToFirst()) {

View File

@ -15,19 +15,39 @@ import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.BackupProtos.*
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
import org.thoughtcrime.securesms.database.*
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.SearchDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.BackupUtil
import java.io.*
import java.io.Closeable
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.*
import java.util.LinkedList
import java.util.Locale
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.Mac
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ -172,7 +192,7 @@ object FullBackupImporter {
}
private fun trimEntriesForExpiredMessages(context: Context, db: SQLiteDatabase) {
val trimmedCondition = " NOT IN (SELECT ${MmsDatabase.ID} FROM ${MmsDatabase.TABLE_NAME})"
val trimmedCondition = " NOT IN (SELECT ${MmsSmsColumns.ID} FROM ${MmsDatabase.TABLE_NAME})"
db.delete(GroupReceiptDatabase.TABLE_NAME, GroupReceiptDatabase.MMS_ID + trimmedCondition, null)
val columns = arrayOf(AttachmentDatabase.ROW_ID, AttachmentDatabase.UNIQUE_ID)
val where = AttachmentDatabase.MMS_ID + trimmedCondition

View File

@ -8,28 +8,26 @@ import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.session.libsession.avatars.ContactColors;
import org.session.libsession.avatars.ContactPhoto;
import org.session.libsession.avatars.ResourceContactPhoto;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.ThemeUtil;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.RecipientExporter;
import org.session.libsession.utilities.ThemeUtil;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
import java.util.Objects;
@ -139,7 +137,7 @@ public class AvatarImageView extends AppCompatImageView {
requestManager.load(photo.contactPhoto)
.fallback(photoPlaceholderDrawable)
.error(photoPlaceholderDrawable)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(this);
} else {

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
@ -10,16 +9,20 @@ import androidx.annotation.DimenRes
import com.bumptech.glide.load.engine.DiskCacheStrategy
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewProfilePictureBinding
import org.session.libsession.avatars.ContactColors
import org.session.libsession.avatars.PlaceholderAvatarPhoto
import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.avatars.ResourceContactPhoto
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
class ProfilePictureView : RelativeLayout {
private lateinit var binding: ViewProfilePictureBinding
class ProfilePictureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : RelativeLayout(context, attrs) {
private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) }
lateinit var glide: GlideRequests
var publicKey: String? = null
var displayName: String? = null
@ -28,16 +31,9 @@ class ProfilePictureView : RelativeLayout {
var isLarge = false
private val profilePicturesCache = mutableMapOf<String, String?>()
private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() }
private fun initialize() {
binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this, true)
}
// endregion
// region Updating
@ -105,21 +101,24 @@ class ProfilePictureView : RelativeLayout {
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
val signalProfilePicture = recipient.contactPhoto
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
val sizeInPX = resources.getDimensionPixelSize(sizeResId)
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
glide.clear(imageView)
glide.load(signalProfilePicture)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.circleCrop()
.error(AvatarPlaceholderGenerator.generate(context,sizeInPX, publicKey, displayName))
.into(imageView)
profilePicturesCache[publicKey] = recipient.profileAvatar
.placeholder(unknownRecipientDrawable)
.centerCrop()
.error(unknownRecipientDrawable)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(imageView)
} else {
glide.clear(imageView)
glide.load(AvatarPlaceholderGenerator.generate(context, sizeInPX, publicKey, displayName))
.diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
profilePicturesCache[publicKey] = recipient.profileAvatar
glide.load(placeholder)
.placeholder(unknownRecipientDrawable)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
}
profilePicturesCache[publicKey] = recipient.profileAvatar
} else {
imageView.setImageDrawable(null)
}

View File

@ -264,7 +264,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
}
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(thumbnailView);
} else if (!documentSlides.isEmpty()){
thumbnailView.setVisibility(GONE);

View File

@ -35,6 +35,13 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
return items.size
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
if (holder is UserViewHolder) {
holder.view.unbind()
}
}
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is ContactSelectionListItem.Header -> ViewType.Divider

View File

@ -54,8 +54,8 @@ class UserView : LinearLayout {
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
val address = user.address.serialize()
binding.profilePictureView.glide = glide
binding.profilePictureView.update(user)
binding.profilePictureView.root.glide = glide
binding.profilePictureView.root.update(user)
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
when (actionIndicator) {
@ -83,7 +83,7 @@ class UserView : LinearLayout {
}
fun unbind() {
binding.profilePictureView.root.recycle()
}
// endregion
}

View File

@ -49,6 +49,7 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding
import network.loki.messenger.databinding.ActivityConversationV2Binding
import network.loki.messenger.databinding.ViewVisibleMessageBinding
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.mentions.Mention
@ -241,7 +242,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
actionMode?.let {
onDeselect(message, position, it)
}
}
},
lifecycleCoroutineScope = lifecycleScope
)
adapter.visibleMessageContentViewDelegate = this
adapter
@ -314,11 +316,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
scrollToFirstUnreadMessageIfNeeded()
showOrHideInputIfNeeded()
setUpMessageRequestsBar()
if (viewModel.recipient.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
if (openGroup == null) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish()
viewModel.recipient?.let { recipient ->
if (recipient.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
if (openGroup == null) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish()
}
}
}
}
@ -326,7 +330,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun onResume() {
super.onResume()
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId)
threadDb.markAllAsRead(viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
val recipient = viewModel.recipient ?: return
threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient)
}
override fun onPause() {
@ -391,17 +396,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
actionBar.title = ""
actionBar.customView = actionBarBinding!!.root
actionBar.setDisplayShowCustomEnabled(true)
actionBarBinding!!.conversationTitleView.text = viewModel.recipient.toShortString()
@DimenRes val sizeID: Int = if (viewModel.recipient.isClosedGroupRecipient) {
actionBarBinding!!.conversationTitleView.text = viewModel.recipient?.toShortString()
@DimenRes val sizeID: Int = if (viewModel.recipient?.isClosedGroupRecipient == true) {
R.dimen.medium_profile_picture_size
} else {
R.dimen.small_profile_picture_size
}
val size = resources.getDimension(sizeID).roundToInt()
actionBarBinding!!.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
actionBarBinding!!.profilePictureView.glide = glide
actionBarBinding!!.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
actionBarBinding!!.profilePictureView.root.glide = glide
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
actionBarBinding!!.profilePictureView.update(viewModel.recipient)
val profilePictureView = actionBarBinding!!.profilePictureView.root
viewModel.recipient?.let { recipient ->
profilePictureView.update(recipient)
}
}
// called from onCreate
@ -437,7 +445,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (mediaURI != null && mediaType != null) {
if (AttachmentManager.MediaType.IMAGE == mediaType || AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType) {
val media = Media(mediaURI, MediaUtil.getMimeType(this, mediaURI)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent())
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient, ""), PICK_FROM_LIBRARY)
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient!!, ""), PICK_FROM_LIBRARY)
return
} else {
prepMediaForSending(mediaURI, mediaType).addListener(object : ListenableFuture.Listener<Boolean> {
@ -491,11 +499,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun setUpRecipientObserver() {
viewModel.recipient.addListener(this)
viewModel.recipient?.addListener(this)
}
private fun tearDownRecipientObserver() {
viewModel.recipient.removeListener(this)
viewModel.recipient?.removeListener(this)
}
private fun getLatestOpenGroupInfoIfNeeded() {
@ -505,12 +513,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate
private fun setUpBlockedBanner() {
if (viewModel.recipient.isGroupRecipient) { return }
val sessionID = viewModel.recipient.address.toString()
val recipient = viewModel.recipient ?: return
if (recipient.isGroupRecipient) { return }
val sessionID = recipient.address.toString()
val contact = sessionContactDb.getContactWithSessionID(sessionID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
binding?.blockedBannerTextView?.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
binding?.blockedBanner?.isVisible = viewModel.recipient.isBlocked
binding?.blockedBanner?.isVisible = recipient.isBlocked
binding?.blockedBanner?.setOnClickListener { viewModel.unblock() }
}
@ -558,13 +567,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
if (!isMessageRequestThread()) {
ConversationMenuHelper.onPrepareOptionsMenu(
menu,
menuInflater,
viewModel.recipient,
viewModel.threadId,
this
) { onOptionsItemSelected(it) }
val recipient = viewModel.recipient
if (recipient != null) {
ConversationMenuHelper.onPrepareOptionsMenu(
menu,
menuInflater,
recipient,
viewModel.threadId,
this
) { onOptionsItemSelected(it) }
}
}
super.onPrepareOptionsMenu(menu)
return true
@ -582,21 +594,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// region Animation & Updating
override fun onModified(recipient: Recipient) {
runOnUiThread {
if (viewModel.recipient.isContactRecipient) {
binding?.blockedBanner?.isVisible = viewModel.recipient.isBlocked
val recipient = viewModel.recipient
if (recipient != null && recipient.isContactRecipient) {
binding?.blockedBanner?.isVisible = recipient.isBlocked
}
setUpMessageRequestsBar()
invalidateOptionsMenu()
updateSubtitle()
showOrHideInputIfNeeded()
actionBarBinding?.profilePictureView?.update(recipient)
actionBarBinding?.conversationTitleView?.text = recipient.toShortString()
if (recipient != null) {
actionBarBinding?.profilePictureView?.root?.update(recipient)
}
actionBarBinding?.conversationTitleView?.text = recipient?.toShortString()
}
}
private fun showOrHideInputIfNeeded() {
if (viewModel.recipient.isClosedGroupRecipient) {
val group = groupDb.getGroup(viewModel.recipient.address.toGroupString()).orNull()
val recipient = viewModel.recipient
if (recipient != null && recipient.isClosedGroupRecipient) {
val group = groupDb.getGroup(recipient.address.toGroupString()).orNull()
val isActive = (group?.isActive == true)
binding?.inputBar?.showInput = isActive
} else {
@ -632,17 +648,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun isMessageRequestThread(): Boolean {
return !viewModel.recipient.isGroupRecipient && !viewModel.recipient.isApproved
val recipient = viewModel.recipient ?: return false
return !recipient.isGroupRecipient && !recipient.isApproved
}
private fun isOutgoingMessageRequestThread(): Boolean {
return !viewModel.recipient.isGroupRecipient &&
!(viewModel.recipient.hasApprovedMe() || viewModel.hasReceived())
val recipient = viewModel.recipient ?: return false
return !recipient.isGroupRecipient &&
!recipient.isLocalNumber &&
!(recipient.hasApprovedMe() || viewModel.hasReceived())
}
private fun isIncomingMessageRequestThread(): Boolean {
return !viewModel.recipient.isGroupRecipient &&
!viewModel.recipient.isApproved &&
val recipient = viewModel.recipient ?: return false
return !recipient.isGroupRecipient &&
!recipient.isApproved &&
!recipient.isLocalNumber &&
!threadDb.getLastSeenAndHasSent(viewModel.threadId).second() &&
threadDb.getMessageCount(viewModel.threadId) > 0
}
@ -701,17 +722,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun showOrUpdateMentionCandidatesIfNeeded(query: String = "") {
val additionalContentContainer = binding?.additionalContentContainer ?: return
val recipient = viewModel.recipient ?: return
if (!isShowingMentionCandidatesView) {
additionalContentContainer.removeAllViews()
val view = MentionCandidatesView(this)
view.glide = glide
view.onCandidateSelected = { handleMentionSelected(it) }
additionalContentContainer.addView(view)
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
this.mentionCandidatesView = view
view.show(candidates, viewModel.threadId)
} else {
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
this.mentionCandidatesView!!.setMentionCandidates(candidates)
}
isShowingMentionCandidatesView = true
@ -839,15 +861,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun updateSubtitle() {
val actionBarBinding = actionBarBinding ?: return
actionBarBinding.muteIconImageView.isVisible = viewModel.recipient.isMuted
val recipient = viewModel.recipient ?: return
actionBarBinding.muteIconImageView.isVisible = recipient.isMuted
actionBarBinding.conversationSubtitleView.isVisible = true
if (viewModel.recipient.isMuted) {
if (viewModel.recipient.mutedUntil != Long.MAX_VALUE) {
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(viewModel.recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
if (recipient.isMuted) {
if (recipient.mutedUntil != Long.MAX_VALUE) {
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
} else {
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever)
}
} else if (viewModel.recipient.isGroupRecipient) {
} else if (recipient.isGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
if (openGroup != null) {
val userCount = lokiApiDb.getUserCount(openGroup.room, openGroup.server) ?: 0
@ -866,7 +889,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (item.itemId == android.R.id.home) {
return false
}
return ConversationMenuHelper.onOptionItemSelected(this, item, viewModel.recipient)
return viewModel.recipient?.let { recipient ->
ConversationMenuHelper.onOptionItemSelected(this, item, recipient)
} ?: false
}
// `position` is the adapter position; not the visual position
@ -896,7 +921,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// `position` is the adapter position; not the visual position
private fun handleSwipeToReply(message: MessageRecord, position: Int) {
binding?.inputBar?.draftQuote(viewModel.recipient, message, glide)
val recipient = viewModel.recipient ?: return
binding?.inputBar?.draftQuote(recipient, message, glide)
}
// `position` is the adapter position; not the visual position
@ -1002,12 +1028,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) {
if (indexInAdapter < 0 || indexInAdapter >= adapter.itemCount) { return }
val viewHolder = binding?.conversationRecyclerView?.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder ?: return
viewHolder.view.playVoiceMessage()
val visibleMessageView = ViewVisibleMessageBinding.bind(viewHolder.view).visibleMessageView
visibleMessageView.playVoiceMessage()
}
override fun sendMessage() {
if (viewModel.recipient.isContactRecipient && viewModel.recipient.isBlocked) {
BlockedDialog(viewModel.recipient).show(supportFragmentManager, "Blocked Dialog")
val recipient = viewModel.recipient ?: return
if (recipient.isContactRecipient && recipient.isBlocked) {
BlockedDialog(recipient).show(supportFragmentManager, "Blocked Dialog")
return
}
val binding = binding ?: return
@ -1019,24 +1047,26 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun commitInputContent(contentUri: Uri) {
val recipient = viewModel.recipient ?: return
val media = Media(contentUri, MediaUtil.getMimeType(this, contentUri)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent())
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient, getMessageBody()), PICK_FROM_LIBRARY)
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), recipient, getMessageBody()), PICK_FROM_LIBRARY)
}
private fun processMessageRequestApproval() {
if (isIncomingMessageRequestThread()) {
acceptMessageRequest()
} else if (!viewModel.recipient.isApproved) {
} else if (viewModel.recipient?.isApproved == false) {
// edge case for new outgoing thread on new recipient without sending approval messages
viewModel.setRecipientApproved()
}
}
private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false) {
val recipient = viewModel.recipient ?: return
processMessageRequestApproval()
val text = getMessageBody()
val userPublicKey = textSecurePreferences.getLocalNumber()
val isNoteToSelf = (viewModel.recipient.isContactRecipient && viewModel.recipient.address.toString() == userPublicKey)
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
val dialog = SendSeedDialog { sendTextOnlyMessage(true) }
return dialog.show(supportFragmentManager, "Send Seed Dialog")
@ -1045,7 +1075,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis()
message.text = text
val outgoingTextMessage = OutgoingTextMessage.from(message, viewModel.recipient)
val outgoingTextMessage = OutgoingTextMessage.from(message, recipient)
// Clear the input bar
binding?.inputBar?.text = ""
binding?.inputBar?.cancelQuoteDraft()
@ -1055,14 +1085,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
currentMentionStartIndex = -1
mentions.clear()
// Put the message in the database
message.id = smsDb.insertMessageOutbox(viewModel.threadId, outgoingTextMessage, false, message.sentTimestamp!!) { }
message.id = smsDb.insertMessageOutbox(viewModel.threadId, outgoingTextMessage, false, message.sentTimestamp!!, null, true)
// Send it
MessageSender.send(message, viewModel.recipient.address)
MessageSender.send(message, recipient.address)
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
}
private fun sendAttachments(attachments: List<Attachment>, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
val recipient = viewModel.recipient ?: return
processMessageRequestApproval()
// Create the message
val message = VisibleMessage()
@ -1073,7 +1104,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val sender = if (it.isOutgoing) fromSerialized(textSecurePreferences.getLocalNumber()!!) else it.individualRecipient.address
QuoteModel(it.dateSent, sender, it.body, false, quotedAttachments)
}
val outgoingTextMessage = OutgoingMediaMessage.from(message, viewModel.recipient, attachments, quote, linkPreview)
val outgoingTextMessage = OutgoingMediaMessage.from(message, recipient, attachments, quote, linkPreview)
// Clear the input bar
binding?.inputBar?.text = ""
binding?.inputBar?.cancelQuoteDraft()
@ -1087,9 +1118,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// Reset attachments button if needed
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
// Put the message in the database
message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, viewModel.threadId, false) { }
message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, viewModel.threadId, false, null, runThreadUpdate = true)
// Send it
MessageSender.send(message, viewModel.recipient.address, attachments, quote, linkPreview)
MessageSender.send(message, recipient.address, attachments, quote, linkPreview)
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
}
@ -1119,8 +1150,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun pickFromLibrary() {
val recipient = viewModel.recipient ?: return
binding?.inputBar?.text?.trim()?.let { text ->
AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, viewModel.recipient, text)
AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, recipient, text)
}
}
@ -1187,7 +1219,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
sendAttachments(slideDeck.asAttachments(), body)
}
INVITE_CONTACTS -> {
if (!viewModel.recipient.isOpenGroupRecipient) { return }
if (viewModel.recipient?.isOpenGroupRecipient != true) { return }
val extras = intent?.extras ?: return
if (!intent.hasExtra(selectedContactsKey)) { return }
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
@ -1268,13 +1300,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun deleteMessages(messages: Set<MessageRecord>) {
val recipient = viewModel.recipient ?: return
if (!IS_UNSEND_REQUESTS_ENABLED) {
deleteMessagesWithoutUnsendRequest(messages)
return
}
val allSentByCurrentUser = messages.all { it.isOutgoing }
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id) != null }
if (viewModel.recipient.isOpenGroupRecipient) {
if (recipient.isOpenGroupRecipient) {
val messageCount = messages.size
val builder = AlertDialog.Builder(this)
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
@ -1293,7 +1326,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
builder.show()
} else if (allSentByCurrentUser && allHasHash) {
val bottomSheet = DeleteOptionsBottomSheet()
bottomSheet.recipient = viewModel.recipient
bottomSheet.recipient = recipient
bottomSheet.onDeleteForMeTapped = {
for (message in messages) {
viewModel.deleteLocally(message)
@ -1452,16 +1485,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun reply(messages: Set<MessageRecord>) {
binding?.inputBar?.draftQuote(viewModel.recipient, messages.first(), glide)
val recipient = viewModel.recipient ?: return
binding?.inputBar?.draftQuote(recipient, messages.first(), glide)
endActionMode()
}
private fun sendMediaSavedNotification() {
if (viewModel.recipient.isGroupRecipient) { return }
val recipient = viewModel.recipient ?: return
if (recipient.isGroupRecipient) { return }
val timestamp = System.currentTimeMillis()
val kind = DataExtractionNotification.Kind.MediaSaved(timestamp)
val message = DataExtractionNotification(kind)
MessageSender.send(message, viewModel.recipient.address)
MessageSender.send(message, recipient.address)
}
private fun endActionMode() {

View File

@ -4,10 +4,25 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.util.SparseArray
import android.util.SparseBooleanArray
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.annotation.WorkerThread
import androidx.core.util.getOrDefault
import androidx.core.util.set
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
@ -19,13 +34,33 @@ import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit,
private val glide: GlideRequests, private val onDeselect: (MessageRecord, Int) -> Unit)
private val glide: GlideRequests, private val onDeselect: (MessageRecord, Int) -> Unit, lifecycleCoroutineScope: LifecycleCoroutineScope)
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
private val messageDB = DatabaseComponent.get(context).mmsSmsDatabase()
private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() }
private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() }
var selectedItems = mutableSetOf<MessageRecord>()
private var searchQuery: String? = null
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null
private val updateQueue = Channel<String>(1024, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val contactCache = SparseArray<Contact>(100)
private val contactLoadedCache = SparseBooleanArray(100)
init {
lifecycleCoroutineScope.launch(IO) {
while (isActive) {
val item = updateQueue.receive()
val contact = getSenderInfo(item) ?: continue
contactCache[item.hashCode()] = contact
contactLoadedCache[item.hashCode()] = true
}
}
}
@WorkerThread
private fun getSenderInfo(sender: String): Contact? {
return contactDB.getContactWithSessionID(sender)
}
sealed class ViewType(val rawValue: Int) {
object Visible : ViewType(0)
object Control : ViewType(1)
@ -39,7 +74,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
}
}
class VisibleMessageViewHolder(val view: VisibleMessageView) : ViewHolder(view)
class VisibleMessageViewHolder(val view: View) : ViewHolder(view)
class ControlMessageViewHolder(val view: ControlMessageView) : ViewHolder(view)
override fun getItemViewType(cursor: Cursor): Int {
@ -52,7 +87,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
@Suppress("NAME_SHADOWING")
val viewType = ViewType.allValues[viewType]
return when (viewType) {
ViewType.Visible -> VisibleMessageViewHolder(VisibleMessageView(context))
ViewType.Visible -> VisibleMessageViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.view_visible_message, parent, false))
ViewType.Control -> ControlMessageViewHolder(ControlMessageView(context))
else -> throw IllegalStateException("Unexpected view type: $viewType.")
}
@ -65,20 +100,31 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
when (viewHolder) {
is VisibleMessageViewHolder -> {
val view = viewHolder.view
val visibleMessageView = ViewVisibleMessageBinding.bind(viewHolder.view).visibleMessageView
val isSelected = selectedItems.contains(message)
view.snIsSelected = isSelected
view.indexInAdapter = position
view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery)
if (!message.isDeleted) {
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
} else {
view.onPress = null
view.onSwipeToReply = null
view.onLongPress = null
visibleMessageView.snIsSelected = isSelected
visibleMessageView.indexInAdapter = position
val senderId = message.individualRecipient.address.serialize()
val senderIdHash = senderId.hashCode()
updateQueue.trySend(senderId)
if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault(senderIdHash, false)) {
getSenderInfo(senderId)?.let { contact ->
contactCache[senderIdHash] = contact
}
}
view.contentViewDelegate = visibleMessageContentViewDelegate
val contact = contactCache[senderIdHash]
visibleMessageView.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery, contact, senderId)
if (!message.isDeleted) {
visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) }
visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
visibleMessageView.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
} else {
visibleMessageView.onPress = null
visibleMessageView.onSwipeToReply = null
visibleMessageView.onLongPress = null
}
visibleMessageView.contentViewDelegate = visibleMessageContentViewDelegate
}
is ControlMessageViewHolder -> {
viewHolder.view.bind(message, messageBefore)
@ -105,7 +151,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
override fun onItemViewRecycled(viewHolder: ViewHolder?) {
when (viewHolder) {
is VisibleMessageViewHolder -> viewHolder.view.recycle()
is VisibleMessageViewHolder -> viewHolder.view.findViewById<VisibleMessageView>(R.id.visibleMessageView).recycle()
is ControlMessageViewHolder -> viewHolder.view.recycle()
}
super.onItemViewRecycled(viewHolder)

View File

@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
import java.util.UUID
@ -22,8 +23,8 @@ class ConversationViewModel(
private val _uiState = MutableStateFlow(ConversationUiState())
val uiState: StateFlow<ConversationUiState> = _uiState
val recipient: Recipient
get() = repository.getRecipientForThreadId(threadId)
val recipient: Recipient?
get() = repository.maybeGetRecipientForThreadId(threadId)
init {
_uiState.update {
@ -44,20 +45,24 @@ class ConversationViewModel(
}
fun unblock() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for unblock action")
if (recipient.isContactRecipient) {
repository.unblock(recipient)
}
}
fun deleteLocally(message: MessageRecord) {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for delete locally action")
repository.deleteLocally(recipient, message)
}
fun setRecipientApproved() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for set approved action")
repository.setApproved(recipient, true)
}
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
val recipient = recipient ?: return@launch
repository.deleteForEveryone(threadId, recipient, message)
.onFailure {
showMessage("Couldn't delete message due to error: $it")
@ -92,6 +97,7 @@ class ConversationViewModel(
}
fun acceptMessageRequest() = viewModelScope.launch {
val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for accept message request action")
repository.acceptMessageRequest(threadId, recipient)
.onSuccess {
_uiState.update {
@ -104,6 +110,7 @@ class ConversationViewModel(
}
fun declineMessageRequest() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for decline message request action")
repository.declineMessageRequest(threadId, recipient)
}

View File

@ -118,7 +118,7 @@ class AlbumThumbnailView : FrameLayout {
this.slideSize = slides.size
}
// iterate binding
slides.take(5).forEachIndexed { position, slide ->
slides.take(MAX_ALBUM_DISPLAY_SIZE).forEachIndexed { position, slide ->
val thumbnailView = getThumbnailView(position)
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
}

View File

@ -28,11 +28,11 @@ class MentionCandidateView : LinearLayout {
private fun update() = with(binding) {
mentionCandidateNameTextView.text = mentionCandidate.displayName
profilePictureView.publicKey = mentionCandidate.publicKey
profilePictureView.displayName = mentionCandidate.displayName
profilePictureView.additionalPublicKey = null
profilePictureView.glide = glide!!
profilePictureView.update()
profilePictureView.root.publicKey = mentionCandidate.publicKey
profilePictureView.root.displayName = mentionCandidate.displayName
profilePictureView.root.additionalPublicKey = null
profilePictureView.root.glide = glide!!
profilePictureView.root.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupAPIV2.isUserModerator(mentionCandidate.publicKey, openGroupRoom!!, openGroupServer!!)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE

View File

@ -9,6 +9,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import network.loki.messenger.R
import network.loki.messenger.databinding.DialogJoinOpenGroupBinding
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
@ -37,6 +38,7 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : B
val activity = requireContext() as AppCompatActivity
ThreadUtils.queue {
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(url)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
}
dismiss()

View File

@ -122,9 +122,12 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
linkPreview = null
linkPreviewDraftView = null
binding.inputBarAdditionalContentContainer.removeAllViews()
val quoteView = QuoteView(context, QuoteView.Mode.Draft)
// inflate quoteview with typed array here
val layout = LayoutInflater.from(context).inflate(R.layout.view_quote_draft, binding.inputBarAdditionalContentContainer, false)
val quoteView = layout.findViewById<QuoteView>(R.id.mainQuoteViewContainer)
quoteView.delegate = this
binding.inputBarAdditionalContentContainer.addView(quoteView)
binding.inputBarAdditionalContentContainer.addView(layout)
val attachments = (message as? MmsMessageRecord)?.slideDeck
val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize()
quoteView.bind(sender, message.body, attachments,

View File

@ -28,11 +28,11 @@ class MentionCandidateView : RelativeLayout {
private fun update() = with(binding) {
mentionCandidateNameTextView.text = candidate.displayName
profilePictureView.publicKey = candidate.publicKey
profilePictureView.displayName = candidate.displayName
profilePictureView.additionalPublicKey = null
profilePictureView.glide = glide!!
profilePictureView.update()
profilePictureView.root.publicKey = candidate.publicKey
profilePictureView.root.displayName = candidate.displayName
profilePictureView.root.additionalPublicKey = null
profilePictureView.root.glide = glide!!
profilePictureView.root.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupAPIV2.isUserModerator(candidate.publicKey, openGroupRoom!!, openGroupServer!!)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE

View File

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import network.loki.messenger.R
@ -11,15 +10,12 @@ import network.loki.messenger.databinding.ViewDeletedMessageBinding
import org.thoughtcrime.securesms.database.model.MessageRecord
class DeletedMessageView : LinearLayout {
private lateinit var binding: ViewDeletedMessageBinding
private val binding: ViewDeletedMessageBinding by lazy { ViewDeletedMessageBinding.bind(this) }
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private fun initialize() {
binding = ViewDeletedMessageBinding.inflate(LayoutInflater.from(context), this, true)
}
// endregion
// region Updating

View File

@ -3,22 +3,17 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import network.loki.messenger.databinding.ViewDocumentBinding
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
class DocumentView : LinearLayout {
private lateinit var binding: ViewDocumentBinding
private val binding: ViewDocumentBinding by lazy { ViewDocumentBinding.bind(this) }
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() {
binding = ViewDocumentBinding.inflate(LayoutInflater.from(context), this, true)
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
// endregion
// region Updating

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
@ -14,16 +13,12 @@ import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog
import org.thoughtcrime.securesms.database.model.MessageRecord
class OpenGroupInvitationView : LinearLayout {
private lateinit var binding: ViewOpenGroupInvitationBinding
private val binding: ViewOpenGroupInvitationBinding by lazy { ViewOpenGroupInvitationBinding.bind(this) }
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
constructor(context: Context): super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() {
binding = ViewOpenGroupInvitationBinding.inflate(LayoutInflater.from(context), this, true)
}
constructor(context: Context): super(context)
constructor(context: Context, attrs: AttributeSet?): super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr)
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
// FIXME: This is a really weird approach...

View File

@ -2,12 +2,11 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.content.res.ColorStateList
import android.text.StaticLayout
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.ResourcesCompat
import androidx.core.content.res.use
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
@ -16,17 +15,13 @@ import network.loki.messenger.databinding.ViewQuoteBinding
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.database.model.Quote
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.util.toPx
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.min
// There's quite some calculation going on here. It's a bit complex so don't make changes
// if you don't need to. If you do then test:
@ -35,27 +30,29 @@ import kotlin.math.min
// • Quoted voice messages and documents in both private chats and group chats
// • All of the above in both dark mode and light mode
@AndroidEntryPoint
class QuoteView : LinearLayout {
class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) {
@Inject lateinit var contactDb: SessionContactDatabase
private lateinit var binding: ViewQuoteBinding
private lateinit var mode: Mode
private val binding: ViewQuoteBinding by lazy { ViewQuoteBinding.bind(this) }
private val vPadding by lazy { toPx(6, resources) }
var delegate: QuoteViewDelegate? = null
private val mode: Mode
enum class Mode { Regular, Draft }
// region Lifecycle
constructor(context: Context) : this(context, Mode.Regular)
constructor(context: Context, attrs: AttributeSet) : this(context, Mode.Regular, attrs)
init {
mode = attrs?.let { attrSet ->
context.obtainStyledAttributes(attrSet, R.styleable.QuoteView).use { typedArray ->
val modeIndex = typedArray.getInt(R.styleable.QuoteView_quote_mode, 0)
Mode.values()[modeIndex]
}
} ?: Mode.Regular
}
constructor(context: Context, mode: Mode, attrs: AttributeSet? = null) : super(context, attrs) {
this.mode = mode
binding = ViewQuoteBinding.inflate(LayoutInflater.from(context), this, true)
// Add padding here (not on binding.mainQuoteViewContainer) to get a bit of a top inset while avoiding
// the clipping issue described in getIntrinsicHeight(maxContentWidth:).
setPadding(0, toPx(6, resources), 0, 0)
// region Lifecycle
override fun onFinishInflate() {
super.onFinishInflate()
when (mode) {
Mode.Draft -> binding.quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
Mode.Regular -> {
@ -66,44 +63,6 @@ class QuoteView : LinearLayout {
}
// endregion
// region General
fun getIntrinsicContentHeight(maxContentWidth: Int): Int {
// If we're showing an attachment thumbnail, just constrain to the height of that
if (binding.quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) }
var result = 0
val authorTextViewIntrinsicHeight: Int
if (binding.quoteViewAuthorTextView.isVisible) {
val author = binding.quoteViewAuthorTextView.text
authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, binding.quoteViewAuthorTextView.paint, maxContentWidth)
result += authorTextViewIntrinsicHeight
}
val body = binding.quoteViewBodyTextView.text
val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
val staticLayout = TextUtilities.getIntrinsicLayout(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
result += bodyTextViewIntrinsicHeight
if (!binding.quoteViewAuthorTextView.isVisible) {
// We want to at least be as high as the cancel button 36DP, and no higher than 3 lines of text.
// Height from intrinsic layout is the height of the text before truncation so we shorten
// proportionally to our max lines setting.
return max(toPx(32, resources) ,min((result / staticLayout.lineCount) * 3, result))
} else {
// Because we're showing the author text view, we should have a height of at least 32 DP
// anyway, so there's no need to constrain to that. We constrain to a max height of 56 DP
// because that's approximately the height of the author text view + 2 lines of the body
// text view.
return min(result, toPx(56, resources))
}
}
fun getIntrinsicHeight(maxContentWidth: Int): Int {
// The way all this works is that we just calculate the total height the quote view should be
// and then center everything inside vertically. This effectively means we're applying padding.
// Applying padding the regular way results in a clipping issue though due to a bug in
// RelativeLayout.
return getIntrinsicContentHeight(maxContentWidth) + (2 * vPadding )
}
// endregion
// region Updating
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long,
@ -115,7 +74,7 @@ class QuoteView : LinearLayout {
// Author
if (thread.isGroupRecipient) {
val author = contactDb.getContactWithSessionID(authorPublicKey)
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: "${authorPublicKey.take(4)}...${authorPublicKey.takeLast(4)}"
binding.quoteViewAuthorTextView.text = authorDisplayName
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
}
@ -190,30 +149,6 @@ class QuoteView : LinearLayout {
}
}
fun calculateWidth(quote: Quote, bodyWidth: Int, maxContentWidth: Int, thread: Recipient): Int {
binding.quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
var paddingWidth = resources.getDimensionPixelSize(R.dimen.medium_spacing) * 5 // initial horizontal padding
with (binding) {
if (quoteViewAttachmentPreviewContainer.isVisible) {
paddingWidth += toPx(40, resources)
}
if (quoteViewAccentLine.isVisible) {
paddingWidth += resources.getDimensionPixelSize(R.dimen.accent_line_thickness)
}
}
val quoteBodyWidth = StaticLayout.getDesiredWidth(binding.quoteViewBodyTextView.text, binding.quoteViewBodyTextView.paint).toInt() + paddingWidth
val quoteAuthorWidth = if (thread.isGroupRecipient) {
val authorPublicKey = quote.author.serialize()
val author = contactDb.getContactWithSessionID(authorPublicKey)
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
StaticLayout.getDesiredWidth(authorDisplayName, binding.quoteViewBodyTextView.paint).toInt() + paddingWidth
} else 0
val quoteWidth = max(quoteBodyWidth, quoteAuthorWidth)
val usedWidth = max(quoteWidth, bodyWidth)
return min(maxContentWidth, usedWidth)
}
// endregion
}

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
@ -14,7 +13,7 @@ import org.thoughtcrime.securesms.util.ActivityDispatcher
import java.util.Locale
class UntrustedAttachmentView: LinearLayout {
private lateinit var binding: ViewUntrustedAttachmentBinding
private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) }
enum class AttachmentType {
AUDIO,
DOCUMENT,
@ -22,13 +21,10 @@ class UntrustedAttachmentView: LinearLayout {
}
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private fun initialize() {
binding = ViewUntrustedAttachmentBinding.inflate(LayoutInflater.from(context), this, true)
}
// endregion
// region Updating

View File

@ -5,7 +5,6 @@ import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.StaticLayout
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.style.URLSpan
@ -28,6 +27,11 @@ import androidx.core.view.isVisible
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
@ -65,7 +69,7 @@ class VisibleMessageContentView : LinearLayout {
// region Updating
fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean,
glide: GlideRequests, maxWidth: Int, thread: Recipient, searchQuery: String?, contactIsTrusted: Boolean) {
glide: GlideRequests, thread: Recipient, searchQuery: String?, contactIsTrusted: Boolean) {
// Background
val background = getBackground(message.isOutgoing, isStartOfMessageCluster, isEndOfMessageCluster)
val colorID = if (message.isOutgoing) R.attr.message_sent_background_color else R.attr.message_received_background_color
@ -83,14 +87,17 @@ class VisibleMessageContentView : LinearLayout {
onContentDoubleTap = null
if (message.isDeleted) {
binding.deletedMessageView.isVisible = true
binding.deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message))
binding.deletedMessageView.root.isVisible = true
binding.deletedMessageView.root.bind(message, VisibleMessageContentView.getTextColor(context,message))
return
} else {
binding.deletedMessageView.isVisible = false
binding.deletedMessageView.root.isVisible = false
}
// clear the
binding.bodyTextView.text = null
binding.quoteView.isVisible = message is MmsMessageRecord && message.quote != null
binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null
binding.linkPreviewView.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty()
@ -98,36 +105,55 @@ class VisibleMessageContentView : LinearLayout {
linkPreviewLayout.width = if (mediaThumbnailMessage) 0 else ViewGroup.LayoutParams.WRAP_CONTENT
binding.linkPreviewView.layoutParams = linkPreviewLayout
binding.untrustedView.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty()
binding.voiceMessageView.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
binding.documentView.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
binding.untrustedView.root.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty()
binding.voiceMessageView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
binding.documentView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
binding.albumThumbnailView.isVisible = mediaThumbnailMessage
binding.openGroupInvitationView.isVisible = message.isOpenGroupInvitation
binding.openGroupInvitationView.root.isVisible = message.isOpenGroupInvitation
var hideBody = false
if (message is MmsMessageRecord && message.quote != null) {
binding.quoteView.isVisible = true
binding.quoteView.root.isVisible = true
val quote = message.quote!!
// The max content width is the max message bubble size - 2 times the horizontal padding - 2
// times the horizontal margin. This unfortunately has to be calculated manually
// here to get the layout right.
val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - 2 * toPx(16, resources)).roundToInt()
val quoteText = if (quote.isOriginalMissing) {
context.getString(R.string.QuoteView_original_missing)
} else {
quote.text
}
binding.quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
binding.quoteView.root.bind(quote.author.toString(), quoteText, quote.attachment, thread,
message.isOutgoing, message.isOpenGroupInvitation, message.threadId,
quote.isOriginalMissing, glide)
onContentClick.add { event ->
val r = Rect()
binding.quoteView.getGlobalVisibleRect(r)
binding.quoteView.root.getGlobalVisibleRect(r)
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
delegate?.scrollToMessageIfPossible(quote.id)
}
}
val layoutParams = binding.quoteView.root.layoutParams as MarginLayoutParams
val hasMedia = message.slideDeck.asAttachments().isNotEmpty()
binding.quoteView.root.minWidth = if (hasMedia) 0 else toPx(300,context.resources)
}
if (message is MmsMessageRecord) {
message.slideDeck.asAttachments().forEach { attach ->
val dbAttachment = attach as? DatabaseAttachment ?: return@forEach
val attachmentId = dbAttachment.attachmentId.rowId
if (attach.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
&& MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) {
// start download
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, dbAttachment.mmsId))
}
}
message.linkPreviews.forEach { preview ->
val previewThumbnail = preview.getThumbnail().orNull() as? DatabaseAttachment ?: return@forEach
val attachmentId = previewThumbnail.attachmentId.rowId
if (previewThumbnail.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
&& MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) {
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, previewThumbnail.mmsId))
}
}
}
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
@ -138,26 +164,26 @@ class VisibleMessageContentView : LinearLayout {
hideBody = true
// Audio attachment
if (contactIsTrusted || message.isOutgoing) {
binding.voiceMessageView.indexInAdapter = indexInAdapter
binding.voiceMessageView.delegate = context as? ConversationActivityV2
binding.voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
binding.voiceMessageView.root.indexInAdapter = indexInAdapter
binding.voiceMessageView.root.delegate = context as? ConversationActivityV2
binding.voiceMessageView.root.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
// We have to use onContentClick (rather than a click listener directly on the voice
// message view) so as to not interfere with all the other gestures.
onContentClick.add { binding.voiceMessageView.togglePlayback() }
onContentDoubleTap = { binding.voiceMessageView.handleDoubleTap() }
onContentClick.add { binding.voiceMessageView.root.togglePlayback() }
onContentDoubleTap = { binding.voiceMessageView.root.handleDoubleTap() }
} else {
// TODO: move this out to its own area
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
}
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
hideBody = true
// Document attachment
if (contactIsTrusted || message.isOutgoing) {
binding.documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
binding.documentView.root.bind(message, VisibleMessageContentView.getTextColor(context, message))
} else {
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
}
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
/*
@ -178,34 +204,21 @@ class VisibleMessageContentView : LinearLayout {
} else {
hideBody = true
binding.albumThumbnailView.clearViews()
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
}
} else if (message.isOpenGroupInvitation) {
hideBody = true
binding.openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
onContentClick.add { binding.openGroupInvitationView.joinOpenGroup() }
binding.openGroupInvitationView.root.bind(message, VisibleMessageContentView.getTextColor(context, message))
onContentClick.add { binding.openGroupInvitationView.root.joinOpenGroup() }
}
binding.bodyTextView.isVisible = message.body.isNotEmpty() && !hideBody
// set it to use constraints if not only a text message, otherwise wrap content to whatever width it wants
val params = binding.bodyTextView.layoutParams
params.width = if (onlyBodyMessage || binding.barrierViewsGone()) ViewGroup.LayoutParams.WRAP_CONTENT else 0
params.width = if (onlyBodyMessage || binding.barrierViewsGone()) ViewGroup.LayoutParams.MATCH_PARENT else 0
binding.bodyTextView.layoutParams = params
binding.bodyTextView.maxWidth = maxWidth
val bodyWidth = with (binding.bodyTextView) {
StaticLayout.getDesiredWidth(text, paint).roundToInt()
}
val quote = (message as? MmsMessageRecord)?.quote
val quoteLayoutParams = binding.quoteView.layoutParams
quoteLayoutParams.width =
if (mediaThumbnailMessage || quote == null) 0
else binding.quoteView.calculateWidth(quote, bodyWidth, maxWidth, thread)
binding.quoteView.layoutParams = quoteLayoutParams
if (message.body.isNotEmpty() && !hideBody) {
val color = getTextColor(context, message)
@ -222,7 +235,7 @@ class VisibleMessageContentView : LinearLayout {
}
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
listOf<View>(albumThumbnailView, linkPreviewView, voiceMessageView, quoteView).none { it.isVisible }
listOf<View>(albumThumbnailView, linkPreviewView, voiceMessageView.root, quoteView.root).none { it.isVisible }
private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable {
val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster)
@ -245,20 +258,20 @@ class VisibleMessageContentView : LinearLayout {
fun recycle() {
arrayOf(
binding.deletedMessageView,
binding.untrustedView,
binding.voiceMessageView,
binding.openGroupInvitationView,
binding.documentView,
binding.quoteView,
binding.deletedMessageView.root,
binding.untrustedView.root,
binding.voiceMessageView.root,
binding.openGroupInvitationView.root,
binding.documentView.root,
binding.quoteView.root,
binding.linkPreviewView,
binding.albumThumbnailView,
binding.bodyTextView
).forEach { view -> view.isVisible = false }
).forEach { view: View -> view.isVisible = false }
}
fun playVoiceMessage() {
binding.voiceMessageView.togglePlayback()
binding.voiceMessageView.root.togglePlayback()
}
// endregion

View File

@ -8,14 +8,12 @@ import android.graphics.drawable.ColorDrawable
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf
@ -23,6 +21,7 @@ import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.contacts.Contact.ContactContext
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.utilities.ViewUtil
@ -31,7 +30,6 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
@ -43,7 +41,6 @@ import org.thoughtcrime.securesms.util.getColorWithID
import org.thoughtcrime.securesms.util.toDp
import org.thoughtcrime.securesms.util.toPx
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.min
@ -54,13 +51,12 @@ import kotlin.math.sqrt
class VisibleMessageView : LinearLayout {
@Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var contactDb: SessionContactDatabase
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
@Inject lateinit var mmsSmsDb: MmsSmsDatabase
@Inject lateinit var smsDb: SmsDatabase
@Inject lateinit var mmsDb: MmsDatabase
private lateinit var binding: ViewVisibleMessageBinding
private val binding by lazy { ViewVisibleMessageBinding.bind(this) }
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
private val swipeToReplyIconRect = Rect()
@ -75,7 +71,6 @@ class VisibleMessageView : LinearLayout {
var snIsSelected = false
set(value) {
field = value
binding.messageTimestampTextView.isVisible = isSelected
handleIsSelectedChanged()
}
var onPress: ((event: MotionEvent) -> Unit)? = null
@ -91,73 +86,84 @@ class VisibleMessageView : LinearLayout {
}
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onFinishInflate() {
super.onFinishInflate()
initialize()
}
private fun initialize() {
binding = ViewVisibleMessageBinding.inflate(LayoutInflater.from(context), this, true)
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
isHapticFeedbackEnabled = true
setWillNotDraw(false)
binding.expirationTimerViewContainer.disableClipping()
binding.messageContentContainer.disableClipping()
binding.messageContentView.disableClipping()
}
// endregion
// region Updating
fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?, glide: GlideRequests, searchQuery: String?) {
val sender = message.individualRecipient
val senderSessionID = sender.address.serialize()
fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?,
glide: GlideRequests, searchQuery: String?, contact: Contact?, senderSessionID: String,
) {
val threadID = message.threadId
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
val contact = contactDb.getContactWithSessionID(senderSessionID)
val isGroupThread = thread.isGroupRecipient
val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread)
val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread)
// Show profile picture and sender name if this is a group thread AND
// the message is incoming
binding.moderatorIconImageView.isVisible = false
binding.profilePictureView.root.visibility = when {
thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE
thread.isGroupRecipient -> View.INVISIBLE
else -> View.GONE
}
val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing)
else ViewUtil.dpToPx(context,2)
if (binding.profilePictureView.root.visibility == View.GONE) {
val expirationParams = binding.expirationTimerViewContainer.layoutParams as MarginLayoutParams
expirationParams.bottomMargin = bottomMargin
binding.expirationTimerViewContainer.layoutParams = expirationParams
} else {
val avatarLayoutParams = binding.profilePictureView.root.layoutParams as MarginLayoutParams
avatarLayoutParams.bottomMargin = bottomMargin
binding.profilePictureView.root.layoutParams = avatarLayoutParams
}
if (isGroupThread && !message.isOutgoing) {
binding.profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
binding.profilePictureView.publicKey = senderSessionID
binding.profilePictureView.glide = glide
binding.profilePictureView.update(message.individualRecipient)
binding.profilePictureView.setOnClickListener {
showUserDetails(senderSessionID, threadID)
}
if (thread.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server)
binding.moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
} else {
binding.moderatorIconImageView.visibility = View.INVISIBLE
if (isEndOfMessageCluster) {
binding.profilePictureView.root.publicKey = senderSessionID
binding.profilePictureView.root.glide = glide
binding.profilePictureView.root.update(message.individualRecipient)
binding.profilePictureView.root.setOnClickListener {
showUserDetails(senderSessionID, threadID)
}
if (thread.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
val isModerator = OpenGroupAPIV2.isUserModerator(
senderSessionID,
openGroup.room,
openGroup.server
)
binding.moderatorIconImageView.isVisible = !message.isOutgoing && isModerator
}
}
binding.senderNameTextView.isVisible = isStartOfMessageCluster
val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
val context =
if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
binding.senderNameTextView.text = contact?.displayName(context) ?: senderSessionID
} else {
binding.profilePictureContainer.visibility = View.GONE
binding.senderNameTextView.visibility = View.GONE
}
// Date break
binding.dateBreakTextView.showDateBreak(message, previous)
// Timestamp
binding.messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
// Margins
val startPadding = if (isGroupThread) {
if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.very_large_spacing) else toPx(50,resources)
} else {
if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.very_large_spacing)
else resources.getDimensionPixelSize(R.dimen.medium_spacing)
}
val endPadding = if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.medium_spacing)
else resources.getDimensionPixelSize(R.dimen.very_large_spacing)
binding.messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0)
// binding.messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
// Set inter-message spacing
setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster)
// Gravity
val gravity = if (message.isOutgoing) Gravity.END else Gravity.START
binding.mainContainer.gravity = gravity or Gravity.BOTTOM
// Message status indicator
val (iconID, iconColor) = getMessageStatusImage(message)
if (iconID != null) {
@ -169,29 +175,29 @@ class VisibleMessageView : LinearLayout {
}
if (message.isOutgoing) {
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
binding.messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
binding.messageStatusImageView.isVisible =
!message.isSent || message.id == lastMessageID
} else {
binding.messageStatusImageView.isVisible = false
}
// Expiration timer
updateExpirationTimer(message)
// Calculate max message bubble width
var maxWidth = screenWidth - startPadding - endPadding
if (binding.profilePictureContainer.visibility != View.GONE) { maxWidth -= binding.profilePictureContainer.width }
// Populate content view
binding.messageContentView.indexInAdapter = indexInAdapter
binding.messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false))
binding.messageContentView.bind(
message,
isStartOfMessageCluster,
isEndOfMessageCluster,
glide,
thread,
searchQuery,
message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false)
)
binding.messageContentView.delegate = contentViewDelegate
onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() }
}
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
val topPadding = if (isStartOfMessageCluster) R.dimen.conversation_vertical_message_spacing_default else R.dimen.conversation_vertical_message_spacing_collapse
ViewUtil.setPaddingTop(this, resources.getDimension(topPadding).roundToInt())
val bottomPadding = if (isEndOfMessageCluster) R.dimen.conversation_vertical_message_spacing_default else R.dimen.conversation_vertical_message_spacing_collapse
ViewUtil.setPaddingBottom(this, resources.getDimension(bottomPadding).roundToInt())
}
private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean {
return if (isGroupThread) {
previous == null || previous.isUpdate || !DateUtils.isSameHour(current.timestamp, previous.timestamp)
@ -223,18 +229,17 @@ class VisibleMessageView : LinearLayout {
}
private fun updateExpirationTimer(message: MessageRecord) {
val expirationTimerViewLayoutParams = binding.expirationTimerView.layoutParams as MarginLayoutParams
val container = binding.expirationTimerViewContainer
val content = binding.messageContentView
val expiration = binding.expirationTimerView
val spacing = binding.messageContentSpacing
container.removeAllViewsInLayout()
container.addView(if (message.isOutgoing) expiration else content)
container.addView(if (message.isOutgoing) content else expiration)
val expirationTimerViewSize = toPx(12, resources)
val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt()
expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0
expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize)
binding.expirationTimerView.layoutParams = expirationTimerViewLayoutParams
container.addView(spacing, if (message.isOutgoing) 0 else 2)
val containerParams = container.layoutParams as ConstraintLayout.LayoutParams
containerParams.horizontalBias = if (message.isOutgoing) 1f else 0f
container.layoutParams = containerParams
if (message.expiresIn > 0 && !message.isPending) {
binding.expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
binding.expirationTimerView.isVisible = true
@ -279,9 +284,9 @@ class VisibleMessageView : LinearLayout {
val threshold = swipeToReplyThreshold
val iconSize = toPx(24, context.resources)
val bottomVOffset = paddingBottom + binding.messageStatusImageView.height + (binding.messageContentView.height - iconSize) / 2
swipeToReplyIconRect.left = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + spacing
swipeToReplyIconRect.left = binding.messageContentView.right - binding.messageContentView.paddingEnd + spacing
swipeToReplyIconRect.top = height - bottomVOffset - iconSize
swipeToReplyIconRect.right = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + iconSize + spacing
swipeToReplyIconRect.right = binding.messageContentView.right - binding.messageContentView.paddingEnd + iconSize + spacing
swipeToReplyIconRect.bottom = height - bottomVOffset
swipeToReplyIcon.bounds = swipeToReplyIconRect
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
@ -293,7 +298,7 @@ class VisibleMessageView : LinearLayout {
}
fun recycle() {
binding.profilePictureView.recycle()
binding.profilePictureView.root.recycle()
binding.messageContentView.recycle()
}
// endregion

View File

@ -3,9 +3,7 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
@ -21,12 +19,13 @@ import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.roundToInt
import kotlin.math.roundToLong
@AndroidEntryPoint
class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener {
@Inject lateinit var attachmentDb: AttachmentDatabase
private lateinit var binding: ViewVoiceMessageBinding
private val binding: ViewVoiceMessageBinding by lazy { ViewVoiceMessageBinding.bind(this) }
private val cornerMask by lazy { CornerMask(this) }
private var isPlaying = false
set(value) {
@ -40,16 +39,17 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
var indexInAdapter = -1
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private fun initialize() {
binding = ViewVoiceMessageBinding.inflate(LayoutInflater.from(context), this, true)
override fun onFinishInflate() {
super.onFinishInflate()
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(0),
TimeUnit.MILLISECONDS.toSeconds(0))
}
// endregion
// region Updating

View File

@ -140,7 +140,7 @@ open class KThumbnailView: FrameLayout {
val dimens = dimensDelegate.resourceSize()
val request = glide.load(DecryptableUri(slide.thumbnailUri!!))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.let { request ->
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
request.override(getDefaultWidth(), getDefaultHeight())

View File

@ -20,14 +20,26 @@ object MentionUtilities {
@JvmStatic
fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String {
return highlightMentions(text, false, threadID, context).toString() // isOutgoingMessage is irrelevant
val threadDB = DatabaseComponent.get(context).threadDatabase()
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
return highlightMentions(text, false, isOpenGroup, context).toString() // isOutgoingMessage is irrelevant
}
@JvmStatic
fun highlightMentions(text:CharSequence, isOpenGroup: Boolean, context: Context): String {
return highlightMentions(text, false, isOpenGroup, context).toString()
}
@JvmStatic
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString {
@Suppress("NAME_SHADOWING") var text = text
val threadDB = DatabaseComponent.get(context).threadDatabase()
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
return highlightMentions(text, isOutgoingMessage, isOpenGroup, context) // isOutgoingMessage is irrelevant
}
@JvmStatic
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, isOpenGroup: Boolean, context: Context): SpannableString {
@Suppress("NAME_SHADOWING") var text = text
val pattern = Pattern.compile("@[0-9a-fA-F]*")
var matcher = pattern.matcher(text)
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()

View File

@ -349,7 +349,7 @@ public class ThumbnailView extends FrameLayout {
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(withCrossFade()), new CenterCrop());
if (slide.isInProgress()) return request;

View File

@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.crypto
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import org.session.libsession.utilities.Util
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import java.security.Signature
class BiometricSecretProvider {
companion object {
private const val BIOMETRIC_ASYM_KEY_ALIAS = "Session-biometric-asym"
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val SIGNATURE_ALGORITHM = "SHA512withECDSA"
}
fun getRandomData() = Util.getSecretBytes(32)
private fun createAsymmetricKey(context: Context) {
val keyGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE
)
val builder = KeyGenParameterSpec.Builder(BIOMETRIC_ASYM_KEY_ALIAS,
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
)
.setDigests(
KeyProperties.DIGEST_SHA256,
KeyProperties.DIGEST_SHA512
)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(-1)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUnlockedDeviceRequired(true)
if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
builder.setIsStrongBoxBacked(true)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setInvalidatedByBiometricEnrollment(true)
}
keyGenerator.initialize(builder.build())
keyGenerator.generateKeyPair()
}
fun getOrCreateBiometricSignature(context: Context): Signature {
val ks = KeyStore.getInstance(ANDROID_KEYSTORE)
ks.load(null)
if (!ks.containsAlias(BIOMETRIC_ASYM_KEY_ALIAS)) {
createAsymmetricKey(context)
}
val key = ks.getKey(BIOMETRIC_ASYM_KEY_ALIAS, null) as PrivateKey
val signature = Signature.getInstance(SIGNATURE_ALGORITHM)
signature.initSign(key)
return signature
}
fun verifySignature(data: ByteArray, signedData: ByteArray): Boolean {
val ks = KeyStore.getInstance(ANDROID_KEYSTORE)
ks.load(null)
val certificate = ks.getCertificate(BIOMETRIC_ASYM_KEY_ALIAS)
val signature = Signature.getInstance(SIGNATURE_ALGORITHM)
signature.initVerify(certificate)
signature.update(data)
return signature.verify(signedData)
}
}

View File

@ -52,6 +52,7 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MmsAttachmentInfo;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
@ -67,6 +68,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
@ -266,6 +268,33 @@ public class AttachmentDatabase extends Database {
return attachments;
}
void deleteAttachmentsForMessages(String[] messageIds) {
StringBuilder queryBuilder = new StringBuilder();
for (int i = 0; i < messageIds.length; i++) {
queryBuilder.append(MMS_ID+" = ").append(messageIds[i]);
if (i+1 < messageIds.length) {
queryBuilder.append(" OR ");
}
}
String idsAsString = queryBuilder.toString();
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
List<MmsAttachmentInfo> attachmentInfos = new ArrayList<>();
try {
cursor = database.query(TABLE_NAME, new String[] { DATA, THUMBNAIL, CONTENT_TYPE}, idsAsString, null, null, null, null);
while (cursor != null && cursor.moveToNext()) {
attachmentInfos.add(new MmsAttachmentInfo(cursor.getString(0), cursor.getString(1), cursor.getString(2)));
}
} finally {
if (cursor != null) {
cursor.close();
}
}
deleteAttachmentsOnDisk(attachmentInfos);
database.delete(TABLE_NAME, idsAsString, null);
notifyAttachmentListeners();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
void deleteAttachmentsForMessage(long mmsId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
@ -327,6 +356,30 @@ public class AttachmentDatabase extends Database {
notifyAttachmentListeners();
}
private void deleteAttachmentsOnDisk(List<MmsAttachmentInfo> mmsAttachmentInfos) {
for (MmsAttachmentInfo info : mmsAttachmentInfos) {
if (info.getDataFile() != null && !TextUtils.isEmpty(info.getDataFile())) {
File data = new File(info.getDataFile());
if (data.exists()) {
data.delete();
}
}
if (info.getThumbnailFile() != null && !TextUtils.isEmpty(info.getThumbnailFile())) {
File thumbnail = new File(info.getThumbnailFile());
if (thumbnail.exists()) {
thumbnail.delete();
}
}
}
boolean anyImageType = MmsAttachmentInfo.anyImages(mmsAttachmentInfos);
boolean anyThumbnail = MmsAttachmentInfo.anyThumbnailNonNull(mmsAttachmentInfos);
if (anyImageType || anyThumbnail) {
Glide.get(context).clearDiskCache();
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private void deleteAttachmentOnDisk(@Nullable String data, @Nullable String thumbnail, @Nullable String contentType) {
if (!TextUtils.isEmpty(data)) {

View File

@ -2,15 +2,13 @@ package org.thoughtcrime.securesms.database
import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.Looper
import org.session.libsession.utilities.Debouncer
import org.thoughtcrime.securesms.ApplicationContext
class ConversationNotificationDebouncer(private val context: Context) {
private val threadIDs = mutableSetOf<Long>()
private val handler = Handler(Looper.getMainLooper())
private val debouncer = Debouncer(handler, 250);
private val handler = (context.applicationContext as ApplicationContext).conversationListNotificationHandler
private val debouncer = Debouncer(handler, 1000)
companion object {
@SuppressLint("StaticFieldLeak")
@ -29,7 +27,7 @@ class ConversationNotificationDebouncer(private val context: Context) {
}
private fun publish() {
for (threadID in threadIDs) {
for (threadID in threadIDs.toList()) {
context.contentResolver.notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadID), null)
}
threadIDs.clear()

View File

@ -23,7 +23,7 @@ import android.database.Cursor;
import androidx.annotation.NonNull;
import org.session.libsession.utilities.Debouncer;
import org.session.libsession.utilities.WindowDebouncer;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@ -32,16 +32,21 @@ import java.util.Set;
public abstract class Database {
protected static final String ID_WHERE = "_id = ?";
protected static final String ID_IN = "_id IN (?)";
protected SQLCipherOpenHelper databaseHelper;
protected final Context context;
private final Debouncer conversationListNotificationDebouncer;
private final WindowDebouncer conversationListNotificationDebouncer;
private final Runnable conversationListUpdater;
@SuppressLint("WrongConstant")
public Database(Context context, SQLCipherOpenHelper databaseHelper) {
this.context = context;
this.conversationListUpdater = () -> {
context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
};
this.databaseHelper = databaseHelper;
this.conversationListNotificationDebouncer = new Debouncer(ApplicationContext.getInstance(context).getConversationListNotificationHandler(), 250);
this.conversationListNotificationDebouncer = ApplicationContext.getInstance(context).getConversationListDebouncer();
}
protected void notifyConversationListeners(Set<Long> threadIds) {
@ -54,7 +59,7 @@ public abstract class Database {
}
protected void notifyConversationListListeners() {
conversationListNotificationDebouncer.publish(()->context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null));
conversationListNotificationDebouncer.publish(conversationListUpdater);
}
protected void notifyStickerListeners() {

View File

@ -4,6 +4,7 @@ import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -41,7 +42,7 @@ public class DatabaseContentProviders {
@Override
public boolean onCreate() {
return false;
return true;
}
@Nullable

View File

@ -36,11 +36,12 @@ fun <T> SQLiteDatabase.getAll(table: String, query: String?, arguments: Array<St
return listOf()
}
fun SQLiteDatabase.insertOrUpdate(table: String, values: ContentValues, query: String, arguments: Array<String>) {
fun SQLiteDatabase.insertOrUpdate(table: String, values: ContentValues, query: String, arguments: Array<String>): Int {
val id = insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE).toInt()
if (id == -1) {
update(table, values, query, arguments)
return update(table, values, query, arguments)
}
return id
}
fun Cursor.getInt(columnName: String): Int {

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());
});
notifyConversationListeners(threadId);
notifyConversationListListeners();
return threadId;
}
@ -314,6 +315,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
new String[] {groupID});
Recipient.applyCached(Address.fromSerialized(groupID), recipient -> recipient.setGroupAvatarId(avatarId == 0 ? null : avatarId));
notifyConversationListListeners();
}
public void updateMembers(String groupId, List<Address> members) {

View File

@ -4,13 +4,13 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import java.util.LinkedList;
import java.util.List;
@ -92,6 +92,19 @@ public class GroupReceiptDatabase extends Database {
return results;
}
void deleteRowsForMessages(String[] mmsIds) {
StringBuilder queryBuilder = new StringBuilder();
for (int i = 0; i < mmsIds.length; i++) {
queryBuilder.append(MMS_ID+" = ").append(mmsIds[i]);
if (i+1 < mmsIds.length) {
queryBuilder.append(" OR ");
}
}
String idsAsString = queryBuilder.toString();
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, idsAsString, null);
}
void deleteRowsForMessage(long mmsId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});

View File

@ -265,7 +265,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
override fun getLastMessageHashValue(snode: Snode, publicKey: String, namespace: Int): String? {
val database = databaseHelper.readableDatabase
val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ? AND $lastMessageHashNamespace = ?"
return database.get(lastMessageHashValueTable2, query, arrayOf( snode.toString(), publicKey, namespace.toString() )) { cursor ->
return database.get(lastMessageHashValueTable2, query, arrayOf(snode.toString(), publicKey, namespace.toString())) { cursor ->
cursor.getString(cursor.getColumnIndexOrThrow(lastMessageHashValue))
}
}
@ -279,7 +279,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
lastMessageHashNamespace to namespace.toString()
))
val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ? AND $lastMessageHashNamespace = ?"
database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey, namespace.toString() ))
val lastHash = database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey, namespace.toString() ))
}
override fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set<String>? {

View File

@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.session.libsignal.database.LokiMessageDatabaseProtocol
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
@ -77,6 +77,9 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
database.endTransaction()
}
/**
* @return pair of sms or mms table-specific ID and whether it is in SMS table
*/
fun getMessageID(serverID: Long, threadID: Long): Pair<Long, Boolean>? {
val database = databaseHelper.readableDatabase
val mappingResult = database.get(messageThreadMappingTable, "${Companion.serverID} = ? AND ${Companion.threadID} = ?",

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import android.content.ContentValues
import android.content.Context
import net.sqlcipher.Cursor
import org.session.libsession.messaging.jobs.AttachmentUploadJob
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.MessageReceiveJob
@ -135,6 +136,13 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
job.failureCount = cursor.getInt(failureCount)
return job
}
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean {
val database = databaseHelper.readableDatabase
return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(BackgroundGroupAddJob.KEY)) { cursor ->
jobFromCursor(cursor) as? BackgroundGroupAddJob
}.filterNotNull().any { it.joinUrl == groupJoinUrl }
}
}
object SessionJobHelper {

View File

@ -362,7 +362,7 @@ public class SmsDatabase extends MessagingDatabase {
return new Pair<>(messageId, threadId);
}
protected Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp) {
protected Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
if (message.isSecureMessage()) {
type |= Types.SECURE_MESSAGE_BIT;
} else if (message.isGroup()) {
@ -440,11 +440,13 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, null, values);
if (unread) {
if (unread && runIncrement) {
DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1);
}
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
if (runThreadUpdate) {
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
}
if (message.getSubscriptionId() != -1) {
DatabaseComponent.get(context).recipientDatabase().setDefaultSubscriptionId(recipient, message.getSubscriptionId());
@ -456,23 +458,23 @@ public class SmsDatabase extends MessagingDatabase {
}
}
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message) {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0);
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, boolean runIncrement, boolean runThreadUpdate) {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runIncrement, runThreadUpdate);
}
public Optional<InsertResult> insertCallMessage(IncomingTextMessage message) {
return insertMessageInbox(message, 0, 0);
return insertMessageInbox(message, 0, 0, true, true);
}
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp) {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp);
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runIncrement, runThreadUpdate);
}
public Optional<InsertResult> insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp) {
public Optional<InsertResult> insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp, boolean runThreadUpdate) {
if (threadId == -1) {
threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(message.getRecipient());
}
long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, null);
long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, null, runThreadUpdate);
if (messageId == -1) {
return Optional.absent();
}
@ -481,7 +483,8 @@ public class SmsDatabase extends MessagingDatabase {
}
public long insertMessageOutbox(long threadId, OutgoingTextMessage message,
boolean forceSms, long date, InsertListener insertListener)
boolean forceSms, long date, InsertListener insertListener,
boolean runThreadUpdate)
{
long type = Types.BASE_SENDING_TYPE;
@ -517,7 +520,9 @@ public class SmsDatabase extends MessagingDatabase {
insertListener.onComplete();
}
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
if (runThreadUpdate) {
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
}
DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId);
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true);

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.MessageReceiveJob
import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.jobs.TrimThreadJob
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage
@ -102,7 +101,29 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return database.getAttachmentsForMessage(messageID)
}
override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? {
override fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean) {
val threadDb = DatabaseComponent.get(context).threadDatabase()
threadDb.setRead(threadId, updateLastSeen)
}
override fun incrementUnread(threadId: Long, amount: Int) {
val threadDb = DatabaseComponent.get(context).threadDatabase()
threadDb.incrementUnread(threadId, amount)
}
override fun updateThread(threadId: Long, unarchive: Boolean) {
val threadDb = DatabaseComponent.get(context).threadDatabase()
threadDb.update(threadId, unarchive)
}
override fun persist(message: VisibleMessage,
quotes: QuoteModel?,
linkPreview: List<LinkPreview?>,
groupPublicKey: String?,
openGroupID: String?,
attachments: List<Attachment>,
runIncrement: Boolean,
runThreadUpdate: Boolean): Long? {
var messageID: Long? = null
val senderAddress = Address.fromSerialized(message.sender!!)
val isUserSender = (message.sender!! == getUserPublicKey())
@ -139,14 +160,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
val insertResult = if (message.sender == getUserPublicKey()) {
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!, runThreadUpdate)
} else {
// It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment
val signalServiceAttachments = attachments.mapNotNull {
it.toSignalPointer()
}
val mediaMessage = IncomingMediaMessage.from(message, senderAddress, targetRecipient.expireMessages * 1000L, group, signalServiceAttachments, quote, linkPreviews)
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.receivedTimestamp ?: 0)
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate)
}
if (insertResult.isPresent) {
messageID = insertResult.get().messageId
@ -158,12 +179,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val insertResult = if (message.sender == getUserPublicKey()) {
val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, targetRecipient, message.sentTimestamp)
else OutgoingTextMessage.from(message, targetRecipient)
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!)
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!, runThreadUpdate)
} else {
val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, senderAddress, message.sentTimestamp)
else IncomingTextMessage.from(message, senderAddress, group, targetRecipient.expireMessages * 1000L)
val encrypted = IncomingEncryptedMessage(textMessage, textMessage.messageBody)
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0)
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate)
}
insertResult.orNull()?.let { result ->
messageID = result.messageId
@ -171,8 +192,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
val threadID = message.threadID
// open group trim thread job is scheduled after processing in OpenGroupPollerV2
if (openGroupID.isNullOrEmpty() && threadID != null && threadID >= 0) {
JobQueue.shared.add(TrimThreadJob(threadID))
if (openGroupID.isNullOrEmpty() && threadID != null && threadID >= 0 && TextSecurePreferences.isThreadLengthTrimmingEnabled(context)) {
JobQueue.shared.queueThreadForTrim(threadID)
}
message.serverHash?.let { serverHash ->
messageID?.let { id ->
@ -436,7 +457,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
val smsDB = DatabaseComponent.get(context).smsDatabase()
smsDB.insertMessageInbox(infoMessage)
smsDB.insertMessageInbox(infoMessage, true, true)
}
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) {
@ -448,7 +469,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val mmsDB = DatabaseComponent.get(context).mmsDatabase()
val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase()
if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true)
mmsDB.markAsSent(infoMessageID, true)
}
@ -519,6 +540,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
OpenGroupManager.addOpenGroup(urlAsString, context)
}
override fun onOpenGroupAdded(urlAsString: String) {
val server = OpenGroupV2.getServer(urlAsString)
OpenGroupManager.restartPollerForServer(server.toString().removeSuffix("/"))
}
override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean {
val jobDb = DatabaseComponent.get(context).sessionJobDatabase()
return jobDb.hasBackgroundGroupAddJob(groupJoinUrl)
}
override fun setProfileSharing(address: Address, value: Boolean) {
val recipient = Recipient.from(context, address, false)
DatabaseComponent.get(context).recipientDatabase().setProfileSharing(recipient, value)
@ -667,7 +698,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
Optional.of(message)
)
database.insertSecureDecryptedMessageInbox(mediaMessage, -1)
database.insertSecureDecryptedMessageInbox(mediaMessage, -1, runIncrement = true, runThreadUpdate = true)
}
override fun insertMessageRequestResponse(response: MessageRequestResponse) {
@ -705,7 +736,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
Optional.absent()
)
val threadId = getOrCreateThreadIdFor(senderAddress)
mmsDb.insertSecureDecryptedMessageInbox(message, threadId)
mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runIncrement = true, runThreadUpdate = true)
}
}

View File

@ -574,6 +574,17 @@ public class ThreadDatabase extends Database {
return getOrCreateThreadIdFor(recipient, DistributionTypes.DEFAULT);
}
public void setThreadArchived(long threadId) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(ARCHIVED, 1);
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE,
new String[] {String.valueOf(threadId)});
notifyConversationListListeners();
notifyConversationListeners(threadId);
}
public long getOrCreateThreadIdFor(Recipient recipient, int distributionType) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String where = ADDRESS + " = ?";

View File

@ -10,6 +10,7 @@ import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteDatabaseHook;
import net.sqlcipher.database.SQLiteOpenHelper;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
@ -86,6 +87,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
public void postKey(SQLiteDatabase db) {
db.rawExecSQL("PRAGMA kdf_iter = '1';");
db.rawExecSQL("PRAGMA cipher_page_size = 4096;");
// if not vacuumed in a while, perform that operation
long currentTime = System.currentTimeMillis();
// 7 days
if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) {
db.rawExecSQL("VACUUM;");
TextSecurePreferences.setLastVacuumNow(context);
}
}
});
@ -144,7 +152,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(GroupDatabase.getCreateUpdatedTimestampCommand());
db.execSQL(RecipientDatabase.getCreateApprovedCommand());
db.execSQL(RecipientDatabase.getCreateApprovedMeCommand());
db.execSQL(MmsDatabase.getCreateMessageRequestResponseCommand());
db.execSQL(MmsDatabase.createMessageRequestResponseCommand);
db.execSQL(LokiAPIDatabase.CREATE_FORK_INFO_TABLE_COMMAND);
db.execSQL(LokiAPIDatabase.CREATE_DEFAULT_FORK_INFO_COMMAND);
db.execSQL(LokiAPIDatabase.UPDATE_HASHES_INCLUDE_NAMESPACE_COMMAND);
@ -339,7 +347,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(RecipientDatabase.getCreateApprovedCommand());
db.execSQL(RecipientDatabase.getCreateApprovedMeCommand());
db.execSQL(RecipientDatabase.getUpdateApprovedCommand());
db.execSQL(MmsDatabase.getCreateMessageRequestResponseCommand());
db.execSQL(MmsDatabase.createMessageRequestResponseCommand);
}
if (oldVersion < lokiV32) {

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());
}
}
cursor.close();
if (cursor != null) {
cursor.close();
}
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.Nullable;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
@ -74,7 +74,6 @@ public class ThreadRecord extends DisplayRecord {
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
Recipient recipient = getRecipient();
if (isGroupUpdateMessage()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
} else if (isOpenGroupInvitation()) {

View File

@ -4,18 +4,15 @@ package org.thoughtcrime.securesms.giph.ui;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import org.session.libsignal.utilities.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
@ -26,19 +23,20 @@ import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.util.ByteBufferUtil;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.session.libsession.utilities.MaterialColor;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.List;
import java.util.concurrent.ExecutionException;
import network.loki.messenger.R;
class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
@ -154,12 +152,12 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
RequestBuilder<Drawable> thumbnailRequest = GlideApp.with(context)
.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
.diskCacheStrategy(DiskCacheStrategy.ALL);
.diskCacheStrategy(DiskCacheStrategy.NONE);
if (org.thoughtcrime.securesms.util.Util.isLowMemory(context)) {
glideRequests.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(DrawableTransitionOptions.withCrossFade())
.listener(holder)
.into(holder.thumbnail);
@ -169,7 +167,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
glideRequests.load(new ChunkedImageUrl(image.getGifUrl(), image.getGifSize()))
.thumbnail(thumbnailRequest)
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(DrawableTransitionOptions.withCrossFade())
.listener(holder)
.into(holder.thumbnail);

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) {
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupID), false);
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupID), true);
return DatabaseComponent.get(context).threadDatabase().getThreadIdIfExistsFor(groupRecipient);
}
@ -59,6 +59,7 @@ public class GroupManager {
long threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(
groupRecipient, DistributionTypes.CONVERSATION);
DatabaseComponent.get(context).threadDatabase().setThreadArchived(threadID);
return new GroupActionResult(groupRecipient, threadID);
}

View File

@ -25,6 +25,7 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
@ -101,6 +102,7 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
val sanitizedServer = server.toString().removeSuffix("/")
val openGroupID = "$sanitizedServer.${room!!}"
OpenGroupManager.add(sanitizedServer, room, publicKey!!, this@JoinPublicChatActivity)
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(stringWithExplicitScheme)
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, this@JoinPublicChatActivity)
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
threadID to groupID

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.OpenGroupV2
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
import org.session.libsession.utilities.Util
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.BitmapUtil
import java.util.concurrent.Executors
object OpenGroupManager {
private val executorService = Executors.newScheduledThreadPool(4)
private var pollers = mutableMapOf<String, OpenGroupPollerV2>() // One for each server
private var isPolling = false
private val pollUpdaterLock = Any()
val isAllCaughtUp: Boolean
get() {
@ -49,8 +48,11 @@ object OpenGroupManager {
}
fun stopPolling() {
pollers.forEach { it.value.stop() }
pollers.clear()
synchronized(pollUpdaterLock) {
pollers.forEach { it.value.stop() }
pollers.clear()
isPolling = false
}
}
@WorkerThread
@ -67,7 +69,7 @@ object OpenGroupManager {
storage.removeLastMessageServerID(room, server)
// Store the public key
storage.setOpenGroupPublicKey(server,publicKey)
// Get an auth token
// Get group info
OpenGroupAPIV2.getAuthToken(room, server).get()
// Get group info
val info = OpenGroupAPIV2.getInfo(room, server).get()
@ -77,11 +79,17 @@ object OpenGroupManager {
}
val openGroup = OpenGroupV2(server, room, info.name, publicKey)
threadDB.setOpenGroupChat(openGroup, threadID)
}
fun restartPollerForServer(server: String) {
// Start the poller if needed
pollers[server]?.startIfNeeded() ?: run {
val poller = OpenGroupPollerV2(server, executorService)
Util.runOnMain { poller.startIfNeeded() }
pollers[server] = poller
synchronized(pollUpdaterLock) {
pollers[server]?.stop()
pollers[server]?.startIfNeeded() ?: run {
val poller = OpenGroupPollerV2(server, executorService)
pollers[server] = poller
poller.startIfNeeded()
}
}
}
@ -91,13 +99,16 @@ object OpenGroupManager {
val openGroupID = "$server.$room"
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
val recipient = threadDB.getRecipientForThreadId(threadID) ?: return
threadDB.setThreadArchived(threadID)
val groupID = recipient.address.serialize()
// Stop the poller if needed
val openGroups = storage.getAllV2OpenGroups().filter { it.value.server == server }
if (openGroups.count() == 1) {
val poller = pollers[server]
poller?.stop()
pollers.remove(server)
synchronized(pollUpdaterLock) {
val poller = pollers[server]
poller?.stop()
pollers.remove(server)
}
}
// Delete
storage.removeLastDeletionServerID(room, server)
@ -112,12 +123,7 @@ object OpenGroupManager {
fun addOpenGroup(urlAsString: String, context: Context) {
val url = HttpUrl.parse(urlAsString) ?: return
val builder = HttpUrl.Builder().scheme(url.scheme()).host(url.host())
if (url.port() != 80 || url.port() != 443) {
// Non-standard port; add to server
builder.port(url.port())
}
val server = builder.build()
val server = OpenGroupV2.getServer(urlAsString)
val room = url.pathSegments().firstOrNull() ?: return
val publicKey = url.queryParameter("public_key") ?: return
add(server.toString().removeSuffix("/"), room, publicKey, context)

View File

@ -15,7 +15,8 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ViewConversationBinding
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_ALL
import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_NONE
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
@ -47,7 +48,7 @@ class ConversationView : LinearLayout {
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
}
binding.profilePictureView.glide = glide
binding.profilePictureView.root.glide = glide
val unreadCount = thread.unreadCount
if (thread.recipient.isBlocked) {
binding.accentView.setBackgroundResource(R.color.destructive)
@ -73,15 +74,15 @@ class ConversationView : LinearLayout {
binding.conversationViewDisplayNameTextView.text = senderDisplayName
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
val recipient = thread.recipient
binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL
val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != NOTIFY_TYPE_ALL
val drawableRes = if (recipient.isMuted || recipient.notifyType == NOTIFY_TYPE_NONE) {
R.drawable.ic_outline_notifications_off_24
} else {
R.drawable.ic_notifications_mentions
}
binding.muteIndicatorImageView.setImageResource(drawableRes)
val rawSnippet = thread.getDisplayBody(context)
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
val snippet = highlightMentions(rawSnippet, recipient.isOpenGroupRecipient, context)
binding.snippetTextView.text = snippet
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
@ -103,13 +104,11 @@ class ConversationView : LinearLayout {
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
}
post {
binding.profilePictureView.update(thread.recipient)
}
binding.profilePictureView.root.update(thread.recipient)
}
fun recycle() {
binding.profilePictureView.recycle()
binding.profilePictureView.root.recycle()
}
private fun getUserDisplayName(recipient: Recipient): String? {

View File

@ -5,18 +5,15 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.database.Cursor
import android.os.Bundle
import android.text.SpannableString
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
@ -73,7 +70,6 @@ import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import java.io.IOException
import java.util.Locale
import javax.inject.Inject
@ -83,7 +79,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
ConversationClickListener,
SeedReminderViewDelegate,
NewConversationButtonSetViewDelegate,
LoaderManager.LoaderCallbacks<Cursor>,
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
private lateinit var binding: ActivityHomeBinding
@ -97,12 +92,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
@Inject lateinit var textSecurePreferences: TextSecurePreferences
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
private val homeViewModel by viewModels<HomeViewModel>()
private val publicKey: String
get() = textSecurePreferences.getLocalNumber()!!
private val homeAdapter: HomeAdapter by lazy {
HomeAdapter(context = this, cursor = threadDb.approvedConversationList, listener = this)
private val homeAdapter: NewHomeAdapter by lazy {
NewHomeAdapter(context = this, listener = this)
}
private val globalSearchAdapter = GlobalSearchAdapter { model ->
@ -156,8 +152,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Set up Glide
glide = GlideApp.with(this)
// Set up toolbar buttons
binding.profileButton.glide = glide
binding.profileButton.setOnClickListener { openSettings() }
binding.profileButton.root.glide = glide
binding.profileButton.root.setOnClickListener { openSettings() }
binding.searchViewContainer.setOnClickListener {
binding.globalSearchInputLayout.requestFocus()
}
@ -184,8 +180,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Set up empty state view
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
IP2Country.configureIfNeeded(this@HomeActivity)
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
LoaderManager.getInstance(this).restartLoader(0, null, this)
homeViewModel.getObservable(this).observe(this) { newData ->
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
val offsetTop = if(firstPos >= 0) {
manager.findViewByPosition(firstPos)?.let { view ->
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
} ?: 0
} else 0
homeAdapter.data = newData
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
setupMessageRequestsBanner()
updateEmptyState()
}
homeViewModel.tryUpdateChannel()
// Set up new conversation button set
binding.newConversationButtonSet.delegate = this
// Observe blocked contacts changed events
@ -202,17 +210,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Double check that the long poller is up
(applicationContext as ApplicationContext).startPollingIfNeeded()
// update things based on TextSecurePrefs (profile info etc)
// Set up typing observer
withContext(Dispatchers.Main) {
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this@HomeActivity, Observer<Set<Long>> { threadIDs ->
val adapter = binding.recyclerView.adapter as HomeAdapter
adapter.typingThreadIDs = threadIDs ?: setOf()
})
updateProfileButton()
TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect {
updateProfileButton()
}
}
// Set up remaining components if needed
val application = ApplicationContext.getInstance(this@HomeActivity)
application.registerForFCMIfNeeded(false)
@ -220,6 +217,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
OpenGroupManager.startPolling()
JobQueue.shared.resumePendingJobs()
}
// Set up typing observer
withContext(Dispatchers.Main) {
updateProfileButton()
TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect {
updateProfileButton()
}
}
}
// monitor the global search VM query
launch {
@ -293,7 +297,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.searchToolbar.isVisible = isShown
binding.sessionToolbar.isVisible = !isShown
binding.recyclerView.isVisible = !isShown
binding.emptyStateContainer.isVisible = (binding.recyclerView.adapter as HomeAdapter).itemCount == 0 && binding.recyclerView.isVisible
binding.emptyStateContainer.isVisible = (binding.recyclerView.adapter as NewHomeAdapter).itemCount == 0 && binding.recyclerView.isVisible
binding.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown
binding.gradientView.isVisible = !isShown
binding.globalSearchRecycler.isVisible = isShown
@ -314,35 +318,27 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
root.setOnClickListener { showMessageRequests() }
root.setOnLongClickListener { hideMessageRequests(); true }
root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
homeAdapter.headerView = root
homeAdapter.notifyItemChanged(0)
val hadHeader = homeAdapter.hasHeaderView()
homeAdapter.header = root
if (hadHeader) homeAdapter.notifyItemChanged(0)
else homeAdapter.notifyItemInserted(0)
}
} else {
homeAdapter.headerView = null
val hadHeader = homeAdapter.hasHeaderView()
homeAdapter.header = null
if (hadHeader) {
homeAdapter.notifyItemRemoved(0)
}
}
}
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
return HomeLoader(this@HomeActivity)
}
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
homeAdapter.changeCursor(cursor)
setupMessageRequestsBanner()
updateEmptyState()
}
override fun onLoaderReset(cursor: Loader<Cursor>) {
homeAdapter.changeCursor(null)
}
override fun onResume() {
super.onResume()
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared
IdentityKeyUtil.checkUpdate(this)
binding.profileButton.recycle() // clear cached image before update tje profilePictureView
binding.profileButton.update()
binding.profileButton.root.recycle() // clear cached image before update tje profilePictureView
binding.profileButton.root.update()
if (textSecurePreferences.getHasViewedSeed()) {
binding.seedReminderView.isVisible = false
}
@ -377,7 +373,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// region Updating
private fun updateEmptyState() {
val threadCount = (binding.recyclerView.adapter as HomeAdapter).itemCount
val threadCount = (binding.recyclerView.adapter)!!.itemCount
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
}
@ -385,14 +381,16 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
fun onUpdateProfileEvent(event: ProfilePictureModifiedEvent) {
if (event.recipient.isLocalNumber) {
updateProfileButton()
} else {
homeViewModel.tryUpdateChannel()
}
}
private fun updateProfileButton() {
binding.profileButton.publicKey = publicKey
binding.profileButton.displayName = textSecurePreferences.getProfileName()
binding.profileButton.recycle()
binding.profileButton.update()
binding.profileButton.root.publicKey = publicKey
binding.profileButton.root.displayName = textSecurePreferences.getProfileName()
binding.profileButton.root.recycle()
binding.profileButton.root.update()
}
// endregion
@ -534,9 +532,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
lifecycleScope.launch(Dispatchers.IO) {
threadDb.setPinned(threadId, pinned)
withContext(Dispatchers.Main) {
LoaderManager.getInstance(this@HomeActivity).restartLoader(0, null, this@HomeActivity)
}
homeViewModel.tryUpdateChannel()
}
}
@ -608,11 +604,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
show(intent, isForResult = true)
}
private fun showPath() {
val intent = Intent(this, PathActivity::class.java)
show(intent)
}
private fun showMessageRequests() {
val intent = Intent(this, MessageRequestsActivity::class.java)
push(intent)
@ -624,7 +615,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
.setPositiveButton(R.string.yes) { _, _ ->
textSecurePreferences.setHasHiddenMessageRequests()
setupMessageRequestsBanner()
LoaderManager.getInstance(this).restartLoader(0, null, this)
homeViewModel.tryUpdateChannel()
}
.setNegativeButton(R.string.no) { _, _ ->
// Do nothing

View File

@ -1,51 +1,6 @@
package org.thoughtcrime.securesms.home
import android.content.Context
import android.database.Cursor
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests
class HomeAdapter(
context: Context,
cursor: Cursor?,
val listener: ConversationClickListener
) : CursorRecyclerViewAdapter<HomeAdapter.ViewHolder>(context, cursor) {
private val threadDatabase = DatabaseComponent.get(context).threadDatabase()
lateinit var glide: GlideRequests
var typingThreadIDs = setOf<Long>()
set(value) { field = value; notifyDataSetChanged() }
class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = ConversationView(context)
view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } }
view.setOnLongClickListener {
view.thread?.let { listener.onLongConversationClick(it) }
true
}
return ViewHolder(view)
}
override fun onBindItemViewHolder(viewHolder: ViewHolder, cursor: Cursor) {
val thread = getThread(cursor)!!
val isTyping = typingThreadIDs.contains(thread.threadId)
viewHolder.view.bind(thread, isTyping, glide)
}
override fun onItemViewRecycled(holder: ViewHolder?) {
super.onItemViewRecycled(holder)
holder?.view?.recycle()
}
private fun getThread(cursor: Cursor): ThreadRecord? {
return threadDatabase.readerFor(cursor).current
}
}
interface ConversationClickListener {
fun onConversationClick(thread: ThreadRecord)

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.util.AbstractCursorLoader
class HomeLoader(context: Context) : AbstractCursorLoader(context) {
class HomeLoader(context: Context, val onNewCursor: (Cursor?) -> Unit) : AbstractCursorLoader(context) {
override fun getCursor(): Cursor {
return DatabaseComponent.get(context).threadDatabase().approvedConversationList
}
override fun deliverResult(newCursor: Cursor?) {
super.deliverResult(newCursor)
onNewCursor(newCursor)
}
}

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"
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentUserDetailsBottomSheetBinding.inflate(inflater, container, false)
return binding.root
}
@ -51,10 +51,10 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
with(binding) {
profilePictureView.publicKey = publicKey
profilePictureView.glide = GlideApp.with(this@UserDetailsBottomSheet)
profilePictureView.isLarge = true
profilePictureView.update(recipient)
profilePictureView.root.publicKey = publicKey
profilePictureView.root.glide = GlideApp.with(this@UserDetailsBottomSheet)
profilePictureView.root.isLarge = true
profilePictureView.root.update(recipient)
nameTextViewContainer.visibility = View.VISIBLE
nameTextViewContainer.setOnClickListener {
nameTextViewContainer.visibility = View.INVISIBLE

View File

@ -11,7 +11,6 @@ import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.search.model.MessageResult
import java.security.InvalidParameterException
@ -84,14 +83,14 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is ContentView) {
holder.binding.searchResultProfilePicture.recycle()
holder.binding.searchResultProfilePicture.root.recycle()
}
}
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
val binding = ViewGlobalSearchResultBinding.bind(view).apply {
searchResultProfilePicture.glide = GlideApp.with(root)
searchResultProfilePicture.root.glide = GlideApp.with(root)
}
fun bindPayload(newQuery: String, model: Model) {
@ -99,7 +98,7 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
}
fun bind(query: String, model: Model) {
binding.searchResultProfilePicture.recycle()
binding.searchResultProfilePicture.root.recycle()
when (model) {
is Model.GroupConversation -> bindModel(query, model)
is Model.Contact -> bindModel(query, model)

View File

@ -3,9 +3,7 @@ package org.thoughtcrime.securesms.home.search
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.TypedValue
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import network.loki.messenger.R
@ -86,12 +84,12 @@ private fun getHighlight(query: String?, toSearch: String): Spannable? {
}
fun ContentView.bindModel(query: String?, model: GroupConversation) {
binding.searchResultProfilePicture.isVisible = true
binding.searchResultProfilePicture.root.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
binding.searchResultTimestamp.isVisible = false
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false)
binding.searchResultProfilePicture.update(threadRecipient)
binding.searchResultProfilePicture.root.update(threadRecipient)
val nameString = model.groupRecord.title
binding.searchResultTitle.text = getHighlight(query, nameString)
@ -107,14 +105,14 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) {
}
fun ContentView.bindModel(query: String?, model: ContactModel) {
binding.searchResultProfilePicture.isVisible = true
binding.searchResultProfilePicture.root.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultSubtitle.isVisible = false
binding.searchResultTimestamp.isVisible = false
binding.searchResultSubtitle.text = null
val recipient =
Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false)
binding.searchResultProfilePicture.update(recipient)
binding.searchResultProfilePicture.root.update(recipient)
val nameString = model.contact.getSearchName()
binding.searchResultTitle.text = getHighlight(query, nameString)
}
@ -123,12 +121,12 @@ fun ContentView.bindModel(model: SavedMessages) {
binding.searchResultSubtitle.isVisible = false
binding.searchResultTimestamp.isVisible = false
binding.searchResultTitle.setText(R.string.note_to_self)
binding.searchResultProfilePicture.isVisible = false
binding.searchResultProfilePicture.root.isVisible = false
binding.searchResultSavedMessages.isVisible = true
}
fun ContentView.bindModel(query: String?, model: Message) {
binding.searchResultProfilePicture.isVisible = true
binding.searchResultProfilePicture.root.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultTimestamp.isVisible = true
// val hasUnreads = model.unread > 0
@ -137,7 +135,7 @@ fun ContentView.bindModel(query: String?, model: Message) {
// binding.unreadCountTextView.text = model.unread.toString()
// }
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.receivedTimestampMs)
binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient)
binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient)
val textSpannable = SpannableStringBuilder()
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
// group chat, bind

View File

@ -48,7 +48,8 @@ public class RetrieveProfileAvatarJob extends BaseJob {
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.HOURS.toMillis(1))
.setMaxAttempts(10)
.setMaxAttempts(2)
.setMaxInstances(1)
.build(),
recipient,
profileAvatar);

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.messagerequests
import android.content.Context
import android.content.res.Resources
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
@ -35,7 +34,7 @@ class MessageRequestView : LinearLayout {
// region Updating
fun bind(thread: ThreadRecord, glide: GlideRequests) {
this.thread = thread
binding.profilePictureView.glide = glide
binding.profilePictureView.root.glide = glide
val senderDisplayName = getUserDisplayName(thread.recipient)
?: thread.recipient.address.toString()
binding.displayNameTextView.text = senderDisplayName
@ -45,12 +44,12 @@ class MessageRequestView : LinearLayout {
binding.snippetTextView.text = snippet
post {
binding.profilePictureView.update(thread.recipient)
binding.profilePictureView.root.update(thread.recipient)
}
}
fun recycle() {
binding.profilePictureView.recycle()
binding.profilePictureView.root.recycle()
}
private fun getUserDisplayName(recipient: Recipient): String? {

View File

@ -2,9 +2,11 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;
import androidx.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.Registry;
@ -21,12 +23,14 @@ import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
import com.bumptech.glide.module.AppGlideModule;
import org.session.libsession.avatars.ContactPhoto;
import org.session.libsession.avatars.PlaceholderAvatarPhoto;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader;
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
import org.thoughtcrime.securesms.glide.OkHttpUrlLoader;
import org.thoughtcrime.securesms.glide.PlaceholderAvatarLoader;
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
@ -69,6 +73,7 @@ public class SignalGlideModule extends AppGlideModule {
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory(context));
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
}

View File

@ -89,14 +89,14 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver {
Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message");
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
try {
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, replyThreadId, false, null);
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, replyThreadId, false, null, true);
} catch (MmsException e) {
Log.w(TAG, e);
}
} else {
Log.w("AndroidAutoReplyReceiver", "Sending regular message ");
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, System.currentTimeMillis(), null);
DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, System.currentTimeMillis(), null, true);
}
List<MarkedMessageInfo> messageIds = DatabaseComponent.get(context).threadDatabase().setRead(replyThreadId, true);

View File

@ -229,7 +229,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
ThreadDatabase threads = DatabaseComponent.get(context).threadDatabase();
Recipient recipient = threads.getRecipientForThreadId(threadId);
if (!recipient.isGroupRecipient() && threads.getMessageCount(threadId) == 1 &&
if (recipient != null && !recipient.isGroupRecipient() && threads.getMessageCount(threadId) == 1 &&
!(recipient.isApproved() || threads.getLastSeenAndHasSent(threadId).second())) {
TextSecurePreferences.removeHasHiddenMessageRequests(context);
}
@ -278,10 +278,10 @@ public class DefaultMessageNotifier implements MessageNotifier {
try {
if (notificationState.hasMultipleThreads()) {
sendMultipleThreadNotification(context, notificationState, signal);
for (long threadId : notificationState.getThreads()) {
sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true);
}
sendMultipleThreadNotification(context, notificationState, signal);
} else if (notificationState.getMessageCount() > 0){
sendSingleThreadNotification(context, notificationState, signal, false);
} else {

View File

@ -83,7 +83,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
case GroupMessage: {
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
try {
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, threadId, false, null);
DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, threadId, false, null, true);
MessageSender.send(message, address);
} catch (MmsException e) {
Log.w(TAG, e);
@ -92,7 +92,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
}
case SecureMessage: {
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), null);
DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), null, true);
MessageSender.send(message, address);
break;
}

View File

@ -92,7 +92,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
Bitmap iconBitmap = GlideApp.with(context.getApplicationContext())
.asBitmap()
.load(contactPhoto)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))

View File

@ -20,7 +20,7 @@ class LandingActivity : BaseActionBarActivity() {
with(binding) {
fakeChatView.startAnimating()
registerButton.setOnClickListener { register() }
restoreButton.setOnClickListener { restore() }
restoreButton.setOnClickListener { link() }
linkButton.setOnClickListener { link() }
}
IdentityKeyUtil.generateIdentityKeyPair(this)
@ -34,11 +34,6 @@ class LandingActivity : BaseActionBarActivity() {
push(intent)
}
private fun restore() {
val intent = Intent(this, RecoveryPhraseRestoreActivity::class.java)
push(intent)
}
private fun link() {
val intent = Intent(this, LinkDeviceActivity::class.java)
push(intent)

View File

@ -77,12 +77,12 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey
glide = GlideApp.with(this)
with(binding) {
profilePictureView.glide = glide
profilePictureView.publicKey = hexEncodedPublicKey
profilePictureView.displayName = displayName
profilePictureView.isLarge = true
profilePictureView.update()
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
profilePictureView.root.glide = glide
profilePictureView.root.publicKey = hexEncodedPublicKey
profilePictureView.root.displayName = displayName
profilePictureView.root.isLarge = true
profilePictureView.root.update()
profilePictureView.root.setOnClickListener { showEditProfilePictureUI() }
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
btnGroupNameDisplay.text = displayName
publicKeyTextView.text = hexEncodedPublicKey
@ -214,8 +214,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
binding.btnGroupNameDisplay.text = displayName
}
if (isUpdatingProfilePicture && profilePicture != null) {
binding.profilePictureView.recycle() // Clear the cached image before updating
binding.profilePictureView.update()
binding.profilePictureView.root.recycle() // Clear the cached image before updating
binding.profilePictureView.root.update()
}
displayNameToBeUploaded = null
profilePictureToBeUploaded = null

View File

@ -22,8 +22,10 @@ import network.loki.messenger.BuildConfig
import network.loki.messenger.R
import network.loki.messenger.databinding.DialogShareLogsBinding
import org.session.libsignal.utilities.ExternalStorageUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
import org.thoughtcrime.securesms.util.FileProviderUtil
import org.thoughtcrime.securesms.util.StreamUtil
import java.io.File
import java.io.FileOutputStream
@ -84,18 +86,26 @@ class ShareLogsDialog : BaseDialog() {
requireContext().contentResolver.update(mediaUri, updateValues, null, null)
}
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, mediaUri)
data = mediaUri
type = "text/plain"
val shareUri = if (mediaUri.scheme == ContentResolver.SCHEME_FILE) {
FileProviderUtil.getUriFor(context, File(mediaUri.path!!))
} else {
mediaUri
}
withContext(Main) {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, shareUri)
type = "text/plain"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
}
dismiss()
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
} catch (e: Exception) {
withContext(Main) {
Log.e("Loki", "Error saving logs", e)
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
}
dismiss()

View File

@ -33,7 +33,7 @@ import kotlin.coroutines.suspendCoroutine
interface ConversationRepository {
fun isOxenHostedOpenGroup(threadId: Long): Boolean
fun getRecipientForThreadId(threadId: Long): Recipient
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
fun saveDraft(threadId: Long, text: String)
fun getDraft(threadId: Long): String?
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
@ -86,12 +86,11 @@ class DefaultConversationRepository @Inject constructor(
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
return openGroup?.room == "session" || openGroup?.room == "oxen"
|| openGroup?.room == "lokinet" || openGroup?.room == "crypto"
return openGroup?.publicKey == OpenGroupAPIV2.defaultServerPublicKey
}
override fun getRecipientForThreadId(threadId: Long): Recipient {
return threadDb.getRecipientForThreadId(threadId)!!
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
return threadDb.getRecipientForThreadId(threadId)
}
override fun saveDraft(threadId: Long, text: String) {
@ -121,7 +120,7 @@ class DefaultConversationRepository @Inject constructor(
contact,
message.sentTimestamp
)
smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!)
smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!, true)
MessageSender.send(message, contact.address)
}
}

View File

@ -119,7 +119,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
Optional.absent(),
Optional.absent());
//insert the timer update message
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
database.insertSecureDecryptedMessageInbox(mediaMessage, -1, true, true);
//set the timer to the conversation
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
@ -141,7 +141,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
try {
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId);
database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp);
database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp, true);
if (groupId != null) {
// we need the group ID as recipient for setExpireMessages below

View File

@ -41,7 +41,8 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol {
override fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String) {
val job = RetrieveProfileAvatarJob(recipient, profilePictureURL)
ApplicationContext.getInstance(context).jobManager.add(job)
val jobManager = ApplicationContext.getInstance(context).jobManager
jobManager.add(job)
val sessionID = recipient.address.serialize()
val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()
var contact = contactDatabase.getContactWithSessionID(sessionID)

View File

@ -8,7 +8,7 @@
android:orientation="horizontal"
android:gravity="center_vertical">
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size" />

View File

@ -27,7 +27,7 @@
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp">
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:id="@+id/profileButton"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"

View File

@ -20,7 +20,7 @@
android:orientation="vertical"
android:gravity="center_horizontal">
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"

View File

@ -13,7 +13,7 @@
app:behavior_hideable="true"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"

View File

@ -13,7 +13,7 @@
app:behavior_hideable="true"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"

View File

@ -23,6 +23,7 @@
tools:visibility="visible" />
<ImageView
tools:visibility="visible"
android:src="@drawable/ic_download_circle_filled_48"
android:id="@+id/thumbnail_download_icon"
android:layout_width="@dimen/medium_button_height"

View File

@ -14,7 +14,7 @@
android:layout_height="match_parent"
android:background="@color/accent" />
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<org.thoughtcrime.securesms.conversation.v2.messages.DeletedMessageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@ -29,4 +29,4 @@
android:maxLines="2"
android:ellipsize="end" />
</LinearLayout>
</org.thoughtcrime.securesms.conversation.v2.messages.DeletedMessageView>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<org.thoughtcrime.securesms.conversation.v2.messages.DocumentView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
@ -27,4 +27,4 @@
android:maxLines="2"
android:ellipsize="end" />
</LinearLayout>
</org.thoughtcrime.securesms.conversation.v2.messages.DocumentView>

View File

@ -18,7 +18,7 @@
android:id="@+id/search_result_profile_picture_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:visibility="gone"
android:id="@+id/search_result_profile_picture"
android:layout_width="@dimen/medium_profile_picture_size"

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/mainLinkPreviewContainer"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_width="300dp"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"

View File

@ -13,7 +13,7 @@
android:layout_width="26dp"
android:layout_height="32dp">
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView"
android:layout_width="@dimen/very_small_profile_picture_size"
android:layout_height="@dimen/very_small_profile_picture_size"

View File

@ -17,7 +17,7 @@
android:layout_width="26dp"
android:layout_height="32dp">
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView"
android:layout_width="@dimen/very_small_profile_picture_size"
android:layout_height="@dimen/very_small_profile_picture_size"

View File

@ -6,7 +6,7 @@
android:gravity="center_vertical"
android:orientation="horizontal">
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<org.thoughtcrime.securesms.conversation.v2.messages.OpenGroupInvitationView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -66,4 +66,4 @@
</LinearLayout>
</LinearLayout>
</org.thoughtcrime.securesms.conversation.v2.messages.OpenGroupInvitationView>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<org.thoughtcrime.securesms.components.ProfilePictureView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/doubleModeImageViewContainer"
@ -39,4 +39,4 @@
android:layout_height="@dimen/large_profile_picture_size"
android:background="@drawable/profile_picture_view_large_background" />
</RelativeLayout>
</org.thoughtcrime.securesms.components.ProfilePictureView>

View File

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.thoughtcrime.securesms.conversation.v2.messages.QuoteView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mainQuoteViewContainer"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/input_bar_background"
android:paddingHorizontal="@dimen/medium_spacing"
android:paddingVertical="@dimen/small_spacing">
android:minWidth="300dp"
android:minHeight="52dp"
android:paddingVertical="12dp"
android:paddingHorizontal="12dp"
app:quote_mode="regular">
<View
android:id="@+id/quoteViewAccentLine"
android:layout_width="@dimen/accent_line_thickness"
android:layout_height="0dp"
android:layout_centerVertical="true"
android:layout_marginVertical="4dp"
android:layout_marginVertical="2dp"
android:background="@color/text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintEnd_toStartOf="@id/quoteStartBarrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
tools:visibility="gone"
android:id="@+id/quoteViewAttachmentPreviewContainer"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_marginVertical="@dimen/small_spacing"
android:background="@drawable/view_quote_attachment_preview_background"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintEnd_toStartOf="@id/quoteStartBarrier"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@ -61,44 +61,45 @@
app:barrierDirection="end"
app:constraint_referenced_ids="quoteViewAttachmentPreviewContainer,quoteViewAccentLine" />
<LinearLayout
android:layout_marginVertical="@dimen/small_spacing"
android:id="@+id/quoteTextParent"
<TextView
android:id="@+id/quoteViewAuthorTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_marginEnd="@dimen/medium_spacing"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/text"
android:textSize="@dimen/small_font_size"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/quoteViewBodyTextView"
app:layout_constraintEnd_toEndOf="@+id/quoteViewBodyTextView"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/quoteStartBarrier"
app:layout_constraintEnd_toStartOf="@id/quoteViewCancelButton"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:orientation="vertical"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Spiderman" />
<TextView
android:id="@+id/quoteViewBodyTextView"
android:layout_width="0dp"
android:layout_height="wrap_content">
<TextView
tools:visibility="gone"
android:id="@+id/quoteViewAuthorTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/text"
android:textSize="@dimen/small_font_size"
android:textStyle="bold"
tools:text="Spiderman" />
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/medium_spacing"
android:ellipsize="end"
android:maxLines="3"
android:textColor="@color/text"
android:textSize="@dimen/small_font_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/quoteStartBarrier"
app:layout_constraintTop_toBottomOf="@+id/quoteViewAuthorTextView"
app:layout_constraintVertical_chainStyle="packed"
android:maxWidth="240dp"
tools:maxLines="1"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/quoteViewBodyTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="3"
android:textColor="@color/text"
android:textSize="@dimen/small_font_size"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
<ImageView
tools:visibility="gone"
<View
android:id="@+id/quoteViewCancelButton"
android:layout_width="32dp"
android:layout_height="32dp"
@ -109,6 +110,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/text" />
app:tint="@color/text"
tools:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</org.thoughtcrime.securesms.conversation.v2.messages.QuoteView>

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"?>
<LinearLayout
<org.thoughtcrime.securesms.conversation.v2.messages.UntrustedAttachmentView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@ -27,4 +27,4 @@
android:maxLines="2"
android:ellipsize="end" />
</LinearLayout>
</org.thoughtcrime.securesms.conversation.v2.messages.UntrustedAttachmentView>

View File

@ -14,7 +14,7 @@
android:gravity="center_vertical"
android:padding="@dimen/medium_spacing">
<org.thoughtcrime.securesms.components.ProfilePictureView
<include layout="@layout/view_profile_picture"
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size" />

View File

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/visibleMessageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
@ -8,108 +10,117 @@
<TextView
android:id="@+id/dateBreakTextView"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_height="@dimen/large_spacing"
tools:text="@tools:sample/date/hhmmss"
android:gravity="center"
android:textColor="@color/text"
android:textSize="@dimen/very_small_font_size"
android:textStyle="bold"
android:gravity="center" />
android:textStyle="bold" />
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mainContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="bottom">
<FrameLayout
android:layout_alignBottom="@+id/messageContentContainer"
android:id="@+id/profilePictureContainer"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:orientation="horizontal">
<View
android:id="@+id/startSpacing"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="8dp"
android:layout_height="1dp"/>
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/very_small_profile_picture_size"
android:layout_height="@dimen/very_small_profile_picture_size"
android:layout_gravity="center" />
<include
tools:visibility="gone"
android:id="@+id/profilePictureView"
layout="@layout/view_profile_picture"
android:layout_marginBottom="@dimen/small_spacing"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/startSpacing"
app:layout_constraintEnd_toStartOf="@+id/expirationTimerViewContainer"
android:layout_marginEnd="@dimen/small_spacing"
android:layout_width="@dimen/very_small_profile_picture_size"
android:layout_height="@dimen/very_small_profile_picture_size"
android:layout_gravity="center" />
<ImageView
android:id="@+id/moderatorIconImageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="@dimen/small_spacing"
android:src="@drawable/ic_crown"
android:layout_gravity="bottom|end" />
<ImageView
android:visibility="gone"
android:id="@+id/moderatorIconImageView"
android:layout_width="16dp"
android:layout_height="16dp"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureView"
app:layout_constraintEnd_toEndOf="@+id/profilePictureView"
android:layout_marginEnd="-4dp"
android:layout_marginBottom="-4dp"
android:src="@drawable/ic_crown" />
</FrameLayout>
<LinearLayout
android:id="@+id/messageContentContainer"
<TextView
android:id="@+id/senderNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/text"
android:textStyle="bold"
tools:text="@tools:sample/full_names"
android:paddingBottom="4dp"
app:layout_constraintStart_toStartOf="@+id/expirationTimerViewContainer"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/senderNameTextView"
<LinearLayout
android:id="@+id/expirationTimerViewContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureView"
app:layout_constraintEnd_toStartOf="@+id/messageTimestampContainer"
app:layout_constraintStart_toEndOf="@+id/profilePictureView"
app:layout_constraintTop_toBottomOf="@+id/senderNameTextView">
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView
android:id="@+id/messageContentView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:textColor="@color/text"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end" />
/>
<LinearLayout
android:id="@+id/expirationTimerViewContainer"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
tools:visibility="visible"
android:visibility="gone"
android:id="@+id/expirationTimerView"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="@dimen/small_spacing" />
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView
android:id="@+id/messageContentView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
android:id="@+id/expirationTimerView"
android:layout_marginHorizontal="@dimen/small_spacing"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/messageTimestampTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="2dp"
android:maxLines="1"
android:textSize="11sp" />
<ImageView
android:id="@+id/messageStatusImageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="2dp"
android:padding="2dp"
android:src="@drawable/ic_delivery_status_sent" />
</RelativeLayout>
<View
android:id="@+id/messageContentSpacing"
android:minWidth="@dimen/very_large_spacing"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="0dp"/>
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/messageTimestampContainer"
android:layout_width="@dimen/medium_spacing"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@+id/expirationTimerViewContainer">
</LinearLayout>
<ImageView
android:id="@+id/messageStatusImageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:padding="2dp"
android:src="@drawable/ic_delivery_status_sent" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView>

Some files were not shown because too many files have changed in this diff Show More