diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..b650b98b11
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "libsession-util/libsession-util"]
+ path = libsession-util/libsession-util
+ url = https://github.com/oxen-io/libsession-util.git
diff --git a/BUILDING.md b/BUILDING.md
index e78207d964..f88509c680 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -32,6 +32,7 @@ Setting up a development environment and building from Android Studio
4. Android Studio should detect the presence of a project file and ask you whether to open it. Click "yes".
5. Default config options should be good enough.
6. Project initialization and building should proceed.
+7. Clone submodules with `git submodule update --init --recursive`
Contributing code
-----------------
diff --git a/README.md b/README.md
index c509dd9566..723d50c758 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ Add the [F-Droid repo](https://fdroid.getsession.org/)
Session integrates directly with [Oxen Service Nodes](https://docs.oxen.io/about-the-oxen-blockchain/oxen-service-nodes), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
-
+
## Want to contribute? Found a bug or have a feature request?
diff --git a/app/build.gradle b/app/build.gradle
index 8a42419732..74b9f84f07 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,3 +1,4 @@
+
buildscript {
repositories {
google()
@@ -13,6 +14,11 @@ buildscript {
}
}
+plugins {
+ id 'kotlin-kapt'
+ id 'com.google.dagger.hilt.android'
+}
+
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'witness'
@@ -22,11 +28,16 @@ apply plugin: 'com.google.gms.google-services'
apply plugin: 'kotlinx-serialization'
apply plugin: 'dagger.hilt.android.plugin'
+
configurations.all {
exclude module: "commons-logging"
}
dependencies {
+
+ implementation("com.google.dagger:hilt-android:2.46.1")
+ kapt("com.google.dagger:hilt-android-compiler:2.44")
+
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "com.google.android.material:material:$materialVersion"
@@ -39,7 +50,6 @@ dependencies {
implementation 'androidx.exifinterface:exifinterface:1.3.4'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
- implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
@@ -93,17 +103,13 @@ dependencies {
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
implementation 'com.annimon:stream:1.1.8'
- implementation 'com.takisoft.fix:colorpicker:1.0.1'
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
- implementation 'androidx.sqlite:sqlite-ktx:2.2.0'
- implementation 'net.zetetic:sqlcipher-android:4.5.3@aar'
- implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
- exclude group: 'com.fasterxml.jackson.core'
- exclude group: 'org.freemarker'
- }
+ implementation 'androidx.sqlite:sqlite-ktx:2.3.1'
+ implementation 'net.zetetic:sqlcipher-android:4.5.4@aar'
implementation project(":libsignal")
implementation project(":libsession")
+ implementation project(":libsession-util")
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
implementation project(":liblazysodium")
@@ -116,52 +122,62 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
- implementation "com.github.lelloman:android-identicons:v11"
- implementation "com.prof.rssparser:rssparser:2.0.4"
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
implementation "com.github.tbruyelle:rxpermissions:0.10.2"
implementation "com.github.ybq:Android-SpinKit:1.4.0"
implementation "com.opencsv:opencsv:4.6"
testImplementation "junit:junit:$junitVersion"
testImplementation 'org.assertj:assertj-core:3.11.1'
- testImplementation "org.mockito:mockito-inline:4.0.0"
+ testImplementation "org.mockito:mockito-inline:4.10.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
- testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
- testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
- testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
- testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
+ androidTestImplementation "org.mockito:mockito-android:4.10.0"
+ androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "androidx.test:core:$testCoreVersion"
- testImplementation "androidx.arch.core:core-testing:2.1.0"
+ testImplementation "androidx.arch.core:core-testing:2.2.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Core library
- androidTestImplementation 'androidx.test:core:1.4.0'
+ androidTestImplementation "androidx.test:core:$testCoreVersion"
+
+ androidTestImplementation('com.adevinta.android:barista:4.2.0') {
+ exclude group: 'org.jetbrains.kotlin'
+ }
// AndroidJUnitRunner and JUnit Rules
- androidTestImplementation 'androidx.test:runner:1.4.0'
- androidTestImplementation 'androidx.test:rules:1.4.0'
+ androidTestImplementation 'androidx.test:runner:1.5.2'
+ androidTestImplementation 'androidx.test:rules:1.5.0'
// Assertions
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.ext:truth:1.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.ext:truth:1.5.0'
androidTestImplementation 'com.google.truth:truth:1.1.3'
// Espresso dependencies
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
- androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
- androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
- androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.4.0'
- androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
- androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
- androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
- androidTestUtil 'androidx.test:orchestrator:1.4.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
+ androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
+ androidTestUtil 'androidx.test:orchestrator:1.4.2'
testImplementation 'org.robolectric:robolectric:4.4'
testImplementation 'org.robolectric:shadows-multidex:4.4'
+
+ implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.1'
+ implementation 'androidx.compose.ui:ui:1.4.3'
+ implementation 'androidx.compose.ui:ui-tooling:1.4.3'
+ implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.31.5-beta"
+ implementation "com.google.accompanist:accompanist-pager-indicators:0.31.5-beta"
+ implementation "androidx.compose.runtime:runtime-livedata:1.4.3"
+
+ implementation 'androidx.compose.foundation:foundation-layout:1.5.0-alpha02'
+ implementation 'androidx.compose.material:material:1.5.0-alpha02'
}
-def canonicalVersionCode = 338
-def canonicalVersionName = "1.16.9"
+def canonicalVersionCode = 354
+def canonicalVersionName = "1.17.0"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
@@ -203,6 +219,13 @@ android {
}
}
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion '1.4.7'
+ }
+
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
@@ -309,3 +332,8 @@ def autoResConfig() {
.collect { matcher -> matcher.group(1) }
.sort()
}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+}
diff --git a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
index 087d486893..eabe06f7d9 100644
--- a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
+++ b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
@@ -1,5 +1,6 @@
package network.loki.messenger
+import android.Manifest
import android.app.Instrumentation
import android.content.ClipboardManager
import android.content.Context
@@ -21,6 +22,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
+import com.adevinta.android.barista.interaction.PermissionGranter
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
@@ -85,6 +87,8 @@ class HomeActivityTests {
}
onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click())
onView(withId(R.id.registerButton)).perform(ViewActions.click())
+ // allow notification permission
+ PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
}
private fun goToMyChat() {
@@ -100,6 +104,7 @@ class HomeActivityTests {
copied = clipboardManager.primaryClip!!.getItemAt(0).text.toString()
}
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.typeText(copied))
+ onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard())
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
}
diff --git a/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt
new file mode 100644
index 0000000000..59cb8ede08
--- /dev/null
+++ b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt
@@ -0,0 +1,102 @@
+package network.loki.messenger
+
+import androidx.core.content.edit
+import androidx.preference.PreferenceManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import network.loki.messenger.libsession_util.ConfigBase
+import network.loki.messenger.libsession_util.Contacts
+import network.loki.messenger.libsession_util.util.Contact
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsignal.utilities.KeyHelper
+import org.session.libsignal.utilities.hexEncodedPublicKey
+import org.thoughtcrime.securesms.ApplicationContext
+import org.thoughtcrime.securesms.crypto.KeyPairUtilities
+import kotlin.random.Random
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class LibSessionTests {
+
+ private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
+ private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
+ private fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
+
+ private var fakeHashI = 0
+ private val nextFakeHash: String
+ get() = "fakehash${fakeHashI++}"
+
+ private fun maybeGetUserInfo(): Pair? {
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
+ val prefs = appContext.prefs
+ val localUserPublicKey = prefs.getLocalNumber()
+ val secretKey = with(appContext) {
+ val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
+ edKey.secretKey.asBytes
+ }
+ return if (localUserPublicKey == null || secretKey == null) null
+ else secretKey to localUserPublicKey
+ }
+
+ private fun buildContactMessage(contactList: List): ByteArray {
+ val (key,_) = maybeGetUserInfo()!!
+ val contacts = Contacts.Companion.newInstance(key)
+ contactList.forEach { contact ->
+ contacts.set(contact)
+ }
+ return contacts.push().config
+ }
+
+ private fun fakePollNewConfig(configBase: ConfigBase, toMerge: ByteArray) {
+ configBase.merge(nextFakeHash to toMerge)
+ MessagingModuleConfiguration.shared.configFactory.persist(configBase, System.currentTimeMillis())
+ }
+
+ @Before
+ fun setupUser() {
+ PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext).edit {
+ putBoolean(TextSecurePreferences.HAS_FORCED_NEW_CONFIG, true).apply()
+ }
+ val newBytes = randomSeedBytes().toByteArray()
+ val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
+ val kp = KeyPairUtilities.generate(newBytes)
+ KeyPairUtilities.store(context, kp.seed, kp.ed25519KeyPair, kp.x25519KeyPair)
+ val registrationID = KeyHelper.generateRegistrationId(false)
+ TextSecurePreferences.setLocalRegistrationId(context, registrationID)
+ TextSecurePreferences.setLocalNumber(context, kp.x25519KeyPair.hexEncodedPublicKey)
+ TextSecurePreferences.setRestorationTime(context, 0)
+ TextSecurePreferences.setHasViewedSeed(context, false)
+ }
+
+ @Test
+ fun migration_one_to_ones() {
+ val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
+ val storageSpy = spy(app.storage)
+ app.storage = storageSpy
+
+ val newContactId = randomSessionId()
+ val singleContact = Contact(
+ id = newContactId,
+ approved = true,
+ expiryMode = ExpiryMode.NONE
+ )
+ val newContactMerge = buildContactMessage(listOf(singleContact))
+ val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
+ fakePollNewConfig(contacts, newContactMerge)
+ verify(storageSpy).addLibSessionContacts(argThat {
+ first().let { it.id == newContactId && it.approved } && size == 1
+ })
+ verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2d2b2123dd..aa81fafc2b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -29,12 +29,16 @@
android:name="android.hardware.touchscreen"
android:required="false" />
+
+
+
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
index 53141534af..e4be27f24b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
@@ -40,6 +40,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPol
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.Address;
+import org.session.libsession.utilities.ConfigFactoryUpdateListener;
import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
@@ -59,6 +60,8 @@ import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.Storage;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
+import org.thoughtcrime.securesms.dependencies.AppComponent;
+import org.thoughtcrime.securesms.dependencies.ConfigFactory;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
import org.thoughtcrime.securesms.emoji.EmojiSource;
@@ -106,6 +109,8 @@ import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp;
import kotlin.Unit;
import kotlinx.coroutines.Job;
+import network.loki.messenger.libsession_util.ConfigBase;
+import network.loki.messenger.libsession_util.UserProfile;
/**
* Will be called once when the TextSecure process is created.
@@ -116,7 +121,7 @@ import kotlinx.coroutines.Job;
* @author Moxie Marlinspike
*/
@HiltAndroidApp
-public class ApplicationContext extends Application implements DefaultLifecycleObserver {
+public class ApplicationContext extends Application implements DefaultLifecycleObserver, ConfigFactoryUpdateListener {
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
@@ -137,9 +142,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private PersistentLogger persistentLogger;
@Inject LokiAPIDatabase lokiAPIDatabase;
- @Inject Storage storage;
+ @Inject public Storage storage;
@Inject MessageDataProvider messageDataProvider;
@Inject TextSecurePreferences textSecurePreferences;
+ @Inject ConfigFactory configFactory;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
@@ -157,6 +163,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return (ApplicationContext) context.getApplicationContext();
}
+ public TextSecurePreferences getPrefs() {
+ return EntryPoints.get(getApplicationContext(), AppComponent.class).getPrefs();
+ }
+
public DatabaseComponent getDatabaseComponent() {
return EntryPoints.get(getApplicationContext(), DatabaseComponent.class);
}
@@ -183,6 +193,15 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return this.persistentLogger;
}
+ @Override
+ public void notifyUpdates(@NonNull ConfigBase forConfigObject) {
+ // forward to the config factory / storage ig
+ if (forConfigObject instanceof UserProfile && !textSecurePreferences.getConfigurationMessageSynced()) {
+ textSecurePreferences.setConfigurationMessageSynced(true);
+ }
+ storage.notifyConfigUpdates(forConfigObject);
+ }
+
@Override
public void onCreate() {
DatabaseModule.init(this);
@@ -191,7 +210,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
storage,
messageDataProvider,
- ()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
+ ()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
+ configFactory
+ );
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
Log.i(TAG, "onCreate()");
startKovenant();
@@ -347,7 +368,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
private void initializeProfileManager() {
- this.profileManager = new ProfileManager();
+ this.profileManager = new ProfileManager(this, configFactory);
}
private void initializeTypingStatusSender() {
@@ -440,7 +461,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
poller.setUserPublicKey(userPublicKey);
return;
}
- poller = new Poller();
+ poller = new Poller(configFactory, new Timer());
}
public void startPollingIfNeeded() {
@@ -483,6 +504,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
});
} catch (Exception exception) {
// Do nothing
+ Log.e("Loki-Avatar", "Uploading avatar failed", exception);
}
});
}
@@ -520,6 +542,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
Log.d("Loki", "Failed to delete database.");
}
+ configFactory.keyPairChanged();
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt
new file mode 100644
index 0000000000..af38c31ff3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt
@@ -0,0 +1,28 @@
+package org.thoughtcrime.securesms
+
+import android.content.Context
+import network.loki.messenger.R
+
+class DeleteMediaDialog {
+ companion object {
+ @JvmStatic
+ fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
+ iconAttribute(R.attr.dialog_alert_icon)
+ title(
+ context.resources.getQuantityString(
+ R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
+ recordCount,
+ recordCount
+ )
+ )
+ text(
+ context.resources.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
+ recordCount,
+ recordCount
+ )
+ )
+ button(R.string.delete) { doDelete.run() }
+ cancelButton()
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaPreviewDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaPreviewDialog.kt
new file mode 100644
index 0000000000..0390a3007d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaPreviewDialog.kt
@@ -0,0 +1,19 @@
+package org.thoughtcrime.securesms
+
+import android.content.Context
+import network.loki.messenger.R
+
+class DeleteMediaPreviewDialog {
+ companion object {
+ @JvmStatic
+ fun show(context: Context, doDelete: Runnable) {
+ context.showSessionDialog {
+ iconAttribute(R.attr.dialog_alert_icon)
+ title(R.string.MediaPreviewActivity_media_delete_confirmation_title)
+ text(R.string.MediaPreviewActivity_media_delete_confirmation_message)
+ button(R.string.delete) { doDelete.run() }
+ cancelButton()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.java b/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.java
deleted file mode 100644
index 469629ed3f..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package org.thoughtcrime.securesms;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.TextView;
-
-import org.session.libsession.utilities.ExpirationUtil;
-
-import cn.carbswang.android.numberpickerview.library.NumberPickerView;
-import network.loki.messenger.R;
-
-public class ExpirationDialog extends AlertDialog {
-
- protected ExpirationDialog(Context context) {
- super(context);
- }
-
- protected ExpirationDialog(Context context, int theme) {
- super(context, theme);
- }
-
- protected ExpirationDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
- super(context, cancelable, cancelListener);
- }
-
- public static void show(final Context context,
- final int currentExpiration,
- final @NonNull OnClickListener listener)
- {
- final View view = createNumberPickerView(context, currentExpiration);
-
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(context.getString(R.string.ExpirationDialog_disappearing_messages));
- builder.setView(view);
- builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
- int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
- listener.onClick(context.getResources().getIntArray(R.array.expiration_times)[selected]);
- });
- builder.setNegativeButton(android.R.string.cancel, null);
- builder.show();
- }
-
- private static View createNumberPickerView(final Context context, final int currentExpiration) {
- final LayoutInflater inflater = LayoutInflater.from(context);
- final View view = inflater.inflate(R.layout.expiration_dialog, null);
- final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker);
- final TextView textView = view.findViewById(R.id.expiration_details);
- final int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
- final String[] expirationDisplayValues = new String[expirationTimes.length];
-
- int selectedIndex = expirationTimes.length - 1;
-
- for (int i=0;i= expirationTimes[i]) &&
- (i == expirationTimes.length -1 || currentExpiration < expirationTimes[i+1])) {
- selectedIndex = i;
- }
- }
-
- numberPickerView.setDisplayedValues(expirationDisplayValues);
- numberPickerView.setMinValue(0);
- numberPickerView.setMaxValue(expirationTimes.length-1);
-
- NumberPickerView.OnValueChangeListener listener = (picker, oldVal, newVal) -> {
- if (newVal == 0) {
- textView.setText(R.string.ExpirationDialog_your_messages_will_not_expire);
- } else {
- textView.setText(context.getString(R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, picker.getDisplayedValues()[newVal]));
- }
- };
-
- numberPickerView.setOnValueChangedListener(listener);
- numberPickerView.setValue(selectedIndex);
- listener.onValueChange(numberPickerView, selectedIndex, selectedIndex);
-
- return view;
- }
-
- public interface OnClickListener {
- public void onClick(int expirationTime);
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.kt
new file mode 100644
index 0000000000..9a34c1ec4b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.kt
@@ -0,0 +1,51 @@
+package org.thoughtcrime.securesms
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import cn.carbswang.android.numberpickerview.library.NumberPickerView
+import network.loki.messenger.R
+import org.session.libsession.utilities.ExpirationUtil
+
+fun Context.showExpirationDialog(
+ expiration: Int,
+ onExpirationTime: (Int) -> Unit
+): AlertDialog {
+ val view = LayoutInflater.from(this).inflate(R.layout.expiration_dialog, null)
+ val numberPickerView = view.findViewById(R.id.expiration_number_picker)
+
+ fun updateText(index: Int) {
+ view.findViewById(R.id.expiration_details).text = when (index) {
+ 0 -> getString(R.string.ExpirationDialog_your_messages_will_not_expire)
+ else -> getString(
+ R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen,
+ numberPickerView.displayedValues[index]
+ )
+ }
+ }
+
+ val expirationTimes = resources.getIntArray(R.array.expiration_times)
+ val expirationDisplayValues = expirationTimes
+ .map { ExpirationUtil.getExpirationDisplayValue(this, it) }
+ .toTypedArray()
+
+ val selectedIndex = expirationTimes.run { indexOfFirst { it >= expiration }.coerceIn(indices) }
+
+ numberPickerView.apply {
+ displayedValues = expirationDisplayValues
+ minValue = 0
+ maxValue = expirationTimes.lastIndex
+ setOnValueChangedListener { _, _, index -> updateText(index) }
+ value = selectedIndex
+ }
+
+ updateText(selectedIndex)
+
+ return showSessionDialog {
+ title(getString(R.string.ExpirationDialog_disappearing_messages))
+ view(view)
+ okButton { onExpirationTime(numberPickerView.let { expirationTimes[it.value] }) }
+ cancelButton()
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java
index b36fdb3ca1..95ba15c82e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java
@@ -76,6 +76,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
+import kotlin.Unit;
import network.loki.messenger.R;
/**
@@ -318,9 +319,9 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
@SuppressWarnings("CodeBlock2Expr")
@SuppressLint({"InlinedApi", "StaticFieldLeak"})
private void handleSaveMedia(@NonNull Collection mediaRecords) {
- final Context context = getContext();
+ final Context context = requireContext();
- SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> {
+ SaveAttachmentTask.showWarningDialog(context, mediaRecords.size(), () -> {
Permissions.with(this)
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P)
@@ -362,7 +363,8 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
}.execute();
})
.execute();
- }, mediaRecords.size());
+ return Unit.INSTANCE;
+ });
}
private void sendMediaSavedNotificationIfNeeded() {
@@ -374,41 +376,26 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
@SuppressLint("StaticFieldLeak")
private void handleDeleteMedia(@NonNull Collection mediaRecords) {
int recordCount = mediaRecords.size();
- Resources res = getContext().getResources();
- String confirmTitle = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
- recordCount,
- recordCount);
- String confirmMessage = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
- recordCount,
- recordCount);
- AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
- builder.setIconAttribute(R.attr.dialog_alert_icon);
- builder.setTitle(confirmTitle);
- builder.setMessage(confirmMessage);
- builder.setCancelable(true);
-
- builder.setPositiveButton(R.string.delete, (dialogInterface, i) -> {
- new ProgressDialogAsyncTask(getContext(),
- R.string.MediaOverviewActivity_Media_delete_progress_title,
- R.string.MediaOverviewActivity_Media_delete_progress_message)
- {
- @Override
- protected Void doInBackground(MediaDatabase.MediaRecord... records) {
- if (records == null || records.length == 0) {
- return null;
- }
-
- for (MediaDatabase.MediaRecord record : records) {
- AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
- }
+ DeleteMediaDialog.show(
+ requireContext(),
+ recordCount,
+ () -> new ProgressDialogAsyncTask(
+ requireContext(),
+ R.string.MediaOverviewActivity_Media_delete_progress_title,
+ R.string.MediaOverviewActivity_Media_delete_progress_message) {
+ @Override
+ protected Void doInBackground(MediaDatabase.MediaRecord... records) {
+ if (records == null || records.length == 0) {
return null;
}
- }.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()]));
- });
- builder.setNegativeButton(android.R.string.cancel, null);
- builder.show();
+ for (MediaDatabase.MediaRecord record : records) {
+ AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
+ }
+ return null;
+ }
+ }.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()])));
}
private void handleSelectAllMedia() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
index 6544c2ab89..f19a1fc45e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -85,6 +85,7 @@ import java.io.IOException;
import java.util.Locale;
import java.util.WeakHashMap;
+import kotlin.Unit;
import network.loki.messenger.R;
/**
@@ -146,6 +147,10 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
};
+ public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) {
+ return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread());
+ }
+
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
Intent previewIntent = null;
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
@@ -416,7 +421,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem == null) return;
- SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
+ SaveAttachmentTask.showWarningDialog(this, 1, () -> {
Permissions.with(this)
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P)
@@ -433,6 +438,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
})
.execute();
+ return Unit.INSTANCE;
});
}
@@ -449,29 +455,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
return;
}
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setIconAttribute(R.attr.dialog_alert_icon);
- builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
- builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);
- builder.setCancelable(true);
-
- builder.setPositiveButton(R.string.delete, (dialogInterface, which) -> {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... voids) {
- if (mediaItem.attachment == null) {
- return null;
- }
- AttachmentUtil.deleteAttachment(MediaPreviewActivity.this.getApplicationContext(),
- mediaItem.attachment);
- return null;
- }
- }.execute();
+ DeleteMediaPreviewDialog.show(this, () -> {
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... voids) {
+ DatabaseAttachment attachment = mediaItem.attachment;
+ if (attachment != null) {
+ AttachmentUtil.deleteAttachment(getApplicationContext(), attachment);
+ }
+ return null;
+ }
+ }.execute();
finish();
});
- builder.setNegativeButton(android.R.string.cancel, null);
- builder.show();
}
@Override
@@ -531,7 +528,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override
public void onLoadFinished(@NonNull Loader> loader, @Nullable Pair data) {
if (data != null) {
- @SuppressWarnings("ConstantConditions")
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt
new file mode 100644
index 0000000000..00e2c3d6d8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt
@@ -0,0 +1,11 @@
+package org.thoughtcrime.securesms
+
+import org.session.libsession.utilities.recipients.Recipient
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord
+import org.thoughtcrime.securesms.mms.Slide
+
+data class MediaPreviewArgs(
+ val slide: Slide,
+ val mmsRecord: MmsMessageRecord?,
+ val thread: Recipient?,
+)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.java b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.java
deleted file mode 100644
index acca9f8375..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.thoughtcrime.securesms;
-
-import android.content.Context;
-import android.content.DialogInterface;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-
-import java.util.concurrent.TimeUnit;
-
-import network.loki.messenger.R;
-
-public class MuteDialog extends AlertDialog {
-
-
- protected MuteDialog(Context context) {
- super(context);
- }
-
- protected MuteDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
- super(context, cancelable, cancelListener);
- }
-
- protected MuteDialog(Context context, int theme) {
- super(context, theme);
- }
-
- public static void show(final Context context, final @NonNull MuteSelectionListener listener) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.MuteDialog_mute_notifications);
- builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, final int which) {
- final long muteUntil;
-
- switch (which) {
- case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(2); break;
- case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
- case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
- case 4: muteUntil = Long.MAX_VALUE; break;
- default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
- }
-
- listener.onMuted(muteUntil);
- }
- });
-
- builder.show();
-
- }
-
- public interface MuteSelectionListener {
- public void onMuted(long until);
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
new file mode 100644
index 0000000000..f294e387ff
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
@@ -0,0 +1,27 @@
+package org.thoughtcrime.securesms
+
+import android.content.Context
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AlertDialog
+import network.loki.messenger.R
+import java.util.concurrent.TimeUnit
+
+fun showMuteDialog(
+ context: Context,
+ onMuteDuration: (Long) -> Unit
+): AlertDialog = context.showSessionDialog {
+ title(R.string.MuteDialog_mute_notifications)
+ items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) {
+ onMuteDuration(Option.values()[it].getTime())
+ }
+}
+
+private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
+ ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
+ TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.DAYS.toMillis(2)),
+ ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
+ SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
+ FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });
+
+ constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { System.currentTimeMillis() + duration })
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt
new file mode 100644
index 0000000000..141a98e4ac
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt
@@ -0,0 +1,146 @@
+package org.thoughtcrime.securesms
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.LinearLayout.VERTICAL
+import android.widget.TextView
+import androidx.annotation.AttrRes
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+import androidx.annotation.StyleRes
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.setMargins
+import androidx.core.view.setPadding
+import androidx.core.view.updateMargins
+import androidx.fragment.app.Fragment
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.util.toPx
+
+
+@DslMarker
+@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
+annotation class DialogDsl
+
+@DialogDsl
+class SessionDialogBuilder(val context: Context) {
+
+ private val dp20 = toPx(20, context.resources)
+ private val dp40 = toPx(40, context.resources)
+
+ private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
+
+ private var dialog: AlertDialog? = null
+ private fun dismiss() = dialog?.dismiss()
+
+ private val topView = LinearLayout(context).apply { orientation = VERTICAL }
+ .also(dialogBuilder::setCustomTitle)
+ private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
+ private val buttonLayout = LinearLayout(context)
+
+ private val root = LinearLayout(context).apply { orientation = VERTICAL }
+ .also(dialogBuilder::setView)
+ .apply {
+ addView(contentView)
+ addView(buttonLayout)
+ }
+
+ fun title(@StringRes id: Int) = title(context.getString(id))
+
+ fun title(text: CharSequence?) = title(text?.toString())
+ fun title(text: String?) {
+ text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20) }
+ }
+
+ fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
+ fun text(text: CharSequence?, @StyleRes style: Int = 0) {
+ text(text, style) {
+ layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ .apply { updateMargins(dp40, 0, dp40, dp20) }
+ }
+ }
+
+
+ private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
+ text ?: return
+ TextView(context, null, 0, style)
+ .apply {
+ setText(text)
+ textAlignment = View.TEXT_ALIGNMENT_CENTER
+ modify()
+ }.let(topView::addView)
+ }
+
+ fun view(view: View) = contentView.addView(view)
+
+ fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView)
+
+ fun iconAttribute(@AttrRes icon: Int): AlertDialog.Builder = dialogBuilder.setIconAttribute(icon)
+
+ fun singleChoiceItems(
+ options: Collection,
+ currentSelected: Int = 0,
+ onSelect: (Int) -> Unit
+ ) = singleChoiceItems(options.toTypedArray(), currentSelected, onSelect)
+
+ fun singleChoiceItems(
+ options: Array,
+ currentSelected: Int = 0,
+ onSelect: (Int) -> Unit
+ ): AlertDialog.Builder = dialogBuilder.setSingleChoiceItems(
+ options,
+ currentSelected
+ ) { dialog, it -> onSelect(it); dialog.dismiss() }
+
+ fun items(
+ options: Array,
+ onSelect: (Int) -> Unit
+ ): AlertDialog.Builder = dialogBuilder.setItems(
+ options,
+ ) { dialog, it -> onSelect(it); dialog.dismiss() }
+
+ fun destructiveButton(
+ @StringRes text: Int,
+ @StringRes contentDescription: Int,
+ listener: () -> Unit = {}
+ ) = button(
+ text,
+ contentDescription,
+ R.style.Widget_Session_Button_Dialog_DestructiveText,
+ listener
+ )
+
+ fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok, listener = listener)
+ fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button, listener = listener)
+
+ fun button(
+ @StringRes text: Int,
+ @StringRes contentDescriptionRes: Int = text,
+ @StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
+ listener: (() -> Unit) = {}
+ ) = Button(context, null, 0, style).apply {
+ setText(text)
+ contentDescription = resources.getString(contentDescriptionRes)
+ layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1f)
+ .apply { setMargins(toPx(20, resources)) }
+ setOnClickListener {
+ listener.invoke()
+ dismiss()
+ }
+ }.let(buttonLayout::addView)
+
+ fun create(): AlertDialog = dialogBuilder.create().also { dialog = it }
+ fun show(): AlertDialog = dialogBuilder.show().also { dialog = it }
+}
+
+fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
+ SessionDialogBuilder(this).apply { build() }.show()
+
+fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
+ SessionDialogBuilder(requireContext()).apply { build() }.show()
+fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
+ SessionDialogBuilder(requireContext()).apply { build() }.create()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
index 7e732d1aa7..b87eac12c4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
@@ -249,17 +249,12 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
viewModel.callState.collect { state ->
Log.d("Loki", "Consuming view model state $state")
when (state) {
- CALL_RINGING -> {
- if (wantsToAnswer) {
- answerCall()
- wantsToAnswer = false
- }
- }
- CALL_OUTGOING -> {
- }
- CALL_CONNECTED -> {
+ CALL_RINGING -> if (wantsToAnswer) {
+ answerCall()
wantsToAnswer = false
}
+ CALL_CONNECTED -> wantsToAnswer = false
+ else -> {}
}
updateControls(state)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
index fcc8a97ca1..d2fd5b548d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
@@ -2,12 +2,14 @@ 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
import com.bumptech.glide.load.engine.DiskCacheStrategy
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewProfilePictureBinding
+import network.loki.messenger.databinding.ViewUserBinding
import org.session.libsession.avatars.ContactColors
import org.session.libsession.avatars.PlaceholderAvatarPhoto
import org.session.libsession.avatars.ProfileContactPhoto
@@ -17,13 +19,14 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
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
+ private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
+ private val glide: GlideRequests = GlideApp.with(this)
var publicKey: String? = null
var displayName: String? = null
var additionalPublicKey: String? = null
@@ -31,13 +34,17 @@ class ProfilePictureView @JvmOverloads constructor(
var isLarge = false
private val profilePicturesCache = mutableMapOf()
- private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
- .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
- private val unknownOpenGroupDrawable = ResourceContactPhoto(R.drawable.ic_notification)
- .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
+ private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
+ .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
+ private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
+ .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
// endregion
+ constructor(context: Context, sender: Recipient): this(context) {
+ update(sender)
+ }
+
// region Updating
fun update(recipient: Recipient) {
fun getUserDisplayName(publicKey: String): String {
@@ -51,12 +58,19 @@ class ProfilePictureView @JvmOverloads constructor(
.sorted()
.take(2)
.toMutableList()
- val pk = members.getOrNull(0)?.serialize() ?: ""
- publicKey = pk
- displayName = getUserDisplayName(pk)
- val apk = members.getOrNull(1)?.serialize() ?: ""
- additionalPublicKey = apk
- additionalDisplayName = getUserDisplayName(apk)
+ if (members.size <= 1) {
+ publicKey = ""
+ displayName = ""
+ additionalPublicKey = ""
+ additionalDisplayName = ""
+ } else {
+ val pk = members.getOrNull(0)?.serialize() ?: ""
+ publicKey = pk
+ displayName = getUserDisplayName(pk)
+ val apk = members.getOrNull(1)?.serialize() ?: ""
+ additionalPublicKey = apk
+ additionalDisplayName = getUserDisplayName(apk)
+ }
} else if(recipient.isOpenGroupInboxRecipient) {
val publicKey = GroupUtil.getDecodedOpenGroupInbox(recipient.address.serialize())
this.publicKey = publicKey
@@ -72,7 +86,6 @@ class ProfilePictureView @JvmOverloads constructor(
}
fun update() {
- if (!this::glide.isInitialized) return
val publicKey = publicKey ?: return
val additionalPublicKey = additionalPublicKey
if (additionalPublicKey != null) {
@@ -104,31 +117,38 @@ class ProfilePictureView @JvmOverloads constructor(
if (publicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
if (profilePicturesCache[imageView] == recipient) return
+ profilePicturesCache[imageView] = recipient
val signalProfilePicture = recipient.contactPhoto
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
glide.clear(imageView)
+ val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
+
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
glide.load(signalProfilePicture)
.placeholder(unknownRecipientDrawable)
.centerCrop()
- .error(unknownRecipientDrawable)
+ .error(glide.load(placeholder))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(imageView)
} else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) {
- imageView.setImageDrawable(unknownOpenGroupDrawable)
+ glide.load(unknownOpenGroupDrawable)
+ .centerCrop()
+ .circleCrop()
+ .into(imageView)
} else {
- val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
glide.load(placeholder)
.placeholder(unknownRecipientDrawable)
.centerCrop()
+ .circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
}
- profilePicturesCache[imageView] = recipient
} else {
- imageView.setImageDrawable(null)
+ glide.load(unknownRecipientDrawable)
+ .centerCrop()
+ .into(imageView)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
index e88cf1d08b..36a8c1adf5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
@@ -7,6 +7,7 @@ import android.view.View
import android.widget.LinearLayout
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewUserBinding
+import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
@@ -47,15 +48,14 @@ class UserView : LinearLayout {
// region Updating
fun bind(user: Recipient, glide: GlideRequests, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
+ val isLocalUser = user.isLocalNumber
fun getUserDisplayName(publicKey: String): String {
+ if (isLocalUser) return context.getString(R.string.MessageRecord_you)
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
}
- 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.root.glide = glide
- binding.profilePictureView.root.update(user)
+ binding.profilePictureView.update(user)
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
when (actionIndicator) {
@@ -87,7 +87,7 @@ class UserView : LinearLayout {
}
fun unbind() {
- binding.profilePictureView.root.recycle()
+ binding.profilePictureView.recycle()
}
// endregion
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt
index 99e7c90615..68e2f975c9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt
@@ -32,14 +32,13 @@ class ContactListAdapter(
class ContactViewHolder(private val binding: ViewContactBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(contact: ContactListItem.Contact, glide: GlideRequests, listener: (Recipient) -> Unit) {
- binding.profilePictureView.root.glide = glide
- binding.profilePictureView.root.update(contact.recipient)
+ binding.profilePictureView.update(contact.recipient)
binding.nameTextView.text = contact.displayName
binding.root.setOnClickListener { listener(contact.recipient) }
}
fun unbind() {
- binding.profilePictureView.root.recycle()
+ binding.profilePictureView.recycle()
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationHomeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationHomeFragment.kt
index 2e62932ab0..92f050f76a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationHomeFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationHomeFragment.kt
@@ -55,7 +55,7 @@ class NewConversationHomeFragment : Fragment() {
val displayName = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionId
ContactListItem.Contact(it, displayName)
}.sortedBy { it.displayName }
- .groupBy { if (PublicKeyValidation.isValid(it.displayName)) unknownSectionTitle else it.displayName.first().uppercase() }
+ .groupBy { if (PublicKeyValidation.isValid(it.displayName)) unknownSectionTitle else it.displayName.firstOrNull()?.uppercase() ?: unknownSectionTitle }
.toMutableMap()
contactGroups.remove(unknownSectionTitle)?.let { contactGroups.put(unknownSectionTitle, it) }
adapter.items = contactGroups.flatMap { entry -> listOf(ContactListItem.Header(entry.key)) + entry.value }
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
index 7844ca4c22..233d43eaeb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
@@ -3,28 +3,50 @@ package org.thoughtcrime.securesms.conversation.v2
import android.Manifest
import android.animation.FloatEvaluator
import android.animation.ValueAnimator
-import android.content.*
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
import android.content.res.Resources
import android.database.Cursor
import android.graphics.Rect
import android.graphics.Typeface
import android.net.Uri
-import android.os.*
+import android.os.AsyncTask
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.provider.MediaStore
+import android.text.SpannableStringBuilder
+import android.text.SpannedString
import android.text.TextUtils
+import android.text.style.StyleSpan
import android.util.Pair
import android.util.TypedValue
-import android.view.*
+import android.view.ActionMode
+import android.view.Menu
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.Toast
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.DimenRes
-import androidx.appcompat.app.AlertDialog
+import androidx.core.text.set
+import androidx.core.text.toSpannable
+import androidx.core.view.drawToBitmap
import androidx.core.view.isVisible
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
@@ -32,6 +54,11 @@ import androidx.recyclerview.widget.RecyclerView
import com.annimon.stream.Stream
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.consumeAsFlow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
@@ -58,8 +85,12 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.snode.SnodeAPI
-import org.session.libsession.utilities.*
+import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
+import org.session.libsession.utilities.GroupUtil
+import org.session.libsession.utilities.MediaTypes
+import org.session.libsession.utilities.Stub
+import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.RecipientModifiedListener
@@ -70,7 +101,6 @@ import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.hexEncodedPrivateKey
import org.thoughtcrime.securesms.ApplicationContext
-import org.thoughtcrime.securesms.ExpirationDialog
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
import org.thoughtcrime.securesms.audio.AudioRecorder
@@ -78,6 +108,10 @@ import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.sele
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener
+import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP
+import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY
+import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND
+import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog
@@ -92,10 +126,24 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
-import org.thoughtcrime.securesms.conversation.v2.utilities.*
+import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
+import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
+import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
+import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
-import org.thoughtcrime.securesms.database.*
+import org.thoughtcrime.securesms.database.GroupDatabase
+import org.thoughtcrime.securesms.database.LokiAPIDatabase
+import org.thoughtcrime.securesms.database.LokiMessageDatabase
+import org.thoughtcrime.securesms.database.LokiThreadDatabase
+import org.thoughtcrime.securesms.database.MmsDatabase
+import org.thoughtcrime.securesms.database.MmsSmsDatabase
+import org.thoughtcrime.securesms.database.ReactionDatabase
+import org.thoughtcrime.securesms.database.RecipientDatabase
+import org.thoughtcrime.securesms.database.SessionContactDatabase
+import org.thoughtcrime.securesms.database.SmsDatabase
+import org.thoughtcrime.securesms.database.Storage
+import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
@@ -108,14 +156,31 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
-import org.thoughtcrime.securesms.mms.*
+import org.thoughtcrime.securesms.mms.AudioSlide
+import org.thoughtcrime.securesms.mms.GifSlide
+import org.thoughtcrime.securesms.mms.GlideApp
+import org.thoughtcrime.securesms.mms.ImageSlide
+import org.thoughtcrime.securesms.mms.MediaConstraints
+import org.thoughtcrime.securesms.mms.Slide
+import org.thoughtcrime.securesms.mms.SlideDeck
+import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
-import org.thoughtcrime.securesms.util.*
+import org.thoughtcrime.securesms.showExpirationDialog
+import org.thoughtcrime.securesms.showSessionDialog
+import org.thoughtcrime.securesms.util.ActivityDispatcher
+import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
+import org.thoughtcrime.securesms.util.DateUtils
+import org.thoughtcrime.securesms.util.MediaUtil
+import org.thoughtcrime.securesms.util.SaveAttachmentTask
+import org.thoughtcrime.securesms.util.isScrolledToBottom
+import org.thoughtcrime.securesms.util.push
+import org.thoughtcrime.securesms.util.toPx
import java.lang.ref.WeakReference
-import java.util.*
+import java.util.Locale
import java.util.concurrent.ExecutionException
+import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
@@ -184,11 +249,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
it
}
val recipient = Recipient.from(this, address, false)
- threadId = threadDb.getOrCreateThreadIdFor(recipient)
+ threadId = storage.getOrCreateThreadIdFor(recipient.address)
}
} ?: finish()
}
- viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair())
+ viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair(), contentResolver)
}
private var actionMode: ActionMode? = null
private var unreadCount = 0
@@ -209,6 +274,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val searchViewModel: SearchViewModel by viewModels()
var searchViewItem: MenuItem? = null
+ private val bufferedLastSeenChannel = Channel(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private var emojiPickerVisible = false
private val isScrolledToBottom: Boolean
@@ -228,11 +294,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
}
+ // There is a bug when initially joining a community where all messages will immediately be marked
+ // as read if we reverse the message list so this is now hard-coded to false
+ private val reverseMessageList = false
+
private val adapter by lazy {
- val cursor = mmsSmsDb.getConversation(viewModel.threadId, !isIncomingMessageRequestThread())
+ val cursor = mmsSmsDb.getConversation(viewModel.threadId, reverseMessageList)
val adapter = ConversationAdapter(
this,
cursor,
+ storage.getLastSeen(viewModel.threadId),
+ reverseMessageList,
onItemPress = { message, position, view, event ->
handlePress(message, position, view, event)
},
@@ -274,6 +346,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val cameraButton by lazy { InputBarButton(this, R.drawable.ic_baseline_photo_camera_24, hasOpaqueBackground = true) }
private val messageToScrollTimestamp = AtomicLong(-1)
private val messageToScrollAuthor = AtomicReference(null)
+ private val firstLoad = AtomicBoolean(true)
private lateinit var reactionDelegate: ConversationReactionDelegate
private val reactWithAnyEmojiStartPage = -1
@@ -318,28 +391,31 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
setUpUiStateObserver()
binding!!.scrollToBottomButton.setOnClickListener {
val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
+ val targetPosition = if (reverseMessageList) 0 else adapter.itemCount
if (layoutManager.isSmoothScrolling) {
- binding?.conversationRecyclerView?.scrollToPosition(0)
+ binding?.conversationRecyclerView?.scrollToPosition(targetPosition)
} else {
// It looks like 'smoothScrollToPosition' will actually load all intermediate items in
// order to do the scroll, this can be very slow if there are a lot of messages so
// instead we check the current position and if there are more than 10 items to scroll
// we jump instantly to the 10th item and scroll from there (this should happen quick
// enough to give a similar scroll effect without having to load everything)
- val position = layoutManager.findFirstVisibleItemPosition()
- if (position > 10) {
- binding?.conversationRecyclerView?.scrollToPosition(10)
- }
+// val position = if (reverseMessageList) layoutManager.findFirstVisibleItemPosition() else layoutManager.findLastVisibleItemPosition()
+// val targetBuffer = if (reverseMessageList) 10 else Math.max(0, (adapter.itemCount - 1) - 10)
+// if (position > targetBuffer) {
+// binding?.conversationRecyclerView?.scrollToPosition(targetBuffer)
+// }
binding?.conversationRecyclerView?.post {
- binding?.conversationRecyclerView?.smoothScrollToPosition(0)
+ binding?.conversationRecyclerView?.smoothScrollToPosition(targetPosition)
}
}
}
updateUnreadCountIndicator()
updateSubtitle()
+ updatePlaceholder()
setUpBlockedBanner()
binding!!.searchBottomBar.setEventListener(this)
updateSendAfterApprovalText()
@@ -349,20 +425,30 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val weakActivity = WeakReference(this)
lifecycleScope.launch(Dispatchers.IO) {
- unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
-
// Note: We are accessing the `adapter` property because we want it to be loaded on
// the background thread to avoid blocking the UI thread and potentially hanging when
// transitioning to the activity
weakActivity.get()?.adapter ?: return@launch
+ // 'Get' instead of 'GetAndSet' here because we want to trigger the highlight in 'onFirstLoad'
+ // by triggering 'jumpToMessage' using these values
+ val messageTimestamp = messageToScrollTimestamp.get()
+ val author = messageToScrollAuthor.get()
+ val targetPosition = if (author != null && messageTimestamp >= 0) mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, messageTimestamp, author, reverseMessageList) else -1
+
withContext(Dispatchers.Main) {
setUpRecyclerView()
setUpTypingObserver()
setUpRecipientObserver()
getLatestOpenGroupInfoIfNeeded()
setUpSearchResultObserver()
- scrollToFirstUnreadMessageIfNeeded()
+
+ if (author != null && messageTimestamp >= 0 && targetPosition >= 0) {
+ binding?.conversationRecyclerView?.scrollToPosition(targetPosition)
+ }
+ else {
+ scrollToFirstUnreadMessageIfNeeded(true)
+ }
}
}
@@ -370,16 +456,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
ViewUtil.findStubById(this, R.id.conversation_reaction_scrubber_stub)
reactionDelegate = ConversationReactionDelegate(reactionOverlayStub)
reactionDelegate.setOnReactionSelectedListener(this)
+ lifecycleScope.launch {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ // only update the conversation every 3 seconds maximum
+ // channel is rendezvous and shouldn't block on try send calls as often as we want
+ val bufferedFlow = bufferedLastSeenChannel.consumeAsFlow()
+ bufferedFlow.filter {
+ it > storage.getLastSeen(viewModel.threadId)
+ }.collectLatest { latestMessageRead ->
+ withContext(Dispatchers.IO) {
+ storage.markConversationAsRead(viewModel.threadId, latestMessageRead)
+ }
+ }
+ }
+ }
}
override fun onResume() {
super.onResume()
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId)
- val recipient = viewModel.recipient ?: return
-
- lifecycleScope.launch(Dispatchers.IO) {
- threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient)
- }
contentResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
@@ -406,23 +501,45 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
push(intent, false)
}
- override fun showDialog(baseDialog: BaseDialog, tag: String?) {
- baseDialog.show(supportFragmentManager, tag)
+ override fun showDialog(dialogFragment: DialogFragment, tag: String?) {
+ dialogFragment.show(supportFragmentManager, tag)
}
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader {
- return ConversationLoader(viewModel.threadId, !isIncomingMessageRequestThread(), this@ConversationActivityV2)
+ return ConversationLoader(viewModel.threadId, reverseMessageList, this@ConversationActivityV2)
}
override fun onLoadFinished(loader: Loader, cursor: Cursor?) {
+ val oldCount = adapter.itemCount
+ val newCount = cursor?.count ?: 0
adapter.changeCursor(cursor)
+
if (cursor != null) {
val messageTimestamp = messageToScrollTimestamp.getAndSet(-1)
val author = messageToScrollAuthor.getAndSet(null)
+ val initialUnreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
+
+ // Update the unreadCount value to be loaded from the database since we got a new message
+ if (firstLoad.get() || oldCount != newCount || initialUnreadCount != unreadCount) {
+ // Update the unreadCount value to be loaded from the database since we got a new
+ // message (we need to store it in a local variable as it can get overwritten on
+ // another thread before the 'firstLoad.getAndSet(false)' case below)
+ unreadCount = initialUnreadCount
+ updateUnreadCountIndicator()
+ }
+
if (author != null && messageTimestamp >= 0) {
- jumpToMessage(author, messageTimestamp, null)
+ jumpToMessage(author, messageTimestamp, firstLoad.get(), null)
+ }
+ else if (firstLoad.getAndSet(false)) {
+ scrollToFirstUnreadMessageIfNeeded(true)
+ handleRecyclerViewScrolled()
+ }
+ else if (oldCount != newCount) {
+ handleRecyclerViewScrolled()
}
}
+ updatePlaceholder()
}
override fun onLoaderReset(cursor: Loader) {
@@ -432,7 +549,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate
private fun setUpRecyclerView() {
binding!!.conversationRecyclerView.adapter = adapter
- val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, !isIncomingMessageRequestThread())
+ val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, reverseMessageList)
binding!!.conversationRecyclerView.layoutManager = layoutManager
// Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will)
LoaderManager.getInstance(this).restartLoader(0, null, this)
@@ -441,6 +558,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
handleRecyclerViewScrolled()
}
+
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+
+ }
})
binding!!.conversationRecyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
@@ -467,10 +588,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
R.dimen.small_profile_picture_size
}
val size = resources.getDimension(sizeID).roundToInt()
- binding.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
- binding.toolbarContent.profilePictureView.root.glide = glide
+ binding.toolbarContent.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
- val profilePictureView = binding.toolbarContent.profilePictureView.root
+ val profilePictureView = binding.toolbarContent.profilePictureView
viewModel.recipient?.let(profilePictureView::update)
}
@@ -576,7 +696,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
binding?.blockedBannerTextView?.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
binding?.blockedBanner?.isVisible = recipient.isBlocked
- binding?.blockedBanner?.setOnClickListener { viewModel.unblock(this@ConversationActivityV2) }
+ binding?.blockedBanner?.setOnClickListener { viewModel.unblock() }
}
private fun setUpLinkPreviewObserver() {
@@ -609,15 +729,37 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (uiState.isMessageRequestAccepted == true) {
binding?.messageRequestBar?.visibility = View.GONE
}
+ if (!uiState.conversationExists && !isFinishing) {
+ // Conversation should be deleted now, just go back
+ finish()
+ }
}
}
}
- private fun scrollToFirstUnreadMessageIfNeeded() {
+ private fun scrollToFirstUnreadMessageIfNeeded(isFirstLoad: Boolean = false, shouldHighlight: Boolean = false): Int {
val lastSeenTimestamp = threadDb.getLastSeenAndHasSent(viewModel.threadId).first()
- val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return
- if (lastSeenItemPosition <= 3) { return }
+ val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return -1
+
+ // If this is triggered when first opening a conversation then we want to position the top
+ // of the first unread message in the middle of the screen
+ if (isFirstLoad && !reverseMessageList) {
+ layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2))
+
+ if (shouldHighlight) { highlightViewAtPosition(lastSeenItemPosition) }
+
+ return lastSeenItemPosition
+ }
+
+ if (lastSeenItemPosition <= 3) { return lastSeenItemPosition }
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
+ return lastSeenItemPosition
+ }
+
+ private fun highlightViewAtPosition(position: Int) {
+ binding?.conversationRecyclerView?.post {
+ (layoutManager?.findViewByPosition(position) as? VisibleMessageView)?.playHighlight()
+ }
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
@@ -658,7 +800,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
updateSendAfterApprovalText()
showOrHideInputIfNeeded()
- binding?.toolbarContent?.profilePictureView?.root?.update(threadRecipient)
+ binding?.toolbarContent?.profilePictureView?.update(threadRecipient)
binding?.toolbarContent?.conversationTitleView?.text = when {
threadRecipient.isLocalNumber -> getString(R.string.note_to_self)
else -> threadRecipient.toShortString()
@@ -701,11 +843,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun acceptMessageRequest() {
binding?.messageRequestBar?.isVisible = false
- binding?.conversationRecyclerView?.layoutManager =
- LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
- adapter.notifyDataSetChanged()
viewModel.acceptMessageRequest()
- LoaderManager.getInstance(this).restartLoader(0, null, this)
+
lifecycleScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ConversationActivityV2)
}
@@ -903,17 +1042,60 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun handleRecyclerViewScrolled() {
- // FIXME: Checking isScrolledToBottom is a quick fix for an issue where the
- // typing indicator overlays the recycler view when scrolled up
val binding = binding ?: return
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
showScrollToBottomButtonIfApplicable()
- val firstVisiblePosition = layoutManager?.findFirstVisibleItemPosition() ?: -1
- unreadCount = min(unreadCount, firstVisiblePosition).coerceAtLeast(0)
+ val maybeTargetVisiblePosition = if (reverseMessageList) layoutManager?.findFirstVisibleItemPosition() else layoutManager?.findLastVisibleItemPosition()
+ val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION
+ if (!firstLoad.get() && targetVisiblePosition != RecyclerView.NO_POSITION) {
+ val visibleItemTimestamp = adapter.getTimestampForItemAt(targetVisiblePosition)
+ if (visibleItemTimestamp != null) {
+ bufferedLastSeenChannel.trySend(visibleItemTimestamp)
+ }
+ }
+
+ if (reverseMessageList) {
+ unreadCount = min(unreadCount, targetVisiblePosition).coerceAtLeast(0)
+ }
+ else {
+ val layoutUnreadCount = layoutManager?.let { (it.itemCount - 1) - it.findLastVisibleItemPosition() }
+ ?: RecyclerView.NO_POSITION
+ unreadCount = min(unreadCount, layoutUnreadCount).coerceAtLeast(0)
+ }
updateUnreadCountIndicator()
}
+ private fun updatePlaceholder() {
+ val recipient = viewModel.recipient
+ ?: return Log.w("Loki", "recipient was null in placeholder update")
+ val binding = binding ?: return
+ val openGroup = viewModel.openGroup
+ val (textResource, insertParam) = when {
+ recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
+ openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
+ else -> R.string.activity_conversation_empty_state_default to recipient.toShortString()
+ }
+ val showPlaceholder = adapter.itemCount == 0
+ binding.placeholderText.isVisible = showPlaceholder
+ if (showPlaceholder) {
+ if (insertParam != null) {
+ val span = getText(textResource) as SpannedString
+ val annotations = span.getSpans(0, span.length, StyleSpan::class.java)
+ val boldSpan = annotations.first()
+ val spannedParam = insertParam.toSpannable()
+ spannedParam[0 until spannedParam.length] = StyleSpan(boldSpan.style)
+ val originalStart = span.getSpanStart(boldSpan)
+ val originalEnd = span.getSpanEnd(boldSpan)
+ val newString = SpannableStringBuilder(span)
+ .replace(originalStart, originalEnd, spannedParam)
+ binding.placeholderText.text = newString
+ } else {
+ binding.placeholderText.setText(textResource)
+ }
+ }
+ }
+
private fun showScrollToBottomButtonIfApplicable() {
binding?.scrollToBottomButton?.isVisible = !emojiPickerVisible && !isScrolledToBottom && adapter.itemCount > 0
}
@@ -965,21 +1147,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun block(deleteThread: Boolean) {
- val title = R.string.RecipientPreferenceActivity_block_this_contact_question
- val message = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact
- val dialog = AlertDialog.Builder(this)
- .setTitle(title)
- .setMessage(message)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.RecipientPreferenceActivity_block) { _, _ ->
- viewModel.block(this@ConversationActivityV2)
+ showSessionDialog {
+ title(R.string.RecipientPreferenceActivity_block_this_contact_question)
+ text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
+ destructiveButton(R.string.RecipientPreferenceActivity_block, R.string.AccessibilityId_block_confirm) {
+ viewModel.block()
if (deleteThread) {
viewModel.deleteThread()
finish()
}
- }.show()
- val button = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
- button.setContentDescription("Confirm block")
+ }
+ cancelButton()
+ }
}
override fun copySessionID(sessionId: String) {
@@ -1006,28 +1185,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val group = groupDb.getGroup(thread.address.toGroupString()).orNull()
if (group?.isActive == false) { return }
}
- ExpirationDialog.show(this, thread.expireMessages) { expirationTime: Int ->
- recipientDb.setExpireMessages(thread, expirationTime)
+ showExpirationDialog(thread.expireMessages) { expirationTime ->
+ storage.setExpirationTimer(thread.address.serialize(), expirationTime)
val message = ExpirationTimerUpdate(expirationTime)
message.recipient = thread.address.serialize()
message.sentTimestamp = SnodeAPI.nowWithOffset
- val expiringMessageManager = ApplicationContext.getInstance(this).expiringMessageManager
- expiringMessageManager.setExpirationTimer(message)
+ ApplicationContext.getInstance(this).expiringMessageManager.setExpirationTimer(message)
MessageSender.send(message, thread.address)
invalidateOptionsMenu()
}
}
override fun unblock() {
- val title = R.string.ConversationActivity_unblock_this_contact_question
- val message = R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact
- AlertDialog.Builder(this)
- .setTitle(title)
- .setMessage(message)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.ConversationActivity_unblock) { _, _ ->
- viewModel.unblock(this@ConversationActivityV2)
- }.show()
+ showSessionDialog {
+ title(R.string.ConversationActivity_unblock_this_contact_question)
+ text(R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
+ destructiveButton(
+ R.string.ConversationActivity_unblock,
+ R.string.AccessibilityId_block_confirm
+ ) { viewModel.unblock() }
+ cancelButton()
+ }
}
// `position` is the adapter position; not the visual position
@@ -1371,11 +1549,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
return
}
val binding = binding ?: return
- if (binding.inputBar.linkPreview != null || binding.inputBar.quote != null) {
+ val sentMessageInfo = if (binding.inputBar.linkPreview != null || binding.inputBar.quote != null) {
sendAttachments(listOf(), getMessageBody(), binding.inputBar.quote, binding.inputBar.linkPreview)
} else {
sendTextOnlyMessage()
}
+
+ // Jump to the newly sent message once it gets added
+ if (sentMessageInfo != null) {
+ messageToScrollAuthor.set(sentMessageInfo.first)
+ messageToScrollTimestamp.set(sentMessageInfo.second)
+ }
}
override fun commitInputContent(contentUri: Uri) {
@@ -1393,19 +1577,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
- private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false) {
- val recipient = viewModel.recipient ?: return
+ private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false): Pair? {
+ val recipient = viewModel.recipient ?: return null
+ val sentTimestamp = SnodeAPI.nowWithOffset
processMessageRequestApproval()
val text = getMessageBody()
val userPublicKey = textSecurePreferences.getLocalNumber()
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")
+ dialog.show(supportFragmentManager, "Send Seed Dialog")
+ return null
}
// Create the message
val message = VisibleMessage()
- message.sentTimestamp = SnodeAPI.nowWithOffset
+ message.sentTimestamp = sentTimestamp
message.text = text
val outgoingTextMessage = OutgoingTextMessage.from(message, recipient)
// Clear the input bar
@@ -1422,14 +1608,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
MessageSender.send(message, recipient.address)
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
+ return Pair(recipient.address, sentTimestamp)
}
- private fun sendAttachments(attachments: List, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
- val recipient = viewModel.recipient ?: return
+ private fun sendAttachments(attachments: List, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null): Pair? {
+ val recipient = viewModel.recipient ?: return null
+ val sentTimestamp = SnodeAPI.nowWithOffset
processMessageRequestApproval()
// Create the message
val message = VisibleMessage()
- message.sentTimestamp = SnodeAPI.nowWithOffset
+ message.sentTimestamp = sentTimestamp
message.text = body
val quote = quotedMessage?.let {
val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf()
@@ -1463,28 +1651,28 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
MessageSender.send(message, recipient.address, attachments, quote, linkPreview)
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
+ return Pair(recipient.address, sentTimestamp)
}
private fun showGIFPicker() {
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
if (!hasSeenGIFMetaDataWarning) {
- val builder = AlertDialog.Builder(this)
- builder.setTitle(R.string.giphy_permission_title)
- builder.setMessage(R.string.giphy_permission_message)
- builder.setPositiveButton(R.string.continue_2) { dialog: DialogInterface, _: Int ->
- textSecurePreferences.setHasSeenGIFMetaDataWarning()
- AttachmentManager.selectGif(this, PICK_GIF)
- dialog.dismiss()
+ showSessionDialog {
+ title(R.string.giphy_permission_title)
+ text(R.string.giphy_permission_message)
+ button(R.string.continue_2) {
+ textSecurePreferences.setHasSeenGIFMetaDataWarning()
+ selectGif()
+ }
+ cancelButton()
}
- builder.setNegativeButton(R.string.cancel) { dialog: DialogInterface, _: Int ->
- dialog.dismiss()
- }
- builder.create().show()
} else {
- AttachmentManager.selectGif(this, PICK_GIF)
+ selectGif()
}
}
+ private fun selectGif() = AttachmentManager.selectGif(this, PICK_GIF)
+
private fun showDocumentPicker() {
AttachmentManager.selectDocument(this, PICK_DOCUMENT)
}
@@ -1584,7 +1772,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
showVoiceMessageUI()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
audioRecorder.startRecording()
- stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 60000) // Limit voice messages to 1 minute each
+ stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 300000) // Limit voice messages to 5 minute each
} else {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
@@ -1631,35 +1819,23 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id) != null }
if (recipient.isOpenGroupRecipient) {
val messageCount = 1
- val builder = AlertDialog.Builder(this)
- builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
- builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
- builder.setCancelable(true)
- builder.setPositiveButton(R.string.delete) { _, _ ->
- for (message in messages) {
- viewModel.deleteForEveryone(message)
- }
- endActionMode()
+
+ showSessionDialog {
+ title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
+ text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
+ button(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
+ cancelButton { endActionMode() }
}
- builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
- dialog.dismiss()
- endActionMode()
- }
- builder.show()
} else if (allSentByCurrentUser && allHasHash) {
val bottomSheet = DeleteOptionsBottomSheet()
bottomSheet.recipient = recipient
bottomSheet.onDeleteForMeTapped = {
- for (message in messages) {
- viewModel.deleteLocally(message)
- }
+ messages.forEach(viewModel::deleteLocally)
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.onDeleteForEveryoneTapped = {
- for (message in messages) {
- viewModel.deleteForEveryone(message)
- }
+ messages.forEach(viewModel::deleteForEveryone)
bottomSheet.dismiss()
endActionMode()
}
@@ -1670,54 +1846,32 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
} else {
val messageCount = 1
- val builder = AlertDialog.Builder(this)
- builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
- builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
- builder.setCancelable(true)
- builder.setPositiveButton(R.string.delete) { _, _ ->
- for (message in messages) {
- viewModel.deleteLocally(message)
- }
- endActionMode()
+
+ showSessionDialog {
+ title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
+ text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
+ button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
+ cancelButton(::endActionMode)
}
- builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
- dialog.dismiss()
- endActionMode()
- }
- builder.show()
}
}
override fun banUser(messages: Set) {
- val builder = AlertDialog.Builder(this)
- builder.setTitle(R.string.ConversationFragment_ban_selected_user)
- builder.setMessage("This will ban the selected user from this room. It won't ban them from other rooms.")
- builder.setCancelable(true)
- builder.setPositiveButton(R.string.ban) { _, _ ->
- viewModel.banUser(messages.first().individualRecipient)
- endActionMode()
+ showSessionDialog {
+ title(R.string.ConversationFragment_ban_selected_user)
+ text("This will ban the selected user from this room. It won't ban them from other rooms.")
+ button(R.string.ban) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
+ cancelButton(::endActionMode)
}
- builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
- dialog.dismiss()
- endActionMode()
- }
- builder.show()
}
override fun banAndDeleteAll(messages: Set) {
- val builder = AlertDialog.Builder(this)
- builder.setTitle(R.string.ConversationFragment_ban_selected_user)
- builder.setMessage("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
- builder.setCancelable(true)
- builder.setPositiveButton(R.string.ban) { _, _ ->
- viewModel.banAndDeleteAll(messages.first().individualRecipient)
- endActionMode()
+ showSessionDialog {
+ title(R.string.ConversationFragment_ban_selected_user)
+ text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
+ button(R.string.ban) { viewModel.banAndDeleteAll(messages.first().individualRecipient); endActionMode() }
+ cancelButton(::endActionMode)
}
- builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
- dialog.dismiss()
- endActionMode()
- }
- builder.show()
}
override fun copyMessages(messages: Set) {
@@ -1772,16 +1926,30 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
endActionMode()
}
+ private val handleMessageDetail = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ val message = result.data?.extras?.getLong(MESSAGE_TIMESTAMP)
+ ?.let(mmsSmsDb::getMessageForTimestamp)
+
+ val set = setOfNotNull(message)
+
+ when (result.resultCode) {
+ ON_REPLY -> reply(set)
+ ON_RESEND -> resendMessage(set)
+ ON_DELETE -> deleteMessages(set)
+ }
+ }
+
override fun showMessageDetail(messages: Set) {
- val intent = Intent(this, MessageDetailActivity::class.java)
- intent.putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, messages.first().timestamp)
- push(intent)
+ Intent(this, MessageDetailActivity::class.java)
+ .apply { putExtra(MESSAGE_TIMESTAMP, messages.first().timestamp) }
+ .let { handleMessageDetail.launch(it) }
+
endActionMode()
}
override fun saveAttachment(messages: Set) {
val message = messages.first() as MmsMessageRecord
- SaveAttachmentTask.showWarningDialog(this, { _, _ ->
+ SaveAttachmentTask.showWarningDialog(this) {
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P)
@@ -1809,12 +1977,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
Toast.LENGTH_LONG).show()
}
.execute()
- })
+ }
}
override fun reply(messages: Set) {
val recipient = viewModel.recipient ?: return
- binding?.inputBar?.draftQuote(recipient, messages.first(), glide)
+ messages.firstOrNull()?.let { binding?.inputBar?.draftQuote(recipient, it, glide) }
endActionMode()
}
@@ -1867,7 +2035,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (result == null) return@Observer
if (result.getResults().isNotEmpty()) {
result.getResults()[result.position]?.let {
- jumpToMessage(it.messageRecipient.address, it.sentTimestampMs) {
+ jumpToMessage(it.messageRecipient.address, it.sentTimestampMs, true) {
searchViewModel.onMissingResult() }
}
}
@@ -1904,15 +2072,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
this.searchViewModel.onMoveDown()
}
- private fun jumpToMessage(author: Address, timestamp: Long, onMessageNotFound: Runnable?) {
+ private fun jumpToMessage(author: Address, timestamp: Long, highlight: Boolean, onMessageNotFound: Runnable?) {
SimpleTask.run(lifecycle, {
- mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, timestamp, author)
- }) { p: Int -> moveToMessagePosition(p, onMessageNotFound) }
+ mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, timestamp, author, reverseMessageList)
+ }) { p: Int -> moveToMessagePosition(p, highlight, onMessageNotFound) }
}
- private fun moveToMessagePosition(position: Int, onMessageNotFound: Runnable?) {
+ private fun moveToMessagePosition(position: Int, highlight: Boolean, onMessageNotFound: Runnable?) {
if (position >= 0) {
binding?.conversationRecyclerView?.scrollToPosition(position)
+
+ if (highlight) {
+ runOnUiThread {
+ highlightViewAtPosition(position)
+ }
+ }
} else {
onMessageNotFound?.run()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
index 85d3c8e6de..6013af5ba4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.conversation.v2
-import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.database.Cursor
@@ -31,10 +30,15 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
+import org.thoughtcrime.securesms.showSessionDialog
+import java.util.concurrent.atomic.AtomicLong
+import kotlin.math.min
class ConversationAdapter(
context: Context,
cursor: Cursor,
+ originalLastSeen: Long,
+ private val isReversed: Boolean,
private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit,
private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit,
@@ -52,6 +56,8 @@ class ConversationAdapter(
private val updateQueue = Channel(1024, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val contactCache = SparseArray(100)
private val contactLoadedCache = SparseBooleanArray(100)
+ private val lastSeen = AtomicLong(originalLastSeen)
+
init {
lifecycleCoroutineScope.launch(IO) {
while (isActive) {
@@ -128,6 +134,7 @@ class ConversationAdapter(
searchQuery,
contact,
senderId,
+ lastSeen.get(),
visibleMessageViewDelegate,
onAttachmentNeedsDownload
)
@@ -146,17 +153,15 @@ class ConversationAdapter(
viewHolder.view.bind(message, messageBefore)
if (message.isCallLog && message.isFirstMissedCall) {
viewHolder.view.setOnClickListener {
- AlertDialog.Builder(context)
- .setTitle(R.string.CallNotificationBuilder_first_call_title)
- .setMessage(R.string.CallNotificationBuilder_first_call_message)
- .setPositiveButton(R.string.activity_settings_title) { _, _ ->
- val intent = Intent(context, PrivacySettingsActivity::class.java)
- context.startActivity(intent)
+ context.showSessionDialog {
+ title(R.string.CallNotificationBuilder_first_call_title)
+ text(R.string.CallNotificationBuilder_first_call_message)
+ button(R.string.activity_settings_title) {
+ Intent(context, PrivacySettingsActivity::class.java)
+ .let(context::startActivity)
}
- .setNeutralButton(R.string.cancel) { d, _ ->
- d.dismiss()
- }
- .show()
+ cancelButton()
+ }
}
} else {
viewHolder.view.setOnClickListener(null)
@@ -185,14 +190,18 @@ class ConversationAdapter(
private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? {
// The message that's visually before the current one is actually after the current
// one for the cursor because the layout is reversed
- if (!cursor.moveToPosition(position + 1)) { return null }
+ if (isReversed && !cursor.moveToPosition(position + 1)) { return null }
+ if (!isReversed && !cursor.moveToPosition(position - 1)) { return null }
+
return messageDB.readerFor(cursor).current
}
private fun getMessageAfter(position: Int, cursor: Cursor): MessageRecord? {
// The message that's visually after the current one is actually before the current
// one for the cursor because the layout is reversed
- if (!cursor.moveToPosition(position - 1)) { return null }
+ if (isReversed && !cursor.moveToPosition(position - 1)) { return null }
+ if (!isReversed && !cursor.moveToPosition(position + 1)) { return null }
+
return messageDB.readerFor(cursor).current
}
@@ -219,11 +228,30 @@ class ConversationAdapter(
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
val cursor = this.cursor
- if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null
+ if (cursor == null || !isActiveCursor) return null
+ if (lastSeenTimestamp == 0L) {
+ if (isReversed && cursor.moveToLast()) { return cursor.position }
+ if (!isReversed && cursor.moveToFirst()) { return cursor.position }
+ }
+
+ // Loop from the newest message to the oldest until we find one older (or equal to)
+ // the lastSeenTimestamp, then return that message index
for (i in 0 until itemCount) {
- cursor.moveToPosition(i)
- val message = messageDB.readerFor(cursor).current
- if (message.isOutgoing || message.dateReceived <= lastSeenTimestamp) { return i }
+ if (isReversed) {
+ cursor.moveToPosition(i)
+ val (outgoing, dateSent) = messageDB.timestampAndDirectionForCurrent(cursor)
+ if (outgoing || dateSent <= lastSeenTimestamp) {
+ return i
+ }
+ }
+ else {
+ val index = ((itemCount - 1) - i)
+ cursor.moveToPosition(index)
+ val (outgoing, dateSent) = messageDB.timestampAndDirectionForCurrent(cursor)
+ if (outgoing || dateSent <= lastSeenTimestamp) {
+ return min(itemCount - 1, (index + 1))
+ }
+ }
}
return null
}
@@ -233,8 +261,8 @@ class ConversationAdapter(
if (timestamp <= 0L || cursor == null || !isActiveCursor) return null
for (i in 0 until itemCount) {
cursor.moveToPosition(i)
- val message = messageDB.readerFor(cursor).current
- if (message.dateSent == timestamp) { return i }
+ val (_, dateSent) = messageDB.timestampAndDirectionForCurrent(cursor)
+ if (dateSent == timestamp) { return i }
}
return null
}
@@ -243,4 +271,11 @@ class ConversationAdapter(
this.searchQuery = query
notifyDataSetChanged()
}
+
+ fun getTimestampForItemAt(firstVisiblePosition: Int): Long? {
+ val cursor = this.cursor ?: return null
+ if (!cursor.moveToPosition(firstVisiblePosition)) return null
+ val message = messageDB.readerFor(cursor).current ?: return null
+ return message.timestamp
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java
index 20462bef34..eee8b5ecd5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java
@@ -695,9 +695,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.conversation_context__menu_ban_and_delete_all), () -> handleActionItemClicked(Action.BAN_AND_DELETE_ALL)));
}
// Message detail
- if (message.isFailed()) {
- items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
- }
+ items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
// Resend
if (message.isFailed()) {
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
index b8b460b603..13736974b1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
@@ -1,10 +1,10 @@
package org.thoughtcrime.securesms.conversation.v2
-import android.content.Context
+import android.content.ContentResolver
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
+import app.cash.copper.flow.observeQuery
import com.goterl.lazysodium.utils.KeyPair
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@@ -21,15 +21,16 @@ import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
-import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import java.util.UUID
class ConversationViewModel(
val threadId: Long,
val edKeyPair: KeyPair?,
+ private val contentResolver: ContentResolver,
private val repository: ConversationRepository,
private val storage: Storage
) : ViewModel() {
@@ -37,7 +38,7 @@ class ConversationViewModel(
val showSendAfterApprovalText: Boolean
get() = recipient?.run { isContactRecipient && !isLocalNumber && !hasApprovedMe() } ?: false
- private val _uiState = MutableStateFlow(ConversationUiState())
+ private val _uiState = MutableStateFlow(ConversationUiState(conversationExists = true))
val uiState: StateFlow = _uiState
private var _recipient: RetrieveOnce = RetrieveOnce {
@@ -61,6 +62,18 @@ class ConversationViewModel(
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
}
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId))
+ .collect {
+ val recipientExists = storage.getRecipientForThread(threadId) != null
+ if (!recipientExists && _uiState.value.conversationExists) {
+ _uiState.update { it.copy(conversationExists = false) }
+ }
+ }
+ }
+ }
+
fun saveDraft(text: String) {
GlobalScope.launch(Dispatchers.IO) {
repository.saveDraft(threadId, text)
@@ -81,27 +94,17 @@ class ConversationViewModel(
repository.inviteContacts(threadId, contacts)
}
- fun block(context: Context) {
+ fun block() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for block action")
if (recipient.isContactRecipient) {
repository.setBlocked(recipient, true)
-
- // TODO: Remove in UserConfig branch
- GlobalScope.launch(Dispatchers.IO) {
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
- }
}
}
- fun unblock(context: Context) {
+ fun unblock() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for unblock action")
if (recipient.isContactRecipient) {
repository.setBlocked(recipient, false)
-
- // TODO: Remove in UserConfig branch
- GlobalScope.launch(Dispatchers.IO) {
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
- }
}
}
@@ -198,19 +201,20 @@ class ConversationViewModel(
@dagger.assisted.AssistedFactory
interface AssistedFactory {
- fun create(threadId: Long, edKeyPair: KeyPair?): Factory
+ fun create(threadId: Long, edKeyPair: KeyPair?, contentResolver: ContentResolver): Factory
}
@Suppress("UNCHECKED_CAST")
class Factory @AssistedInject constructor(
@Assisted private val threadId: Long,
@Assisted private val edKeyPair: KeyPair?,
+ @Assisted private val contentResolver: ContentResolver,
private val repository: ConversationRepository,
private val storage: Storage
) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
- return ConversationViewModel(threadId, edKeyPair, repository, storage) as T
+ return ConversationViewModel(threadId, edKeyPair, contentResolver, repository, storage) as T
}
}
}
@@ -219,7 +223,8 @@ data class UiMessage(val id: Long, val message: String)
data class ConversationUiState(
val uiMessages: List = emptyList(),
- val isMessageRequestAccepted: Boolean? = null
+ val isMessageRequestAccepted: Boolean? = null,
+ val conversationExists: Boolean
)
data class RetrieveOnce(val retrieval: () -> T?) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
index 0938b21dd9..61732827f3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
@@ -1,99 +1,401 @@
package org.thoughtcrime.securesms.conversation.v2
+import android.annotation.SuppressLint
+import android.content.Intent
import android.os.Bundle
-import android.view.View
-import androidx.core.view.isVisible
+import android.view.LayoutInflater
+import android.view.MotionEvent.ACTION_UP
+import androidx.activity.viewModels
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalTextStyle
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.lifecycleScope
+import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
+import com.bumptech.glide.integration.compose.GlideImage
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
import network.loki.messenger.R
-import network.loki.messenger.databinding.ActivityMessageDetailBinding
-import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.messaging.open_groups.OpenGroupApi
-import org.session.libsession.messaging.utilities.SessionId
-import org.session.libsession.messaging.utilities.SodiumUtilities
-import org.session.libsession.snode.SnodeAPI
-import org.session.libsession.utilities.Address
-import org.session.libsession.utilities.ExpirationUtil
-import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsignal.utilities.IdPrefix
+import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
+import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
-import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
import org.thoughtcrime.securesms.database.Storage
-import org.thoughtcrime.securesms.database.model.MessageRecord
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent
-import org.thoughtcrime.securesms.util.DateUtils
-import java.text.SimpleDateFormat
-import java.util.*
+import org.thoughtcrime.securesms.ui.AppTheme
+import org.thoughtcrime.securesms.ui.Avatar
+import org.thoughtcrime.securesms.ui.CarouselNextButton
+import org.thoughtcrime.securesms.ui.CarouselPrevButton
+import org.thoughtcrime.securesms.ui.Cell
+import org.thoughtcrime.securesms.ui.CellNoMargin
+import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
+import org.thoughtcrime.securesms.ui.Divider
+import org.thoughtcrime.securesms.ui.GetString
+import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator
+import org.thoughtcrime.securesms.ui.ItemButton
+import org.thoughtcrime.securesms.ui.PreviewTheme
+import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
+import org.thoughtcrime.securesms.ui.TitledText
+import org.thoughtcrime.securesms.ui.blackAlpha40
+import org.thoughtcrime.securesms.ui.colorDestructive
+import org.thoughtcrime.securesms.ui.destructiveButtonColors
import javax.inject.Inject
@AndroidEntryPoint
-class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
- private lateinit var binding: ActivityMessageDetailBinding
- var messageRecord: MessageRecord? = null
+class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
@Inject
lateinit var storage: Storage
- // region Settings
+ private val viewModel: MessageDetailsViewModel by viewModels()
+
companion object {
// Extras
const val MESSAGE_TIMESTAMP = "message_timestamp"
+
+ const val ON_REPLY = 1
+ const val ON_RESEND = 2
+ const val ON_DELETE = 3
}
- // endregion
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
- binding = ActivityMessageDetailBinding.inflate(layoutInflater)
- setContentView(binding.root)
+
title = resources.getString(R.string.conversation_context__menu_message_details)
- val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
- // We only show this screen for messages fail to send,
- // so the author of the messages must be the current user.
- val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
- messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) ?: run {
- finish()
- return
- }
- val threadId = messageRecord!!.threadId
- val openGroup = storage.getOpenGroup(threadId)
- val blindedKey = openGroup?.let { group ->
- val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return@let null
- val blindingEnabled = storage.getServerCapabilities(group.server).contains(OpenGroupApi.Capability.BLIND.name.lowercase())
- if (blindingEnabled) {
- SodiumUtilities.blindedKeyPair(group.publicKey, userEdKeyPair)?.publicKey?.asBytes
- ?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
- } else null
- }
- updateContent()
- binding.resendButton.setOnClickListener {
- ResendMessageUtilities.resend(this, messageRecord!!, blindedKey)
- finish()
+
+ viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
+
+ ComposeView(this)
+ .apply { setContent { MessageDetailsScreen() } }
+ .let(::setContentView)
+
+ lifecycleScope.launch {
+ viewModel.eventFlow.collect {
+ when (it) {
+ Event.Finish -> finish()
+ is Event.StartMediaPreview -> startActivity(
+ getPreviewIntent(this@MessageDetailActivity, it.args)
+ )
+ }
+ }
}
}
- fun updateContent() {
- val dateLocale = Locale.getDefault()
- val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
- binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
-
- val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId())
- if (errorMessage != null) {
- binding.errorMessage.text = errorMessage
- binding.resendContainer.isVisible = true
- binding.errorContainer.isVisible = true
- } else {
- binding.errorContainer.isVisible = false
- binding.resendContainer.isVisible = false
- }
-
- if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
- binding.expiresContainer.visibility = View.GONE
- } else {
- binding.expiresContainer.visibility = View.VISIBLE
- val elapsed = SnodeAPI.nowWithOffset - messageRecord!!.expireStarted
- val remaining = messageRecord!!.expiresIn - elapsed
-
- val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
- binding.expiresIn.text = duration
+ @Composable
+ private fun MessageDetailsScreen() {
+ val state by viewModel.stateFlow.collectAsState()
+ AppTheme {
+ MessageDetails(
+ state = state,
+ onReply = { setResultAndFinish(ON_REPLY) },
+ onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
+ onDelete = { setResultAndFinish(ON_DELETE) },
+ onClickImage = { viewModel.onClickImage(it) },
+ onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
+ )
}
}
-}
\ No newline at end of file
+
+ private fun setResultAndFinish(code: Int) {
+ Bundle().apply { putLong(MESSAGE_TIMESTAMP, viewModel.timestamp) }
+ .let(Intent()::putExtras)
+ .let { setResult(code, it) }
+
+ finish()
+ }
+}
+
+@SuppressLint("ClickableViewAccessibility")
+@Composable
+fun MessageDetails(
+ state: MessageDetailsState,
+ onReply: () -> Unit = {},
+ onResend: (() -> Unit)? = null,
+ onDelete: () -> Unit = {},
+ onClickImage: (Int) -> Unit = {},
+ onAttachmentNeedsDownload: (Long, Long) -> Unit = { _, _ -> }
+) {
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(vertical = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ state.record?.let { message ->
+ AndroidView(
+ modifier = Modifier.padding(horizontal = 32.dp),
+ factory = {
+ ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply {
+ bind(
+ message,
+ thread = state.thread!!,
+ onAttachmentNeedsDownload = onAttachmentNeedsDownload,
+ suppressThumbnails = true
+ )
+
+ setOnTouchListener { _, event ->
+ if (event.actionMasked == ACTION_UP) onContentClick(event)
+ true
+ }
+ }
+ }
+ )
+ }
+ Carousel(state.imageAttachments) { onClickImage(it) }
+ state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
+ CellMetadata(state)
+ CellButtons(
+ onReply,
+ onResend,
+ onDelete,
+ )
+ }
+}
+
+@Composable
+fun CellMetadata(
+ state: MessageDetailsState,
+) {
+ state.apply {
+ if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return
+ CellWithPaddingAndMargin {
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ TitledText(sent)
+ TitledText(received)
+ TitledErrorText(error)
+ senderInfo?.let {
+ TitledView(state.fromTitle) {
+ Row {
+ sender?.let { Avatar(it) }
+ TitledMonospaceText(it)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun CellButtons(
+ onReply: () -> Unit = {},
+ onResend: (() -> Unit)? = null,
+ onDelete: () -> Unit = {},
+) {
+ Cell {
+ Column {
+ ItemButton(
+ stringResource(R.string.reply),
+ R.drawable.ic_message_details__reply,
+ onClick = onReply
+ )
+ Divider()
+ onResend?.let {
+ ItemButton(
+ stringResource(R.string.resend),
+ R.drawable.ic_message_details__refresh,
+ onClick = it
+ )
+ Divider()
+ }
+ ItemButton(
+ stringResource(R.string.delete),
+ R.drawable.ic_message_details__trash,
+ colors = destructiveButtonColors(),
+ onClick = onDelete
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun Carousel(attachments: List, onClick: (Int) -> Unit) {
+ if (attachments.isEmpty()) return
+
+ val pagerState = rememberPagerState { attachments.size }
+
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Row {
+ CarouselPrevButton(pagerState)
+ Box(modifier = Modifier.weight(1f)) {
+ CellCarousel(pagerState, attachments, onClick)
+ HorizontalPagerIndicator(pagerState)
+ ExpandButton(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(8.dp)
+ ) { onClick(pagerState.currentPage) }
+ }
+ CarouselNextButton(pagerState)
+ }
+ attachments.getOrNull(pagerState.currentPage)?.fileDetails?.let { FileDetails(it) }
+ }
+}
+
+@OptIn(
+ ExperimentalFoundationApi::class,
+ ExperimentalGlideComposeApi::class
+)
+@Composable
+private fun CellCarousel(
+ pagerState: PagerState,
+ attachments: List,
+ onClick: (Int) -> Unit
+) {
+ CellNoMargin {
+ HorizontalPager(state = pagerState) { i ->
+ GlideImage(
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .aspectRatio(1f)
+ .clickable { onClick(i) },
+ model = attachments[i].uri,
+ contentDescription = attachments[i].fileName ?: stringResource(id = R.string.image)
+ )
+ }
+ }
+}
+
+@Composable
+fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
+ Surface(
+ shape = CircleShape,
+ color = blackAlpha40,
+ modifier = modifier,
+ contentColor = Color.White,
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_expand),
+ contentDescription = stringResource(id = R.string.expand),
+ modifier = Modifier.clickable { onClick() },
+ )
+ }
+}
+
+
+@Preview
+@Composable
+fun PreviewMessageDetails(
+ @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
+) {
+ PreviewTheme(themeResId) {
+ MessageDetails(
+ state = MessageDetailsState(
+ nonImageAttachmentFileDetails = listOf(
+ TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
+ TitledText(R.string.message_details_header__file_type, "image/png"),
+ TitledText(R.string.message_details_header__file_size, "195.6kB"),
+ TitledText(R.string.message_details_header__resolution, "342x312"),
+ ),
+ sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"),
+ received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"),
+ error = TitledText(R.string.message_details_header__error, "Message failed to send"),
+ senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
+ )
+ )
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun FileDetails(fileDetails: List) {
+ if (fileDetails.isEmpty()) return
+
+ CellWithPaddingAndMargin(padding = 0.dp) {
+ FlowRow(
+ modifier = Modifier.padding(vertical = 24.dp, horizontal = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ fileDetails.forEach {
+ BoxWithConstraints {
+ TitledText(
+ it,
+ modifier = Modifier
+ .widthIn(min = maxWidth.div(2))
+ .padding(horizontal = 12.dp)
+ .width(IntrinsicSize.Max)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TitledErrorText(titledText: TitledText?) {
+ TitledText(
+ titledText,
+ valueStyle = LocalTextStyle.current.copy(color = colorDestructive)
+ )
+}
+
+@Composable
+fun TitledMonospaceText(titledText: TitledText?) {
+ TitledText(
+ titledText,
+ valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
+ )
+}
+
+@Composable
+fun TitledText(
+ titledText: TitledText?,
+ modifier: Modifier = Modifier,
+ valueStyle: TextStyle = LocalTextStyle.current,
+) {
+ titledText?.apply {
+ TitledView(title, modifier) {
+ Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth())
+ }
+ }
+}
+
+@Composable
+fun TitledView(title: GetString, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+ Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Title(title)
+ content()
+ }
+}
+
+@Composable
+fun Title(title: GetString) {
+ Text(title.string(), fontWeight = FontWeight.Bold)
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt
new file mode 100644
index 0000000000..a73fe41139
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt
@@ -0,0 +1,159 @@
+package org.thoughtcrime.securesms.conversation.v2
+
+import android.net.Uri
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import network.loki.messenger.R
+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.Util
+import org.session.libsession.utilities.recipients.Recipient
+import org.thoughtcrime.securesms.MediaPreviewArgs
+import org.thoughtcrime.securesms.database.AttachmentDatabase
+import org.thoughtcrime.securesms.database.LokiMessageDatabase
+import org.thoughtcrime.securesms.database.MmsSmsDatabase
+import org.thoughtcrime.securesms.database.ThreadDatabase
+import org.thoughtcrime.securesms.database.model.MessageRecord
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord
+import org.thoughtcrime.securesms.mms.ImageSlide
+import org.thoughtcrime.securesms.mms.Slide
+import org.thoughtcrime.securesms.ui.GetString
+import org.thoughtcrime.securesms.ui.TitledText
+import java.util.Date
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+@HiltViewModel
+class MessageDetailsViewModel @Inject constructor(
+ private val attachmentDb: AttachmentDatabase,
+ private val lokiMessageDatabase: LokiMessageDatabase,
+ private val mmsSmsDatabase: MmsSmsDatabase,
+ private val threadDb: ThreadDatabase,
+) : ViewModel() {
+
+ private val state = MutableStateFlow(MessageDetailsState())
+ val stateFlow = state.asStateFlow()
+
+ private val event = Channel()
+ val eventFlow = event.receiveAsFlow()
+
+ var timestamp: Long = 0L
+ set(value) {
+ field = value
+ val record = mmsSmsDatabase.getMessageForTimestamp(timestamp)
+
+ if (record == null) {
+ viewModelScope.launch { event.send(Event.Finish) }
+ return
+ }
+
+ val mmsRecord = record as? MmsMessageRecord
+
+ state.value = record.run {
+ val slides = mmsRecord?.slideDeck?.slides ?: emptyList()
+
+ MessageDetailsState(
+ attachments = slides.map(::Attachment),
+ record = record,
+ sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) },
+ received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) },
+ error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) },
+ senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
+ sender = individualRecipient,
+ thread = threadDb.getRecipientForThreadId(threadId)!!,
+ )
+ }
+ }
+
+ private val Slide.details: List
+ get() = listOfNotNull(
+ fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) },
+ TitledText(R.string.message_details_header__file_type, asAttachment().contentType),
+ TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)),
+ takeIf { it is ImageSlide }
+ ?.let(Slide::asAttachment)
+ ?.run { "${width}x$height" }
+ ?.let { TitledText(R.string.message_details_header__resolution, it) },
+ attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) },
+ )
+
+ private fun AttachmentDatabase.duration(slide: Slide): String? =
+ slide.takeIf { it.hasAudio() }
+ ?.run { asAttachment() as? DatabaseAttachment }
+ ?.run { getAttachmentAudioExtras(attachmentId)?.durationMs }
+ ?.takeIf { it > 0 }
+ ?.let {
+ String.format(
+ "%01d:%02d",
+ TimeUnit.MILLISECONDS.toMinutes(it),
+ TimeUnit.MILLISECONDS.toSeconds(it) % 60
+ )
+ }
+
+ fun Attachment(slide: Slide): Attachment =
+ Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide)
+
+ fun onClickImage(index: Int) {
+ val state = state.value ?: return
+ val mmsRecord = state.mmsRecord ?: return
+ val slide = mmsRecord.slideDeck.slides[index] ?: return
+ // only open to downloaded images
+ if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) {
+ // Restart download here (on IO thread)
+ (slide.asAttachment() as? DatabaseAttachment)?.let { attachment ->
+ onAttachmentNeedsDownload(attachment.attachmentId.rowId, state.mmsRecord.getId())
+ }
+ }
+
+ if (slide.isInProgress) return
+
+ viewModelScope.launch {
+ MediaPreviewArgs(slide, state.mmsRecord, state.thread)
+ .let(Event::StartMediaPreview)
+ .let { event.send(it) }
+ }
+ }
+
+ fun onAttachmentNeedsDownload(attachmentId: Long, mmsId: Long) {
+ viewModelScope.launch(Dispatchers.IO) {
+ JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
+ }
+ }
+}
+
+data class MessageDetailsState(
+ val attachments: List = emptyList(),
+ val imageAttachments: List = attachments.filter { it.hasImage },
+ val nonImageAttachmentFileDetails: List? = attachments.firstOrNull { !it.hasImage }?.fileDetails,
+ val record: MessageRecord? = null,
+ val mmsRecord: MmsMessageRecord? = record as? MmsMessageRecord,
+ val sent: TitledText? = null,
+ val received: TitledText? = null,
+ val error: TitledText? = null,
+ val senderInfo: TitledText? = null,
+ val sender: Recipient? = null,
+ val thread: Recipient? = null,
+) {
+ val fromTitle = GetString(R.string.message_details_header__from)
+}
+
+data class Attachment(
+ val fileDetails: List,
+ val fileName: String?,
+ val uri: Uri?,
+ val hasImage: Boolean
+)
+
+sealed class Event {
+ object Finish: Event()
+ data class StartMediaPreview(val args: MediaPreviewArgs): Event()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt
index 834b77eccb..d544263915 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt
@@ -28,11 +28,10 @@ class MentionCandidateView : LinearLayout {
private fun update() = with(binding) {
mentionCandidateNameTextView.text = mentionCandidate.displayName
- profilePictureView.root.publicKey = mentionCandidate.publicKey
- profilePictureView.root.displayName = mentionCandidate.displayName
- profilePictureView.root.additionalPublicKey = null
- profilePictureView.root.glide = glide!!
- profilePictureView.root.update()
+ profilePictureView.publicKey = mentionCandidate.publicKey
+ profilePictureView.displayName = mentionCandidate.displayName
+ profilePictureView.additionalPublicKey = null
+ profilePictureView.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt
index bcabca98f1..c0ff1cbb1d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt
@@ -1,51 +1,42 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
+import android.app.Dialog
import android.content.Context
import android.graphics.Typeface
+import android.os.Bundle
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
-import android.view.LayoutInflater
-import androidx.appcompat.app.AlertDialog
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
+import androidx.fragment.app.DialogFragment
import network.loki.messenger.R
-import network.loki.messenger.databinding.DialogBlockedBinding
+import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.recipients.Recipient
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
+import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
-import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
/** Shown upon sending a message to a user that's blocked. */
-class BlockedDialog(private val recipient: Recipient, private val context: Context) : BaseDialog() {
+class BlockedDialog(private val recipient: Recipient, private val context: Context) : DialogFragment() {
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogBlockedBinding.inflate(LayoutInflater.from(requireContext()))
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
val sessionID = recipient.address.toString()
val contact = contactDB.getContactWithSessionID(sessionID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
- val title = resources.getString(R.string.dialog_blocked_title, name)
- binding.blockedTitleTextView.text = title
+
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- binding.blockedExplanationTextView.text = spannable
- binding.cancelButton.setOnClickListener { dismiss() }
- binding.unblockButton.setOnClickListener { unblock() }
- builder.setView(binding.root)
+
+ title(resources.getString(R.string.dialog_blocked_title, name))
+ text(spannable)
+ button(R.string.ConversationActivity_unblock) { unblock() }
+ cancelButton { dismiss() }
}
private fun unblock() {
- DatabaseComponent.get(requireContext()).recipientDatabase().setBlocked(recipient, false)
+ MessagingModuleConfiguration.shared.storage.setBlocked(listOf(recipient), false)
dismiss()
-
- // TODO: Remove in UserConfig branch
- GlobalScope.launch(Dispatchers.IO) {
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
- }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt
index 42cca1ad3e..5edd63f100 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt
@@ -1,19 +1,19 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
+import android.app.Dialog
import android.graphics.Typeface
+import android.os.Bundle
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
-import android.view.LayoutInflater
-import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
-import network.loki.messenger.databinding.DialogDownloadBinding
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.utilities.recipients.Recipient
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
+import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import javax.inject.Inject
@@ -21,25 +21,24 @@ import javax.inject.Inject
/** Shown when receiving media from a contact for the first time, to confirm that
* they are to be trusted and files sent by them are to be downloaded. */
@AndroidEntryPoint
-class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
+class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
@Inject lateinit var contactDB: SessionContactDatabase
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogDownloadBinding.inflate(LayoutInflater.from(requireContext()))
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
val sessionID = recipient.address.toString()
val contact = contactDB.getContactWithSessionID(sessionID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
- val title = resources.getString(R.string.dialog_download_title, name)
- binding.downloadTitleTextView.text = title
+ title(resources.getString(R.string.dialog_download_title, name))
+
val explanation = resources.getString(R.string.dialog_download_explanation, name)
val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- binding.downloadExplanationTextView.text = spannable
- binding.cancelButton.setOnClickListener { dismiss() }
- binding.downloadButton.setOnClickListener { trust() }
- builder.setView(binding.root)
+ text(spannable)
+
+ button(R.string.dialog_download_button_title, R.string.AccessibilityId_download_media) { trust() }
+ cancelButton { dismiss() }
}
private fun trust() {
@@ -50,4 +49,4 @@ class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
JobQueue.shared.resumePendingJobs(AttachmentDownloadJob.KEY)
dismiss()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt
index 444c389e04..a886e89192 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt
@@ -1,46 +1,42 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
+import android.app.Dialog
import android.graphics.Typeface
+import android.os.Bundle
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
-import android.view.LayoutInflater
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.DialogFragment
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
+import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
/** Shown upon tapping an open group invitation. */
-class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() {
+class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() {
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogJoinOpenGroupBinding.inflate(LayoutInflater.from(requireContext()))
- val title = resources.getString(R.string.dialog_join_open_group_title, name)
- binding.joinOpenGroupTitleTextView.text = title
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
+ title(resources.getString(R.string.dialog_join_open_group_title, name))
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- binding.joinOpenGroupExplanationTextView.text = spannable
- binding.cancelButton.setOnClickListener { dismiss() }
- binding.joinButton.setOnClickListener { join() }
- builder.setView(binding.root)
+ text(spannable)
+ cancelButton { dismiss() }
+ button(R.string.open_group_invitation_view__join_accessibility_description) { join() }
}
private fun join() {
val openGroup = OpenGroupUrlParser.parseUrl(url)
- val activity = requireContext() as AppCompatActivity
+ val activity = requireActivity()
ThreadUtils.queue {
try {
- OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
- MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server)
+ openGroup.apply { OpenGroupManager.add(server, room, serverPublicKey, activity) }
+ MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
} catch (e: Exception) {
Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
@@ -48,4 +44,4 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : B
}
dismiss()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt
index a16ca86f79..996dd41f94 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt
@@ -1,20 +1,21 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
-import android.view.LayoutInflater
-import androidx.appcompat.app.AlertDialog
-import network.loki.messenger.databinding.DialogLinkPreviewBinding
+import android.app.Dialog
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
+import org.thoughtcrime.securesms.createSessionDialog
/** Shown the first time the user inputs a URL that could generate a link preview, to
* let them know that Session offers the ability to send and receive link previews. */
-class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
+class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() {
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogLinkPreviewBinding.inflate(LayoutInflater.from(requireContext()))
- binding.cancelButton.setOnClickListener { dismiss() }
- binding.enableLinkPreviewsButton.setOnClickListener { enable() }
- builder.setView(binding.root)
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
+ title(R.string.dialog_link_preview_title)
+ text(R.string.dialog_link_preview_explanation)
+ button(R.string.dialog_link_preview_enable_button_title) { enable() }
+ cancelButton { dismiss() }
}
private fun enable() {
@@ -22,4 +23,4 @@ class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
dismiss()
onEnabled()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt
index f51261d499..6abb0814d6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt
@@ -1,22 +1,23 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
-import android.view.LayoutInflater
-import androidx.appcompat.app.AlertDialog
-import network.loki.messenger.databinding.DialogSendSeedBinding
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
+import android.app.Dialog
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.createSessionDialog
/** Shown if the user is about to send their recovery phrase to someone. */
-class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() {
+class SendSeedDialog(private val proceed: (() -> Unit)? = null) : DialogFragment() {
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogSendSeedBinding.inflate(LayoutInflater.from(requireContext()))
- binding.cancelButton.setOnClickListener { dismiss() }
- binding.sendSeedButton.setOnClickListener { send() }
- builder.setView(binding.root)
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
+ title(R.string.dialog_send_seed_title)
+ text(R.string.dialog_send_seed_explanation)
+ button(R.string.dialog_send_seed_send_button_title) { send() }
+ cancelButton()
}
private fun send() {
proceed?.invoke()
dismiss()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt
index a21ba1b502..2d8f745967 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt
@@ -28,11 +28,10 @@ class MentionCandidateView : RelativeLayout {
private fun update() = with(binding) {
mentionCandidateNameTextView.text = candidate.displayName
- profilePictureView.root.publicKey = candidate.publicKey
- profilePictureView.root.displayName = candidate.displayName
- profilePictureView.root.additionalPublicKey = null
- profilePictureView.root.glide = glide!!
- profilePictureView.root.update()
+ profilePictureView.publicKey = candidate.publicKey
+ profilePictureView.displayName = candidate.displayName
+ profilePictureView.additionalPublicKey = null
+ profilePictureView.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", candidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
index f86920f90f..3746aa52e4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
@@ -67,7 +67,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
(thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
// Message detail
- menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing)
+ menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1
// Resend
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
// Resync
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
index ce29efa3a0..02ee4ae45f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.menus
import android.annotation.SuppressLint
import android.content.Context
-import android.content.DialogInterface
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.PorterDuff
@@ -15,7 +14,6 @@ import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt
-import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.widget.SearchView
@@ -34,7 +32,6 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.MediaOverviewActivity
-import org.thoughtcrime.securesms.MuteDialog
import org.thoughtcrime.securesms.ShortcutLauncherActivity
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
@@ -45,6 +42,8 @@ import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
import org.thoughtcrime.securesms.service.WebRtcCallService
+import org.thoughtcrime.securesms.showSessionDialog
+import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException
@@ -64,17 +63,18 @@ object ConversationMenuHelper {
// Base menu (options that should always be present)
inflater.inflate(R.menu.menu_conversation, menu)
// Expiring messages
- if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient)) {
+ if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient) && !thread.isBlocked) {
if (thread.expireMessages > 0) {
inflater.inflate(R.menu.menu_conversation_expiration_on, menu)
val item = menu.findItem(R.id.menu_expiring_messages)
- val actionView = item.actionView
- val iconView = actionView.findViewById(R.id.menu_badge_icon)
- val badgeView = actionView.findViewById(R.id.expiration_badge)
- @ColorInt val color = context.getColorFromAttr(android.R.attr.textColorPrimary)
- iconView.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)
- badgeView.text = ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, thread.expireMessages)
- actionView.setOnClickListener { onOptionsItemSelected(item) }
+ item.actionView?.let { actionView ->
+ val iconView = actionView.findViewById(R.id.menu_badge_icon)
+ val badgeView = actionView.findViewById(R.id.expiration_badge)
+ @ColorInt val color = context.getColorFromAttr(android.R.attr.textColorPrimary)
+ iconView.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)
+ badgeView.text = ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, thread.expireMessages)
+ actionView.setOnClickListener { onOptionsItemSelected(item) }
+ }
} else {
inflater.inflate(R.menu.menu_conversation_expiration_off, menu)
}
@@ -87,7 +87,7 @@ object ConversationMenuHelper {
if (thread.isContactRecipient) {
if (thread.isBlocked) {
inflater.inflate(R.menu.menu_conversation_unblock, menu)
- } else {
+ } else if (!thread.isLocalNumber) {
inflater.inflate(R.menu.menu_conversation_block, menu)
}
}
@@ -186,29 +186,23 @@ object ConversationMenuHelper {
private fun call(context: Context, thread: Recipient) {
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
- val dialog = AlertDialog.Builder(context)
- .setTitle(R.string.ConversationActivity_call_title)
- .setMessage(R.string.ConversationActivity_call_prompt)
- .setPositiveButton(R.string.activity_settings_title) { _, _ ->
- val intent = Intent(context, PrivacySettingsActivity::class.java)
- context.startActivity(intent)
+ context.showSessionDialog {
+ title(R.string.ConversationActivity_call_title)
+ text(R.string.ConversationActivity_call_prompt)
+ button(R.string.activity_settings_title, R.string.AccessibilityId_settings) {
+ Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity)
}
- .setNeutralButton(R.string.cancel) { d, _ ->
- d.dismiss()
- }.create()
- dialog.getButton(DialogInterface.BUTTON_POSITIVE)?.contentDescription = context.getString(R.string.AccessibilityId_settings)
- dialog.getButton(DialogInterface.BUTTON_NEGATIVE)?.contentDescription = context.getString(R.string.AccessibilityId_cancel_button)
- dialog.show()
+ cancelButton()
+ }
return
}
- val service = WebRtcCallService.createCall(context, thread)
- context.startService(service)
+ WebRtcCallService.createCall(context, thread)
+ .let(context::startService)
- val activity = Intent(context, WebRtcCallActivity::class.java).apply {
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
- }
- context.startActivity(activity)
+ Intent(context, WebRtcCallActivity::class.java)
+ .apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
+ .let(context::startActivity)
}
@@ -295,9 +289,7 @@ object ConversationMenuHelper {
private fun leaveClosedGroup(context: Context, thread: Recipient) {
if (!thread.isClosedGroupRecipient) { return }
- val builder = AlertDialog.Builder(context)
- builder.setTitle(context.resources.getString(R.string.ConversationActivity_leave_group))
- builder.setCancelable(true)
+
val group = DatabaseComponent.get(context).groupDatabase().getGroup(thread.address.toGroupString()).orNull()
val admins = group.admins
val sessionID = TextSecurePreferences.getLocalNumber(context)
@@ -307,29 +299,25 @@ object ConversationMenuHelper {
} else {
context.resources.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)
}
- builder.setMessage(message)
- builder.setPositiveButton(R.string.yes) { _, _ ->
- var groupPublicKey: String?
- var isClosedGroup: Boolean
- try {
- groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
- isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
- } catch (e: IOException) {
- groupPublicKey = null
- isClosedGroup = false
- }
- try {
- if (isClosedGroup) {
- MessageSender.leave(groupPublicKey!!, true)
- } else {
- Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
+
+ fun onLeaveFailed() = Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
+
+ context.showSessionDialog {
+ title(R.string.ConversationActivity_leave_group)
+ text(message)
+ button(R.string.yes) {
+ try {
+ val groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
+ val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
+
+ if (isClosedGroup) MessageSender.leave(groupPublicKey, notifyUser = false)
+ else onLeaveFailed()
+ } catch (e: Exception) {
+ onLeaveFailed()
}
- } catch (e: Exception) {
- Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
}
+ button(R.string.no)
}
- builder.setNegativeButton(R.string.no, null)
- builder.show()
}
private fun inviteContacts(context: Context, thread: Recipient) {
@@ -344,7 +332,7 @@ object ConversationMenuHelper {
}
private fun mute(context: Context, thread: Recipient) {
- MuteDialog.show(ContextThemeWrapper(context, context.theme)) { until: Long ->
+ showMuteDialog(ContextThemeWrapper(context, context.theme)) { until ->
DatabaseComponent.get(context).recipientDatabase().setMuted(thread, until)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt
index 75a3c58752..c812d0f731 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
-import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
@@ -15,9 +14,7 @@ import android.view.View
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.content.res.ResourcesCompat
-import androidx.core.graphics.BlendModeColorFilterCompat
-import androidx.core.graphics.BlendModeCompat
+import androidx.core.graphics.ColorUtils
import androidx.core.text.getSpans
import androidx.core.text.toSpannable
import androidx.core.view.children
@@ -28,6 +25,7 @@ import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration
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.getColorFromAttr
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
@@ -38,15 +36,16 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getInt
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
+import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
+import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.getAccentColor
-import java.util.*
+import java.util.Locale
import kotlin.math.roundToInt
class VisibleMessageContentView : ConstraintLayout {
private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) }
- var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
var onContentDoubleTap: (() -> Unit)? = null
var delegate: VisibleMessageViewDelegate? = null
var indexInAdapter: Int = -1
@@ -60,21 +59,20 @@ class VisibleMessageContentView : ConstraintLayout {
// region Updating
fun bind(
message: MessageRecord,
- isStartOfMessageCluster: Boolean,
- isEndOfMessageCluster: Boolean,
- glide: GlideRequests,
+ isStartOfMessageCluster: Boolean = true,
+ isEndOfMessageCluster: Boolean = true,
+ glide: GlideRequests = GlideApp.with(this),
thread: Recipient,
- searchQuery: String?,
- contactIsTrusted: Boolean,
- onAttachmentNeedsDownload: (Long, Long) -> Unit
+ searchQuery: String? = null,
+ contactIsTrusted: Boolean = true,
+ onAttachmentNeedsDownload: (Long, Long) -> Unit,
+ suppressThumbnails: Boolean = false
) {
// Background
- val background = getBackground(message.isOutgoing)
val color = if (message.isOutgoing) context.getAccentColor()
else context.getColorFromAttr(R.attr.message_received_background_color)
- val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
- background.colorFilter = filter
- binding.contentParent.background = background
+ binding.contentParent.mainColor = color
+ binding.contentParent.cornerRadius = resources.getDimension(R.dimen.message_corner_radius)
val onlyBodyMessage = message is SmsMessageRecord
val mediaThumbnailMessage = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.thumbnailSlide != null
@@ -131,7 +129,6 @@ class VisibleMessageContentView : ConstraintLayout {
delegate?.scrollToMessageIfPossible(quote.id)
}
}
- val hasMedia = message.slideDeck.asAttachments().isNotEmpty()
}
if (message is MmsMessageRecord) {
@@ -188,7 +185,7 @@ class VisibleMessageContentView : ConstraintLayout {
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
}
}
- message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty() -> {
+ message is MmsMessageRecord && !suppressThumbnails && message.slideDeck.asAttachments().isNotEmpty() -> {
/*
* Images / Video attachment
*/
@@ -241,14 +238,15 @@ class VisibleMessageContentView : ConstraintLayout {
binding.contentParent.layoutParams = layoutParams
}
+ private val onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
+
+ fun onContentClick(event: MotionEvent) {
+ onContentClick.forEach { clickHandler -> clickHandler.invoke(event) }
+ }
+
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
listOf(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }
- private fun getBackground(isOutgoing: Boolean): Drawable {
- val backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
- return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!!
- }
-
fun recycle() {
arrayOf(
binding.deletedMessageView.root,
@@ -266,6 +264,15 @@ class VisibleMessageContentView : ConstraintLayout {
fun playVoiceMessage() {
binding.voiceMessageView.root.togglePlayback()
}
+
+ fun playHighlight() {
+ // Show the highlight colour immediately then slowly fade out
+ val targetColor = if (ThemeUtil.isDarkTheme(context)) context.getAccentColor() else resources.getColor(R.color.black, context.theme)
+ val clearTargetColor = ColorUtils.setAlphaComponent(targetColor, 0)
+ binding.contentParent.numShadowRenders = if (ThemeUtil.isDarkTheme(context)) 3 else 1
+ binding.contentParent.sessionShadowColor = targetColor
+ GlowViewUtilities.animateShadowColorChange(binding.contentParent, targetColor, clearTargetColor, 1600)
+ }
// endregion
// region Convenience
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
index 319140731a..9538148fd0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
@@ -2,16 +2,17 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.content.Intent
-import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Rect
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.MotionEvent
import android.view.View
+import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
@@ -46,6 +47,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
+import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.disableClipping
@@ -70,7 +72,6 @@ class VisibleMessageView : LinearLayout {
@Inject lateinit var mmsDb: MmsDatabase
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()
private var dx = 0.0f
@@ -111,7 +112,10 @@ class VisibleMessageView : LinearLayout {
private fun initialize() {
isHapticFeedbackEnabled = true
setWillNotDraw(false)
+ binding.root.disableClipping()
+ binding.mainContainer.disableClipping()
binding.messageInnerContainer.disableClipping()
+ binding.messageInnerLayout.disableClipping()
binding.messageContentView.root.disableClipping()
}
// endregion
@@ -119,13 +123,14 @@ class VisibleMessageView : LinearLayout {
// region Updating
fun bind(
message: MessageRecord,
- previous: MessageRecord?,
- next: MessageRecord?,
- glide: GlideRequests,
- searchQuery: String?,
- contact: Contact?,
+ previous: MessageRecord? = null,
+ next: MessageRecord? = null,
+ glide: GlideRequests = GlideApp.with(this),
+ searchQuery: String? = null,
+ contact: Contact? = null,
senderSessionID: String,
- delegate: VisibleMessageViewDelegate?,
+ lastSeen: Long,
+ delegate: VisibleMessageViewDelegate? = null,
onAttachmentNeedsDownload: (Long, Long) -> Unit
) {
val threadID = message.threadId
@@ -136,7 +141,7 @@ class VisibleMessageView : LinearLayout {
// 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 {
+ binding.profilePictureView.visibility = when {
thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE
thread.isGroupRecipient -> View.INVISIBLE
else -> View.GONE
@@ -145,25 +150,25 @@ class VisibleMessageView : LinearLayout {
val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing)
else ViewUtil.dpToPx(context,2)
- if (binding.profilePictureView.root.visibility == View.GONE) {
+ if (binding.profilePictureView.visibility == View.GONE) {
val expirationParams = binding.messageInnerContainer.layoutParams as MarginLayoutParams
expirationParams.bottomMargin = bottomMargin
binding.messageInnerContainer.layoutParams = expirationParams
} else {
- val avatarLayoutParams = binding.profilePictureView.root.layoutParams as MarginLayoutParams
+ val avatarLayoutParams = binding.profilePictureView.layoutParams as MarginLayoutParams
avatarLayoutParams.bottomMargin = bottomMargin
- binding.profilePictureView.root.layoutParams = avatarLayoutParams
+ binding.profilePictureView.layoutParams = avatarLayoutParams
}
if (isGroupThread && !message.isOutgoing) {
if (isEndOfMessageCluster) {
- binding.profilePictureView.root.publicKey = senderSessionID
- binding.profilePictureView.root.glide = glide
- binding.profilePictureView.root.update(message.individualRecipient)
- binding.profilePictureView.root.setOnClickListener {
+ binding.profilePictureView.publicKey = senderSessionID
+ binding.profilePictureView.update(message.individualRecipient)
+ binding.profilePictureView.setOnClickListener {
if (thread.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
+ // TODO: support v2 soon
val intent = Intent(context, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderSessionID))
@@ -177,7 +182,7 @@ class VisibleMessageView : LinearLayout {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
var standardPublicKey = ""
var blindedPublicKey: String? = null
- if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED) {
+ if (IdPrefix.fromValue(senderSessionID)?.isBlinded() == true) {
blindedPublicKey = senderSessionID
} else {
standardPublicKey = senderSessionID
@@ -191,6 +196,8 @@ class VisibleMessageView : LinearLayout {
val contactContext =
if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
+ // Unread marker
+ binding.unreadMarkerContainer.isVisible = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing
// Date break
val showDateBreak = isStartOfMessageCluster || snIsSelected
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
@@ -336,11 +343,14 @@ class VisibleMessageView : LinearLayout {
private fun updateExpirationTimer(message: MessageRecord) {
val container = binding.messageInnerContainer
- val content = binding.messageContentView.root
- val expiration = binding.expirationTimerView
- container.removeAllViewsInLayout()
- container.addView(if (message.isOutgoing) expiration else content)
- container.addView(if (message.isOutgoing) content else expiration)
+ val layout = binding.messageInnerLayout
+
+ if (message.isOutgoing) binding.messageContentView.root.bringToFront()
+ else binding.expirationTimerView.bringToFront()
+
+ layout.layoutParams = layout.layoutParams.let { it as FrameLayout.LayoutParams }
+ .apply { gravity = if (message.isOutgoing) Gravity.END else Gravity.START }
+
val containerParams = container.layoutParams as ConstraintLayout.LayoutParams
containerParams.horizontalBias = if (message.isOutgoing) 1f else 0f
container.layoutParams = containerParams
@@ -386,7 +396,7 @@ class VisibleMessageView : LinearLayout {
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
val iconSize = toPx(24, context.resources)
val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing
- val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2)
+ val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.marginBottom - (iconSize / 2)
val right = left + iconSize
val bottom = top + iconSize
swipeToReplyIconRect.left = left
@@ -406,9 +416,13 @@ class VisibleMessageView : LinearLayout {
}
fun recycle() {
- binding.profilePictureView.root.recycle()
+ binding.profilePictureView.recycle()
binding.messageContentView.root.recycle()
}
+
+ fun playHighlight() {
+ binding.messageContentView.root.playHighlight()
+ }
// endregion
// region Interaction
@@ -503,7 +517,7 @@ class VisibleMessageView : LinearLayout {
}
fun onContentClick(event: MotionEvent) {
- binding.messageContentView.root.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
+ binding.messageContentView.root.onContentClick(event)
}
private fun onPress(event: MotionEvent) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt
index e1bf92c5f2..2b829af152 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt
@@ -92,7 +92,7 @@ class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener {
if (progress == 1.0) {
togglePlayback()
handleProgressChanged(0.0)
- delegate?.playVoiceMessageAtIndexIfPossible(indexInAdapter - 1)
+ delegate?.playVoiceMessageAtIndexIfPossible(indexInAdapter + 1)
} else {
handleProgressChanged(progress)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java
index dd90b699e3..088685241c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java
@@ -25,6 +25,7 @@ import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.util.Pair;
@@ -244,12 +245,17 @@ public class AttachmentManager {
}
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
- Permissions.with(activity)
- .request(Manifest.permission.READ_EXTERNAL_STORAGE)
- .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
- .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
- .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
- .execute();
+ Permissions.PermissionsBuilder builder = Permissions.with(activity);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
+ .request(Manifest.permission.READ_MEDIA_IMAGES);
+ } else {
+ builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
+ }
+ builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
+ .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
+ .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
+ .execute();
}
public static void selectAudio(Activity activity, int requestCode) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/BaseDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/BaseDialog.kt
deleted file mode 100644
index c3a9689a00..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/BaseDialog.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.thoughtcrime.securesms.conversation.v2.utilities
-
-import android.app.Dialog
-import android.graphics.Color
-import android.graphics.drawable.ColorDrawable
-import android.os.Bundle
-import androidx.appcompat.app.AlertDialog
-import androidx.fragment.app.DialogFragment
-import org.thoughtcrime.securesms.util.UiModeUtilities
-
-open class BaseDialog : DialogFragment() {
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val builder = AlertDialog.Builder(requireContext())
- setContentView(builder)
- val result = builder.create()
- result.window?.setDimAmount(0.6f)
- return result
- }
-
- open fun setContentView(builder: AlertDialog.Builder) {
- // To be overridden by subclasses
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt
index dbbcfb51ef..c0ce83f631 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt
@@ -1,21 +1,18 @@
package org.thoughtcrime.securesms.conversation.v2.utilities
import android.content.Context
-import androidx.appcompat.app.AlertDialog
import network.loki.messenger.R
import org.session.libsession.utilities.recipients.Recipient
+import org.thoughtcrime.securesms.showSessionDialog
object NotificationUtils {
fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) {
- val notifyTypes = context.resources.getStringArray(R.array.notify_types)
- val currentSelected = thread.notifyType
-
- AlertDialog.Builder(context)
- .setSingleChoiceItems(notifyTypes,currentSelected) { d, newSelection ->
- notifyTypeHandler(newSelection)
- d.dismiss()
- }
- .setTitle(R.string.RecipientPreferenceActivity_notification_settings)
- .show()
+ context.showSessionDialog {
+ title(R.string.RecipientPreferenceActivity_notification_settings)
+ singleChoiceItems(
+ context.resources.getStringArray(R.array.notify_types),
+ thread.notifyType
+ ) { notifyTypeHandler(it) }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt
new file mode 100644
index 0000000000..19a511bfd6
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt
@@ -0,0 +1,53 @@
+package org.thoughtcrime.securesms.database
+
+import android.content.Context
+import androidx.core.content.contentValuesOf
+import androidx.core.database.getBlobOrNull
+import androidx.core.database.getLongOrNull
+import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
+
+class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) {
+
+ companion object {
+ private const val VARIANT = "variant"
+ private const val PUBKEY = "publicKey"
+ private const val DATA = "data"
+ private const val TIMESTAMP = "timestamp" // Milliseconds
+
+ private const val TABLE_NAME = "configs_table"
+
+ const val CREATE_CONFIG_TABLE_COMMAND =
+ "CREATE TABLE $TABLE_NAME ($VARIANT TEXT NOT NULL, $PUBKEY TEXT NOT NULL, $DATA BLOB, $TIMESTAMP INTEGER NOT NULL DEFAULT 0, PRIMARY KEY($VARIANT, $PUBKEY));"
+
+ private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?"
+ }
+
+ fun storeConfig(variant: String, publicKey: String, data: ByteArray, timestamp: Long) {
+ val db = writableDatabase
+ val contentValues = contentValuesOf(
+ VARIANT to variant,
+ PUBKEY to publicKey,
+ DATA to data,
+ TIMESTAMP to timestamp
+ )
+ db.insertOrUpdate(TABLE_NAME, contentValues, VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey))
+ }
+
+ fun retrieveConfigAndHashes(variant: String, publicKey: String): ByteArray? {
+ val db = readableDatabase
+ val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
+ return query?.use { cursor ->
+ if (!cursor.moveToFirst()) return@use null
+ val bytes = cursor.getBlobOrNull(cursor.getColumnIndex(DATA)) ?: return@use null
+ bytes
+ }
+ }
+
+ fun retrieveConfigLastUpdateTimestamp(variant: String, publicKey: String): Long {
+ val db = readableDatabase
+ val cursor = db.query(TABLE_NAME, arrayOf(TIMESTAMP), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
+ if (cursor == null) return 0
+ if (!cursor.moveToFirst()) return 0
+ return (cursor.getLongOrNull(cursor.getColumnIndex(TIMESTAMP)) ?: 0)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java
index 79adead57e..66d01114ef 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java
@@ -36,9 +36,9 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
@SuppressWarnings("unused")
private static final String TAG = GroupDatabase.class.getSimpleName();
- static final String TABLE_NAME = "groups";
+ public static final String TABLE_NAME = "groups";
private static final String ID = "_id";
- static final String GROUP_ID = "group_id";
+ public static final String GROUP_ID = "group_id";
private static final String TITLE = "title";
private static final String MEMBERS = "members";
private static final String ZOMBIE_MEMBERS = "zombie_members";
@@ -133,12 +133,12 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
return new Reader(cursor);
}
- public List getAllGroups() {
+ public List getAllGroups(boolean includeInactive) {
Reader reader = getGroups();
GroupRecord record;
List groups = new LinkedList<>();
while ((record = reader.getNext()) != null) {
- if (record.isActive()) { groups.add(record); }
+ if (record.isActive() || includeInactive) { groups.add(record); }
}
reader.close();
return groups;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt
index b0f6a676c7..53f4ea3196 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt
@@ -458,9 +458,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize().removingIdPrefixIfNeeded()), DjbECPrivateKey(keyPair.privateKey.serialize()))
}
- fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
+ fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) {
val database = databaseHelper.writableDatabase
- val timestamp = Date().time.toString()
val index = "$groupPublicKey-$timestamp"
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removingIdPrefixIfNeeded()
val encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt
index 300217faba..1cbbf34c9c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt
@@ -4,11 +4,8 @@ import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import org.session.libsession.messaging.open_groups.OpenGroup
-import org.session.libsession.utilities.Address
-import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.JsonUtil
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
@@ -24,12 +21,6 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
}
- fun getThreadID(hexEncodedPublicKey: String): Long {
- val address = Address.fromSerialized(hexEncodedPublicKey)
- val recipient = Recipient.from(context, address, false)
- return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
- }
-
fun getAllOpenGroups(): Map {
val database = databaseHelper.readableDatabase
var cursor: Cursor? = null
@@ -61,6 +52,13 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
}
}
+ fun getThreadId(openGroup: OpenGroup): Long? {
+ val database = databaseHelper.readableDatabase
+ return database.get(publicChatTable, "$publicChat = ?", arrayOf(JsonUtil.toJson(openGroup.toJson()))) { cursor ->
+ cursor.getLong(threadID)
+ }
+ }
+
fun setOpenGroupChat(openGroup: OpenGroup, threadID: Long) {
if (threadID < 0) {
return
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
index e8f65dae06..111b6d5365 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
@@ -20,13 +20,11 @@ import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import com.annimon.stream.Stream
-import com.google.android.mms.pdu_alt.NotificationInd
import com.google.android.mms.pdu_alt.PduHeaders
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
-import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage
@@ -41,16 +39,13 @@ import org.session.libsession.utilities.Address.Companion.UNKNOWN
import org.session.libsession.utilities.Address.Companion.fromExternal
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.Contact
-import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID
import org.session.libsession.utilities.IdentityKeyMismatch
import org.session.libsession.utilities.IdentityKeyMismatchList
import org.session.libsession.utilities.NetworkFailure
import org.session.libsession.utilities.NetworkFailureList
import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled
import org.session.libsession.utilities.Util.toIsoBytes
-import org.session.libsession.utilities.Util.toIsoString
import org.session.libsession.utilities.recipients.Recipient
-import org.session.libsession.utilities.recipients.RecipientFormattingException
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils.queue
@@ -162,7 +157,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
)
get(context).groupReceiptDatabase()
.update(ourAddress, id, status, timestamp)
- get(context).threadDatabase().update(threadId, false)
+ get(context).threadDatabase().update(threadId, false, true)
notifyConversationListeners(threadId)
}
}
@@ -205,25 +200,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
}
}
- @Throws(RecipientFormattingException::class, MmsException::class)
- private fun getThreadIdFor(retrieved: IncomingMediaMessage): Long {
- return if (retrieved.groupId != null) {
- val groupRecipients = Recipient.from(
- context,
- retrieved.groupId,
- true
- )
- get(context).threadDatabase().getOrCreateThreadIdFor(groupRecipients)
- } else {
- val sender = Recipient.from(
- context,
- retrieved.from,
- true
- )
- get(context).threadDatabase().getOrCreateThreadIdFor(sender)
- }
- }
-
private fun rawQuery(where: String, arguments: Array?): Cursor {
val database = databaseHelper.readableDatabase
return database.rawQuery(
@@ -259,7 +235,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
" WHERE " + ID + " = ?", arrayOf(id.toString() + "")
)
if (threadId.isPresent) {
- get(context).threadDatabase().update(threadId.get(), false)
+ get(context).threadDatabase().update(threadId.get(), false, true)
}
}
@@ -316,10 +292,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
val attachmentDatabase = get(context).attachmentDatabase()
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(messageId) })
val threadId = getThreadIdForMessage(messageId)
- if (!read) {
- val mentionChange = if (hasMention) { 1 } else { 0 }
- get(context).threadDatabase().decrementUnread(threadId, 1, mentionChange)
- }
+
markAs(messageId, MmsSmsColumns.Types.BASE_DELETED_TYPE, threadId)
}
@@ -343,6 +316,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(id.toString()))
}
+ fun setMessagesRead(threadId: Long, beforeTime: Long): List {
+ return setMessagesRead(
+ THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?",
+ arrayOf(threadId.toString(), beforeTime.toString())
+ )
+ }
+
fun setMessagesRead(threadId: Long): List {
return setMessagesRead(
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)",
@@ -567,18 +547,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
contentLocation: String,
threadId: Long, mailbox: Long,
serverTimestamp: Long,
- runIncrement: Boolean,
runThreadUpdate: Boolean
): Optional {
- var threadId = threadId
- if (threadId == -1L || retrieved.isGroupMessage) {
- try {
- threadId = getThreadIdFor(retrieved)
- } catch (e: RecipientFormattingException) {
- Log.w("MmsDatabase", e)
- if (threadId == -1L) throw MmsException(e)
- }
- }
+ if (threadId < 0 ) throw MmsException("No thread ID supplied!")
val contentValues = ContentValues()
contentValues.put(DATE_SENT, retrieved.sentTimeMillis)
contentValues.put(ADDRESS, retrieved.from.serialize())
@@ -632,12 +603,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
null,
)
if (!MmsSmsColumns.Types.isExpirationTimerUpdate(mailbox)) {
- if (runIncrement) {
- val mentionAmount = if (retrieved.hasMention()) { 1 } else { 0 }
- get(context).threadDatabase().incrementUnread(threadId, 1, mentionAmount)
- }
if (runThreadUpdate) {
- get(context).threadDatabase().update(threadId, true)
+ get(context).threadDatabase().update(threadId, true, true)
}
}
notifyConversationListeners(threadId)
@@ -651,27 +618,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
serverTimestamp: Long,
runThreadUpdate: Boolean
): Optional {
- var threadId = threadId
- if (threadId == -1L) {
- if (retrieved.isGroup) {
- val decodedGroupId: String = if (retrieved is OutgoingExpirationUpdateMessage) {
- retrieved.groupId
- } else {
- (retrieved as OutgoingGroupMediaMessage).groupId
- }
- val groupId: String
- groupId = try {
- doubleEncodeGroupID(decodedGroupId)
- } catch (e: IOException) {
- Log.e(TAG, "Couldn't encrypt group ID")
- throw MmsException(e)
- }
- val group = Recipient.from(context, fromSerialized(groupId), false)
- threadId = get(context).threadDatabase().getOrCreateThreadIdFor(group)
- } else {
- threadId = get(context).threadDatabase().getOrCreateThreadIdFor(retrieved.recipient)
- }
- }
+ if (threadId < 0 ) throw MmsException("No thread ID supplied!")
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
if (messageId == -1L) {
return Optional.absent()
@@ -686,7 +633,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
retrieved: IncomingMediaMessage,
threadId: Long,
serverTimestamp: Long = 0,
- runIncrement: Boolean,
runThreadUpdate: Boolean
): Optional {
var type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT
@@ -705,7 +651,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
if (retrieved.isMessageRequestResponse) {
type = type or MmsSmsColumns.Types.MESSAGE_REQUEST_RESPONSE_BIT
}
- return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runIncrement, runThreadUpdate)
+ return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runThreadUpdate)
}
@JvmOverloads
@@ -794,10 +740,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
)
}
with (get(context).threadDatabase()) {
- setLastSeen(threadId)
+ val lastSeen = getLastSeenAndHasSent(threadId).first()
+ if (lastSeen < message.sentTimeMillis) {
+ setLastSeen(threadId, message.sentTimeMillis)
+ }
setHasSent(threadId, true)
if (runThreadUpdate) {
- update(threadId, true)
+ update(threadId, true, true)
}
}
return messageId
@@ -932,7 +881,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
groupReceiptDatabase.deleteRowsForMessage(messageId)
val database = databaseHelper.writableDatabase
database!!.delete(TABLE_NAME, ID_WHERE, arrayOf(messageId.toString()))
- val threadDeleted = get(context).threadDatabase().update(threadId, false)
+ val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
notifyConversationListeners(threadId)
notifyStickerListeners()
notifyStickerPackListeners()
@@ -949,7 +898,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
val database = databaseHelper.writableDatabase
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
- val threadDeleted = get(context).threadDatabase().update(threadId, false)
+ val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
notifyConversationListeners(threadId)
notifyStickerListeners()
notifyStickerPackListeners()
@@ -1147,7 +1096,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
}
val threadDb = get(context).threadDatabase()
for (threadId in threadIds) {
- val threadDeleted = threadDb.update(threadId, false)
+ val threadDeleted = threadDb.update(threadId, false, true)
notifyConversationListeners(threadId)
}
notifyStickerListeners()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index c7f9d61324..0db4dd00e5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -16,6 +16,8 @@
*/
package org.thoughtcrime.securesms.database;
+import static org.thoughtcrime.securesms.database.MmsDatabase.MESSAGE_BOX;
+
import android.content.Context;
import android.database.Cursor;
@@ -25,6 +27,7 @@ import androidx.annotation.Nullable;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
+import org.jetbrains.annotations.NotNull;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.Util;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
@@ -36,6 +39,8 @@ import java.io.Closeable;
import java.util.HashSet;
import java.util.Set;
+import kotlin.Pair;
+
public class MmsSmsDatabase extends Database {
@SuppressWarnings("unused")
@@ -259,8 +264,8 @@ public class MmsSmsDatabase extends Database {
return -1;
}
- public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address) {
- String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
+ public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address, boolean reverse) {
+ String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC");
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.ADDRESS }, selection, order, null)) {
@@ -512,6 +517,23 @@ public class MmsSmsDatabase extends Database {
return new Reader(cursor);
}
+ @NotNull
+ public Pair timestampAndDirectionForCurrent(@NotNull Cursor cursor) {
+ int sentColumn = cursor.getColumnIndex(MmsSmsColumns.NORMALIZED_DATE_SENT);
+ String msgType = cursor.getString(cursor.getColumnIndexOrThrow(TRANSPORT));
+ long sentTime = cursor.getLong(sentColumn);
+ long type = 0;
+ if (MmsSmsDatabase.MMS_TRANSPORT.equals(msgType)) {
+ int typeIndex = cursor.getColumnIndex(MESSAGE_BOX);
+ type = cursor.getLong(typeIndex);
+ } else if (MmsSmsDatabase.SMS_TRANSPORT.equals(msgType)) {
+ int typeIndex = cursor.getColumnIndex(SmsDatabase.TYPE);
+ type = cursor.getLong(typeIndex);
+ }
+
+ return new Pair(MmsSmsColumns.Types.isOutgoingMessageType(type), sentTime);
+ }
+
public class Reader implements Closeable {
private final Cursor cursor;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
index e3570fd283..b7b8364184 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
@@ -62,13 +62,14 @@ public class RecipientDatabase extends Database {
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none
+ private static final String WRAPPER_HASH = "wrapper_hash";
private static final String[] RECIPIENT_PROJECTION = new String[] {
BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED,
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
- FORCE_SMS_SELECTION, NOTIFY_TYPE,
+ FORCE_SMS_SELECTION, NOTIFY_TYPE, WRAPPER_HASH
};
static final List TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
@@ -136,6 +137,11 @@ public class RecipientDatabase extends Database {
"OR "+ADDRESS+" IN (SELECT "+GroupDatabase.TABLE_NAME+"."+GroupDatabase.ADMINS+" FROM "+GroupDatabase.TABLE_NAME+")))";
}
+ public static String getAddWrapperHash() {
+ return "ALTER TABLE "+TABLE_NAME+" "+
+ "ADD COLUMN "+WRAPPER_HASH+" TEXT DEFAULT NULL;";
+ }
+
public static final int NOTIFY_TYPE_ALL = 0;
public static final int NOTIFY_TYPE_MENTIONS = 1;
public static final int NOTIFY_TYPE_NONE = 2;
@@ -154,18 +160,14 @@ public class RecipientDatabase extends Database {
public Optional getRecipientSettings(@NonNull Address address) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
- Cursor cursor = null;
- try {
- cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[] {address.serialize()}, null, null, null);
+ try (Cursor cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[]{address.serialize()}, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
return getRecipientSettings(cursor);
}
return Optional.absent();
- } finally {
- if (cursor != null) cursor.close();
}
}
@@ -194,6 +196,7 @@ public class RecipientDatabase extends Database {
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
+ String wrapperHash = cursor.getString(cursor.getColumnIndexOrThrow(WRAPPER_HASH));
MaterialColor color;
byte[] profileKey = null;
@@ -225,7 +228,7 @@ public class RecipientDatabase extends Database {
systemPhoneLabel, systemContactUri,
signalProfileName, signalProfileAvatar, profileSharing,
notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
- forceSmsSelection));
+ forceSmsSelection, wrapperHash));
}
public void setColor(@NonNull Recipient recipient, @NonNull MaterialColor color) {
@@ -252,6 +255,24 @@ public class RecipientDatabase extends Database {
notifyRecipientListeners();
}
+ public boolean getApproved(@NonNull Address address) {
+ SQLiteDatabase db = getReadableDatabase();
+ try (Cursor cursor = db.query(TABLE_NAME, new String[]{APPROVED}, ADDRESS + " = ?", new String[]{address.serialize()}, null, null, null)) {
+ if (cursor != null && cursor.moveToNext()) {
+ return cursor.getInt(cursor.getColumnIndexOrThrow(APPROVED)) == 1;
+ }
+ }
+ return false;
+ }
+
+ public void setRecipientHash(@NonNull Recipient recipient, String recipientHash) {
+ ContentValues values = new ContentValues();
+ values.put(WRAPPER_HASH, recipientHash);
+ updateOrInsert(recipient.getAddress(), values);
+ recipient.resolve().setWrapperHash(recipientHash);
+ notifyRecipientListeners();
+ }
+
public void setApproved(@NonNull Recipient recipient, boolean approved) {
ContentValues values = new ContentValues();
values.put(APPROVED, approved ? 1 : 0);
@@ -268,14 +289,6 @@ public class RecipientDatabase extends Database {
notifyRecipientListeners();
}
- public void setBlocked(@NonNull Recipient recipient, boolean blocked) {
- ContentValues values = new ContentValues();
- values.put(BLOCK, blocked ? 1 : 0);
- updateOrInsert(recipient.getAddress(), values);
- recipient.resolve().setBlocked(blocked);
- notifyRecipientListeners();
- }
-
public void setBlocked(@NonNull Iterable recipients, boolean blocked) {
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt
index 40eee97428..49a6339368 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt
@@ -2,10 +2,12 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
-import androidx.core.database.getStringOrNull
import android.database.Cursor
+import androidx.core.database.getStringOrNull
import org.session.libsession.messaging.contacts.Contact
+import org.session.libsession.messaging.utilities.SessionId
import org.session.libsignal.utilities.Base64
+import org.session.libsignal.utilities.IdPrefix
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
@@ -43,6 +45,9 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
val database = databaseHelper.readableDatabase
return database.getAll(sessionContactTable, null, null) { cursor ->
contactFromCursor(cursor)
+ }.filter { contact ->
+ val sessionId = SessionId(contact.sessionID)
+ sessionId.prefix == IdPrefix.STANDARD
}.toSet()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt
index b081fb007e..6221446aae 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt
@@ -93,6 +93,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
fun cancelPendingMessageSendJobs(threadID: Long) {
val database = databaseHelper.writableDatabase
val attachmentUploadJobKeys = mutableListOf()
+ database.beginTransaction()
database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor ->
val job = jobFromCursor(cursor) as AttachmentUploadJob?
if (job != null && job.threadID == threadID.toString()) { attachmentUploadJobKeys.add(job.id!!) }
@@ -103,15 +104,19 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
if (job != null && job.message.threadID == threadID) { messageSendJobKeys.add(job.id!!) }
}
if (attachmentUploadJobKeys.isNotEmpty()) {
- val attachmentUploadJobKeysAsString = attachmentUploadJobKeys.joinToString(", ")
- database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)",
- arrayOf( AttachmentUploadJob.KEY, attachmentUploadJobKeysAsString ))
+ attachmentUploadJobKeys.forEach {
+ database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} = ?",
+ arrayOf( AttachmentUploadJob.KEY, it ))
+ }
}
if (messageSendJobKeys.isNotEmpty()) {
- val messageSendJobKeysAsString = messageSendJobKeys.joinToString(", ")
- database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)",
- arrayOf( MessageSendJob.KEY, messageSendJobKeysAsString ))
+ messageSendJobKeys.forEach {
+ database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} = ?",
+ arrayOf( MessageSendJob.KEY, it ))
+ }
}
+ database.setTransactionSuccessful()
+ database.endTransaction()
}
fun isJobCanceled(job: Job): Boolean {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
index 42a00ccbb2..4ef576f404 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -148,7 +148,7 @@ public class SmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(id);
- DatabaseComponent.get(context).threadDatabase().update(threadId, false);
+ DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
notifyConversationListeners(threadId);
}
@@ -234,10 +234,6 @@ public class SmsDatabase extends MessagingDatabase {
contentValues.put(BODY, "");
contentValues.put(HAS_MENTION, 0);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
- long threadId = getThreadIdForMessage(messageId);
- if (!read) {
- DatabaseComponent.get(context).threadDatabase().decrementUnread(threadId, 1, (hasMention ? 1 : 0));
- }
updateTypeBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_DELETED_TYPE);
}
@@ -256,7 +252,7 @@ public class SmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(id);
- DatabaseComponent.get(context).threadDatabase().update(threadId, false);
+ DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
notifyConversationListeners(threadId);
}
@@ -319,7 +315,7 @@ public class SmsDatabase extends MessagingDatabase {
ID + " = ?",
new String[] {String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))});
- DatabaseComponent.get(context).threadDatabase().update(threadId, false);
+ DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
notifyConversationListeners(threadId);
foundMessage = true;
}
@@ -337,6 +333,9 @@ public class SmsDatabase extends MessagingDatabase {
}
}
+ public List setMessagesRead(long threadId, long beforeTime) {
+ return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?", new String[]{threadId+"", beforeTime+""});
+ }
public List setMessagesRead(long threadId) {
return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)", new String[] {String.valueOf(threadId)});
}
@@ -400,14 +399,14 @@ public class SmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(messageId);
- DatabaseComponent.get(context).threadDatabase().update(threadId, true);
+ DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
notifyConversationListeners(threadId);
notifyConversationListListeners();
return new Pair<>(messageId, threadId);
}
- protected Optional insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
+ protected Optional insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runThreadUpdate) {
if (message.isSecureMessage()) {
type |= Types.SECURE_MESSAGE_BIT;
} else if (message.isGroup()) {
@@ -486,12 +485,8 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, null, values);
- if (unread && runIncrement) {
- DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1, (message.hasMention() ? 1 : 0));
- }
-
if (runThreadUpdate) {
- DatabaseComponent.get(context).threadDatabase().update(threadId, true);
+ DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
}
if (message.getSubscriptionId() != -1) {
@@ -504,16 +499,16 @@ public class SmsDatabase extends MessagingDatabase {
}
}
- public Optional insertMessageInbox(IncomingTextMessage message, boolean runIncrement, boolean runThreadUpdate) {
- return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runIncrement, runThreadUpdate);
+ public Optional insertMessageInbox(IncomingTextMessage message, boolean runThreadUpdate) {
+ return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runThreadUpdate);
}
public Optional insertCallMessage(IncomingTextMessage message) {
- return insertMessageInbox(message, 0, 0, true, true);
+ return insertMessageInbox(message, 0, 0, true);
}
- public Optional insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
- return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runIncrement, runThreadUpdate);
+ public Optional insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runThreadUpdate) {
+ return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runThreadUpdate);
}
public Optional insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp, boolean runThreadUpdate) {
@@ -567,9 +562,12 @@ public class SmsDatabase extends MessagingDatabase {
}
if (runThreadUpdate) {
- DatabaseComponent.get(context).threadDatabase().update(threadId, true);
+ DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
+ }
+ long lastSeen = DatabaseComponent.get(context).threadDatabase().getLastSeenAndHasSent(threadId).first();
+ if (lastSeen < message.getSentTimestampMillis()) {
+ DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId, message.getSentTimestampMillis());
}
- DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId);
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true);
@@ -616,7 +614,7 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long threadId = getThreadIdForMessage(messageId);
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
- boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
+ boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
notifyConversationListeners(threadId);
return threadDeleted;
}
@@ -640,7 +638,7 @@ public class SmsDatabase extends MessagingDatabase {
ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
argValues
);
- boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
+ boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
notifyConversationListeners(threadId);
return threadDeleted;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
index 365c12b839..c77ad1c638 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
@@ -2,16 +2,43 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
+import network.loki.messenger.libsession_util.ConfigBase
+import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
+import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED
+import network.loki.messenger.libsession_util.Contacts
+import network.loki.messenger.libsession_util.ConversationVolatileConfig
+import network.loki.messenger.libsession_util.UserGroupsConfig
+import network.loki.messenger.libsession_util.UserProfile
+import network.loki.messenger.libsession_util.util.BaseCommunityInfo
+import network.loki.messenger.libsession_util.util.Conversation
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import network.loki.messenger.libsession_util.util.GroupInfo
+import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.BlindedIdMapping
import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.contacts.Contact
-import org.session.libsession.messaging.jobs.*
+import org.session.libsession.messaging.jobs.AttachmentUploadJob
+import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
+import org.session.libsession.messaging.jobs.ConfigurationSyncJob
+import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
+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.RetrieveProfileAvatarJob
+import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse
-import org.session.libsession.messaging.messages.signal.*
+import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage
+import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
+import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
+import org.session.libsession.messaging.messages.signal.IncomingTextMessage
+import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
+import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
+import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.Profile
import org.session.libsession.messaging.messages.visible.Reaction
@@ -23,12 +50,15 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
+import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
+import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.snode.OnionRequestAPI
-import org.session.libsession.utilities.*
+import org.session.libsession.snode.SnodeAPI
+import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.GroupUtil
@@ -36,24 +66,104 @@ import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
+import org.session.libsignal.crypto.ecc.DjbECPrivateKey
+import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
+import org.session.libsignal.utilities.Base64
+import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.KeyHelper
+import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.ReactionRecord
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import org.thoughtcrime.securesms.groups.ClosedGroupManager
+import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.groups.OpenGroupManager
-import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.mms.PartAuthority
+import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.SessionMetaProtocol
import java.security.MessageDigest
+import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
+
+open class Storage(context: Context, helper: SQLCipherOpenHelper, private val configFactory: ConfigFactory) : Database(context, helper), StorageProtocol,
+ ThreadDatabase.ConversationThreadUpdateListener {
+
+ override fun threadCreated(address: Address, threadId: Long) {
+ val localUserAddress = getUserPublicKey() ?: return
+ if (!getRecipientApproved(address) && localUserAddress != address.serialize()) return // don't store unapproved / message requests
+
+ val volatile = configFactory.convoVolatile ?: return
+ if (address.isGroup) {
+ val groups = configFactory.userGroups ?: return
+ if (address.isClosedGroup) {
+ val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
+ val closedGroup = getGroup(address.toGroupString())
+ if (closedGroup != null && closedGroup.isActive) {
+ val legacyGroup = groups.getOrConstructLegacyGroupInfo(sessionId)
+ groups.set(legacyGroup)
+ val newVolatileParams = volatile.getOrConstructLegacyGroup(sessionId).copy(
+ lastRead = SnodeAPI.nowWithOffset,
+ )
+ volatile.set(newVolatileParams)
+ }
+ } else if (address.isOpenGroup) {
+ // these should be added on the group join / group info fetch
+ Log.w("Loki", "Thread created called for open group address, not adding any extra information")
+ }
+ } else if (address.isContact) {
+ // non-standard contact prefixes: 15, 00 etc shouldn't be stored in config
+ if (SessionId(address.serialize()).prefix != IdPrefix.STANDARD) return
+ // don't update our own address into the contacts DB
+ if (getUserPublicKey() != address.serialize()) {
+ val contacts = configFactory.contacts ?: return
+ contacts.upsertContact(address.serialize()) {
+ priority = ConfigBase.PRIORITY_VISIBLE
+ }
+ } else {
+ val userProfile = configFactory.user ?: return
+ userProfile.setNtsPriority(ConfigBase.PRIORITY_VISIBLE)
+ DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true)
+ }
+ val newVolatileParams = volatile.getOrConstructOneToOne(address.serialize())
+ volatile.set(newVolatileParams)
+ }
+ }
+
+ override fun threadDeleted(address: Address, threadId: Long) {
+ val volatile = configFactory.convoVolatile ?: return
+ if (address.isGroup) {
+ val groups = configFactory.userGroups ?: return
+ if (address.isClosedGroup) {
+ val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
+ volatile.eraseLegacyClosedGroup(sessionId)
+ groups.eraseLegacyGroup(sessionId)
+ } else if (address.isOpenGroup) {
+ // these should be removed in the group leave / handling new configs
+ Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
+ }
+ } else {
+ // non-standard contact prefixes: 15, 00 etc shouldn't be stored in config
+ if (SessionId(address.serialize()).prefix != IdPrefix.STANDARD) return
+ volatile.eraseOneToOne(address.serialize())
+ if (getUserPublicKey() != address.serialize()) {
+ val contacts = configFactory.contacts ?: return
+ contacts.upsertContact(address.serialize()) {
+ priority = PRIORITY_HIDDEN
+ }
+ } else {
+ val userProfile = configFactory.user ?: return
+ userProfile.setNtsPriority(PRIORITY_HIDDEN)
+ }
+ }
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
+ }
-class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
-
override fun getUserPublicKey(): String? {
return TextSecurePreferences.getLocalNumber(context)
}
@@ -74,6 +184,25 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
database.setProfileAvatar(recipient, profileAvatar)
}
+ override fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?) {
+ val db = DatabaseComponent.get(context).recipientDatabase()
+ db.setProfileAvatar(recipient, newProfilePicture)
+ db.setProfileKey(recipient, newProfileKey)
+ }
+
+ override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) {
+ val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
+ Recipient.from(context, it, false)
+ }
+ ourRecipient.resolve().profileKey = newProfileKey
+ TextSecurePreferences.setProfileKey(context, newProfileKey?.let { Base64.encodeBytes(it) })
+ TextSecurePreferences.setProfilePictureURL(context, newProfilePicture)
+
+ if (newProfileKey != null) {
+ JobQueue.shared.add(RetrieveProfileAvatarJob(newProfilePicture, ourRecipient.address))
+ }
+ }
+
override fun getOrGenerateRegistrationID(): Int {
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
if (registrationID == 0) {
@@ -94,19 +223,56 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return database.getAttachmentsForMessage(messageID)
}
- override fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean) {
+ override fun getLastSeen(threadId: Long): Long {
val threadDb = DatabaseComponent.get(context).threadDatabase()
- threadDb.setRead(threadId, updateLastSeen)
+ return threadDb.getLastSeenAndHasSent(threadId)?.first() ?: 0L
}
- override fun incrementUnread(threadId: Long, amount: Int, unreadMentionAmount: Int) {
+ override fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean) {
val threadDb = DatabaseComponent.get(context).threadDatabase()
- threadDb.incrementUnread(threadId, amount, unreadMentionAmount)
+ getRecipientForThread(threadId)?.let { recipient ->
+ val currentLastRead = threadDb.getLastSeenAndHasSent(threadId).first()
+ // don't set the last read in the volatile if we didn't set it in the DB
+ if (!threadDb.markAllAsRead(threadId, recipient.isGroupRecipient, lastSeenTime, force) && !force) return
+
+ // don't process configs for inbox recipients
+ if (recipient.isOpenGroupInboxRecipient) return
+
+ configFactory.convoVolatile?.let { config ->
+ val convo = when {
+ // recipient closed group
+ recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize()))
+ // recipient is open group
+ recipient.isOpenGroupRecipient -> {
+ val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return
+ BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
+ config.getOrConstructCommunity(base, room, pubKey)
+ } ?: return
+ }
+ // otherwise recipient is one to one
+ recipient.isContactRecipient -> {
+ // don't process non-standard session IDs though
+ val sessionId = SessionId(recipient.address.serialize())
+ if (sessionId.prefix != IdPrefix.STANDARD) return
+
+ config.getOrConstructOneToOne(recipient.address.serialize())
+ }
+ else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.serialize()}")
+ }
+ convo.lastRead = lastSeenTime
+ if (convo.unread) {
+ convo.unread = lastSeenTime <= currentLastRead
+ notifyConversationListListeners()
+ }
+ config.set(convo)
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
+ }
+ }
}
override fun updateThread(threadId: Long, unarchive: Boolean) {
val threadDb = DatabaseComponent.get(context).threadDatabase()
- threadDb.update(threadId, unarchive)
+ threadDb.update(threadId, unarchive, false)
}
override fun persist(message: VisibleMessage,
@@ -115,7 +281,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
groupPublicKey: String?,
openGroupID: String?,
attachments: List,
- runIncrement: Boolean,
runThreadUpdate: Boolean): Long? {
var messageID: Long? = null
val senderAddress = fromSerialized(message.sender!!)
@@ -142,13 +307,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
val targetRecipient = Recipient.from(context, targetAddress, false)
if (!targetRecipient.isGroupRecipient) {
- val recipientDb = DatabaseComponent.get(context).recipientDatabase()
if (isUserSender || isUserBlindedSender) {
- recipientDb.setApproved(targetRecipient, true)
+ setRecipientApproved(targetRecipient, true)
} else {
- recipientDb.setApprovedMe(targetRecipient, true)
+ setRecipientApprovedMe(targetRecipient, true)
}
}
+ if (message.threadID == null && !targetRecipient.isOpenGroupRecipient) {
+ // open group recipients should explicitly create threads
+ message.threadID = getOrCreateThreadIdFor(targetAddress)
+ }
if (message.isMediaMessage() || attachments.isNotEmpty()) {
val quote: Optional = if (quotes != null) Optional.of(quotes) else Optional.absent()
val linkPreviews: Optional> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! })
@@ -162,7 +330,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
it.toSignalPointer()
}
val mediaMessage = IncomingMediaMessage.from(message, senderAddress, targetRecipient.expireMessages * 1000L, group, signalServiceAttachments, quote, linkPreviews)
- mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate)
+ mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID!!, message.receivedTimestamp ?: 0, runThreadUpdate)
}
if (insertResult.isPresent) {
messageID = insertResult.get().messageId
@@ -179,7 +347,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
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, runIncrement, runThreadUpdate)
+ smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runThreadUpdate)
}
insertResult.orNull()?.let { result ->
messageID = result.messageId
@@ -225,6 +393,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room, imageId)
}
+ override fun getConfigSyncJob(destination: Destination): Job? {
+ return DatabaseComponent.get(context).sessionJobDatabase().getAllJobs(ConfigurationSyncJob.KEY).values.firstOrNull {
+ (it as? ConfigurationSyncJob)?.destination == destination
+ }
+ }
+
override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) {
val job = DatabaseComponent.get(context).sessionJobDatabase().getMessageSendJob(messageSendJobID) ?: return
JobQueue.shared.resumePendingSendMessage(job)
@@ -234,11 +408,201 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return DatabaseComponent.get(context).sessionJobDatabase().isJobCanceled(job)
}
+ override fun cancelPendingMessageSendJobs(threadID: Long) {
+ val jobDb = DatabaseComponent.get(context).sessionJobDatabase()
+ jobDb.cancelPendingMessageSendJobs(threadID)
+ }
+
override fun getAuthToken(room: String, server: String): String? {
val id = "$server.$room"
return DatabaseComponent.get(context).lokiAPIDatabase().getAuthToken(id)
}
+ override fun notifyConfigUpdates(forConfigObject: ConfigBase) {
+ notifyUpdates(forConfigObject)
+ }
+
+ override fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean {
+ return configFactory.conversationInConfig(publicKey, groupPublicKey, openGroupId, visibleOnly)
+ }
+
+ override fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean {
+ return configFactory.canPerformChange(variant, publicKey, changeTimestampMs)
+ }
+
+ fun notifyUpdates(forConfigObject: ConfigBase) {
+ when (forConfigObject) {
+ is UserProfile -> updateUser(forConfigObject)
+ is Contacts -> updateContacts(forConfigObject)
+ is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject)
+ is UserGroupsConfig -> updateUserGroups(forConfigObject)
+ }
+ }
+
+ private fun updateUser(userProfile: UserProfile) {
+ val userPublicKey = getUserPublicKey() ?: return
+ // would love to get rid of recipient and context from this
+ val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
+ // update name
+ val name = userProfile.getName() ?: return
+ val userPic = userProfile.getPic()
+ val profileManager = SSKEnvironment.shared.profileManager
+ if (name.isNotEmpty()) {
+ TextSecurePreferences.setProfileName(context, name)
+ profileManager.setName(context, recipient, name)
+ }
+
+ // update pfp
+ if (userPic == UserPic.DEFAULT) {
+ clearUserPic()
+ } else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty()
+ && TextSecurePreferences.getProfilePictureURL(context) != userPic.url) {
+ setUserProfilePicture(userPic.url, userPic.key)
+ }
+ if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) {
+ // delete nts thread if needed
+ val ourThread = getThreadId(recipient) ?: return
+ deleteConversation(ourThread)
+ } else {
+ // create note to self thread if needed (?)
+ val ourThread = getOrCreateThreadIdFor(recipient.address)
+ DatabaseComponent.get(context).threadDatabase().setHasSent(ourThread, true)
+ setPinned(ourThread, userProfile.getNtsPriority() > 0)
+ }
+
+ }
+
+ private fun updateContacts(contacts: Contacts) {
+ val extracted = contacts.all().toList()
+ addLibSessionContacts(extracted)
+ }
+
+ override fun clearUserPic() {
+ val userPublicKey = getUserPublicKey() ?: return
+ val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
+ // would love to get rid of recipient and context from this
+ val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
+ // clear picture if userPic is null
+ TextSecurePreferences.setProfileKey(context, null)
+ ProfileKeyUtil.setEncodedProfileKey(context, null)
+ recipientDatabase.setProfileAvatar(recipient, null)
+ TextSecurePreferences.setProfileAvatarId(context, 0)
+ TextSecurePreferences.setProfilePictureURL(context, null)
+
+ Recipient.removeCached(fromSerialized(userPublicKey))
+ configFactory.user?.setPic(UserPic.DEFAULT)
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
+ }
+
+ private fun updateConvoVolatile(convos: ConversationVolatileConfig) {
+ val extracted = convos.all()
+ for (conversation in extracted) {
+ val threadId = when (conversation) {
+ is Conversation.OneToOne -> getThreadIdFor(conversation.sessionId, null, null, createThread = false)
+ is Conversation.LegacyGroup -> getThreadIdFor("", conversation.groupId,null, createThread = false)
+ is Conversation.Community -> getThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl.removeSuffix("/")}.${conversation.baseCommunityInfo.room}", createThread = false)
+ }
+ if (threadId != null) {
+ if (conversation.lastRead > getLastSeen(threadId)) {
+ markConversationAsRead(threadId, conversation.lastRead, force = true)
+ }
+ updateThread(threadId, false)
+ }
+ }
+ }
+
+ private fun updateUserGroups(userGroups: UserGroupsConfig) {
+ val threadDb = DatabaseComponent.get(context).threadDatabase()
+ val localUserPublicKey = getUserPublicKey() ?: return Log.w(
+ "Loki",
+ "No user public key when trying to update user groups from config"
+ )
+ val communities = userGroups.allCommunityInfo()
+ val lgc = userGroups.allLegacyGroupInfo()
+ val allOpenGroups = getAllOpenGroups()
+ val toDeleteCommunities = allOpenGroups.filter {
+ Conversation.Community(BaseCommunityInfo(it.value.server, it.value.room, it.value.publicKey), 0, false).baseCommunityInfo.fullUrl() !in communities.map { it.community.fullUrl() }
+ }
+
+ val existingCommunities: Map = allOpenGroups.filterKeys { it !in toDeleteCommunities.keys }
+ val toAddCommunities = communities.filter { it.community.fullUrl() !in existingCommunities.map { it.value.joinURL } }
+ val existingJoinUrls = existingCommunities.values.map { it.joinURL }
+
+ val existingClosedGroups = getAllGroups(includeInactive = true).filter { it.isClosedGroup }
+ val lgcIds = lgc.map { it.sessionId }
+ val toDeleteClosedGroups = existingClosedGroups.filter { group ->
+ GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds
+ }
+
+ // delete the ones which are not listed in the config
+ toDeleteCommunities.values.forEach { openGroup ->
+ OpenGroupManager.delete(openGroup.server, openGroup.room, context)
+ }
+
+ toDeleteClosedGroups.forEach { deleteGroup ->
+ val threadId = getThreadId(deleteGroup.encodedId)
+ if (threadId != null) {
+ ClosedGroupManager.silentlyRemoveGroup(context,threadId,GroupUtil.doubleDecodeGroupId(deleteGroup.encodedId), deleteGroup.encodedId, localUserPublicKey, delete = true)
+ }
+ }
+
+ toAddCommunities.forEach { toAddCommunity ->
+ val joinUrl = toAddCommunity.community.fullUrl()
+ if (!hasBackgroundGroupAddJob(joinUrl)) {
+ JobQueue.shared.add(BackgroundGroupAddJob(joinUrl))
+ }
+ }
+
+ for (groupInfo in communities) {
+ val groupBaseCommunity = groupInfo.community
+ if (groupBaseCommunity.fullUrl() in existingJoinUrls) {
+ // add it
+ val (threadId, _) = existingCommunities.entries.first { (_, v) -> v.joinURL == groupInfo.community.fullUrl() }
+ threadDb.setPinned(threadId, groupInfo.priority == PRIORITY_PINNED)
+ }
+ }
+
+ for (group in lgc) {
+ val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.sessionId }
+ val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) }
+ if (existingGroup != null) {
+ if (group.priority == PRIORITY_HIDDEN && existingThread != null) {
+ ClosedGroupManager.silentlyRemoveGroup(context,existingThread,GroupUtil.doubleDecodeGroupId(existingGroup.encodedId), existingGroup.encodedId, localUserPublicKey, delete = true)
+ } else if (existingThread == null) {
+ Log.w("Loki-DBG", "Existing group had no thread to hide")
+ } else {
+ Log.d("Loki-DBG", "Setting existing group pinned status to ${group.priority}")
+ threadDb.setPinned(existingThread, group.priority == PRIORITY_PINNED)
+ }
+ } else {
+ val members = group.members.keys.map { Address.fromSerialized(it) }
+ val admins = group.members.filter { it.value /*admin = true*/ }.keys.map { Address.fromSerialized(it) }
+ val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId)
+ val title = group.name
+ val formationTimestamp = (group.joinedAt * 1000L)
+ createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp)
+ setProfileSharing(Address.fromSerialized(groupId), true)
+ // Add the group to the user's set of public keys to poll for
+ addClosedGroupPublicKey(group.sessionId)
+ // Store the encryption key pair
+ val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey))
+ addClosedGroupEncryptionKeyPair(keyPair, group.sessionId, SnodeAPI.nowWithOffset)
+ // Set expiration timer
+ val expireTimer = group.disappearingTimer
+ setExpirationTimer(groupId, expireTimer.toInt())
+ // Notify the PN server
+ PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, group.sessionId, localUserPublicKey)
+ // Notify the user
+ val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
+ threadDb.setDate(threadID, formationTimestamp)
+ insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
+ // Don't create config group here, it's from a config update
+ // Start polling
+ ClosedGroupPollerV2.shared.startPolling(group.sessionId)
+ }
+ }
+ }
+
override fun setAuthToken(room: String, server: String, newValue: String) {
val id = "$server.$room"
DatabaseComponent.get(context).lokiAPIDatabase().setAuthToken(id, newValue)
@@ -474,6 +838,59 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).groupDatabase().create(groupId, title, members, avatar, relay, admins, formationTimestamp)
}
+ override fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map, formationTimestamp: Long, encryptionKeyPair: ECKeyPair) {
+ val volatiles = configFactory.convoVolatile ?: return
+ val userGroups = configFactory.userGroups ?: return
+ val groupVolatileConfig = volatiles.getOrConstructLegacyGroup(groupPublicKey)
+ groupVolatileConfig.lastRead = formationTimestamp
+ volatiles.set(groupVolatileConfig)
+ val groupInfo = GroupInfo.LegacyGroupInfo(
+ sessionId = groupPublicKey,
+ name = name,
+ members = members,
+ priority = ConfigBase.PRIORITY_VISIBLE,
+ encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
+ encSecKey = encryptionKeyPair.privateKey.serialize(),
+ disappearingTimer = 0L,
+ joinedAt = (formationTimestamp / 1000L)
+ )
+ // shouldn't exist, don't use getOrConstruct + copy
+ userGroups.set(groupInfo)
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
+ }
+
+ override fun updateGroupConfig(groupPublicKey: String) {
+ val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
+ val groupAddress = fromSerialized(groupID)
+ // TODO: probably add a check in here for isActive?
+ // TODO: also check if local user is a member / maybe run delete otherwise?
+ val existingGroup = getGroup(groupID)
+ ?: return Log.w("Loki-DBG", "No existing group for ${groupPublicKey.take(4)}} when updating group config")
+ val userGroups = configFactory.userGroups ?: return
+ if (!existingGroup.isActive) {
+ userGroups.eraseLegacyGroup(groupPublicKey)
+ return
+ }
+ val name = existingGroup.title
+ val admins = existingGroup.admins.map { it.serialize() }
+ val members = existingGroup.members.map { it.serialize() }
+ val membersMap = GroupUtil.createConfigMemberMap(admins = admins, members = members)
+ val latestKeyPair = getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
+ ?: return Log.w("Loki-DBG", "No latest closed group encryption key pair for ${groupPublicKey.take(4)}} when updating group config")
+ val recipientSettings = getRecipientSettings(groupAddress) ?: return
+ val threadID = getThreadId(groupAddress) ?: return
+ val groupInfo = userGroups.getOrConstructLegacyGroupInfo(groupPublicKey).copy(
+ name = name,
+ members = membersMap,
+ encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
+ encSecKey = latestKeyPair.privateKey.serialize(),
+ priority = if (isPinned(threadID)) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
+ disappearingTimer = recipientSettings.expireMessages.toLong(),
+ joinedAt = (existingGroup.formationTimestamp / 1000L)
+ )
+ userGroups.set(groupInfo)
+ }
+
override fun isGroupActive(groupPublicKey: String): Boolean {
return DatabaseComponent.get(context).groupDatabase().getGroup(GroupUtil.doubleEncodeGroupID(groupPublicKey)).orNull()?.isActive == true
}
@@ -504,7 +921,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, true, true)
+ smsDB.insertMessageInbox(infoMessage, true)
}
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long) {
@@ -552,8 +969,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).lokiAPIDatabase().removeClosedGroupPublicKey(groupPublicKey)
}
- override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
- DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
+ override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) {
+ DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, timestamp)
}
override fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) {
@@ -570,9 +987,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
.updateTimestampUpdated(groupID, updatedTimestamp)
}
- override fun setExpirationTimer(groupID: String, duration: Int) {
- val recipient = Recipient.from(context, fromSerialized(groupID), false)
- DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
+ override fun setExpirationTimer(address: String, duration: Int) {
+ val recipient = Recipient.from(context, fromSerialized(address), false)
+ DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration)
+ if (recipient.isContactRecipient && !recipient.isLocalNumber) {
+ configFactory.contacts?.upsertContact(address) {
+ this.expiryMode = if (duration != 0) {
+ ExpiryMode.AfterRead(duration.toLong())
+ } else { // = 0 / delete
+ ExpiryMode.NONE
+ }
+ }
+ if (configFactory.contacts?.needsPush() == true) {
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
+ }
+ }
}
override fun setServerCapabilities(server: String, capabilities: List) {
@@ -591,16 +1020,29 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
OpenGroupManager.updateOpenGroup(openGroup, context)
}
- override fun getAllGroups(): List {
- return DatabaseComponent.get(context).groupDatabase().allGroups
+ override fun getAllGroups(includeInactive: Boolean): List {
+ return DatabaseComponent.get(context).groupDatabase().getAllGroups(includeInactive)
}
override fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? {
return OpenGroupManager.addOpenGroup(urlAsString, context)
}
- override fun onOpenGroupAdded(server: String) {
+ override fun onOpenGroupAdded(server: String, room: String) {
OpenGroupManager.restartPollerForServer(server.removeSuffix("/"))
+ val groups = configFactory.userGroups ?: return
+ val volatileConfig = configFactory.convoVolatile ?: return
+ val openGroup = getOpenGroup(room, server) ?: return
+ val (infoServer, infoRoom, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
+ val pubKeyHex = Hex.toStringCondensed(pubKey)
+ val communityInfo = groups.getOrConstructCommunityInfo(infoServer, infoRoom, pubKeyHex)
+ groups.set(communityInfo)
+ val volatile = volatileConfig.getOrConstructCommunity(infoServer, infoRoom, pubKey)
+ if (volatile.lastRead != 0L) {
+ val threadId = getThreadId(openGroup) ?: return
+ markConversationAsRead(threadId, volatile.lastRead, force = true)
+ }
+ volatileConfig.set(volatile)
}
override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean {
@@ -618,17 +1060,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
}
- override fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long {
+ override fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? {
val database = DatabaseComponent.get(context).threadDatabase()
return if (!openGroupID.isNullOrEmpty()) {
val recipient = Recipient.from(context, fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false)
- database.getThreadIdIfExistsFor(recipient)
+ database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it }
} else if (!groupPublicKey.isNullOrEmpty()) {
val recipient = Recipient.from(context, fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false)
- database.getOrCreateThreadIdFor(recipient)
+ if (createThread) database.getOrCreateThreadIdFor(recipient)
+ else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it }
} else {
val recipient = Recipient.from(context, fromSerialized(publicKey), false)
- database.getOrCreateThreadIdFor(recipient)
+ if (createThread) database.getOrCreateThreadIdFor(recipient)
+ else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it }
}
}
@@ -637,6 +1081,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return getThreadId(address)
}
+ override fun getThreadId(openGroup: OpenGroup): Long? {
+ return GroupManager.getOpenGroupThreadID("${openGroup.server.removeSuffix("/")}.${openGroup.room}", context)
+ }
+
override fun getThreadId(address: Address): Long? {
val recipient = Recipient.from(context, address, false)
return getThreadId(recipient)
@@ -666,6 +1114,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun setContact(contact: Contact) {
DatabaseComponent.get(context).sessionContactDatabase().setContact(contact)
+ val address = fromSerialized(contact.sessionID)
+ if (!getRecipientApproved(address)) return
+ val recipientHash = SSKEnvironment.shared.profileManager.contactUpdatedInternal(contact)
+ val recipient = Recipient.from(context, address, false)
+ setRecipientHash(recipient, recipientHash)
}
override fun getRecipientForThread(threadId: Long): Recipient? {
@@ -677,6 +1130,51 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return if (recipientSettings.isPresent) { recipientSettings.get() } else null
}
+ override fun addLibSessionContacts(contacts: List) {
+ val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
+ val moreContacts = contacts.filter { contact ->
+ val id = SessionId(contact.id)
+ id.prefix?.isBlinded() == false || mappingDb.getBlindedIdMapping(contact.id).none { it.sessionId != null }
+ }
+ val profileManager = SSKEnvironment.shared.profileManager
+ moreContacts.forEach { contact ->
+ val address = fromSerialized(contact.id)
+ val recipient = Recipient.from(context, address, false)
+ setBlocked(listOf(recipient), contact.blocked, fromConfigUpdate = true)
+ setRecipientApproved(recipient, contact.approved)
+ setRecipientApprovedMe(recipient, contact.approvedMe)
+ if (contact.name.isNotEmpty()) {
+ profileManager.setName(context, recipient, contact.name)
+ } else {
+ profileManager.setName(context, recipient, null)
+ }
+ if (contact.nickname.isNotEmpty()) {
+ profileManager.setNickname(context, recipient, contact.nickname)
+ } else {
+ profileManager.setNickname(context, recipient, null)
+ }
+
+ if (contact.profilePicture != UserPic.DEFAULT) {
+ val (url, key) = contact.profilePicture
+ if (key.size != ProfileKeyUtil.PROFILE_KEY_BYTES) return@forEach
+ profileManager.setProfilePicture(context, recipient, url, key)
+ profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
+ } else {
+ profileManager.setProfilePicture(context, recipient, null, null)
+ }
+ if (contact.priority == PRIORITY_HIDDEN) {
+ getThreadId(fromSerialized(contact.id))?.let { conversationThreadId ->
+ deleteConversation(conversationThreadId)
+ }
+ } else {
+ getThreadId(fromSerialized(contact.id))?.let { conversationThreadId ->
+ setPinned(conversationThreadId, contact.priority == PRIORITY_PINNED)
+ }
+ }
+ setRecipientHash(recipient, contact.hashCode().toString())
+ }
+ }
+
override fun addContacts(contacts: List) {
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
val threadDatabase = DatabaseComponent.get(context).threadDatabase()
@@ -700,19 +1198,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
recipientDatabase.setProfileSharing(recipient, true)
recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED)
// create Thread if needed
- val threadId = threadDatabase.getOrCreateThreadIdFor(recipient)
+ val threadId = threadDatabase.getThreadIdIfExistsFor(recipient)
if (contact.didApproveMe == true) {
recipientDatabase.setApprovedMe(recipient, true)
}
- if (contact.isApproved == true) {
- recipientDatabase.setApproved(recipient, true)
+ if (contact.isApproved == true && threadId != -1L) {
+ setRecipientApproved(recipient, true)
threadDatabase.setHasSent(threadId, true)
}
val contactIsBlocked: Boolean? = contact.isBlocked
if (contactIsBlocked != null && recipient.isBlocked != contactIsBlocked) {
- recipientDatabase.setBlocked(recipient, contactIsBlocked)
- threadDatabase.deleteConversation(threadId)
+ setBlocked(listOf(recipient), contactIsBlocked, fromConfigUpdate = true)
}
}
if (contacts.isNotEmpty()) {
@@ -720,6 +1217,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
}
+ override fun setRecipientHash(recipient: Recipient, recipientHash: String?) {
+ val recipientDb = DatabaseComponent.get(context).recipientDatabase()
+ recipientDb.setRecipientHash(recipient, recipientHash)
+ }
+
override fun getLastUpdated(threadID: Long): Long {
val threadDB = DatabaseComponent.get(context).threadDatabase()
return threadDB.getLastUpdated(threadID)
@@ -740,12 +1242,78 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return mmsSmsDb.getConversationCount(threadID)
}
- override fun deleteConversation(threadId: Long) {
+ override fun setPinned(threadID: Long, isPinned: Boolean) {
val threadDB = DatabaseComponent.get(context).threadDatabase()
- threadDB.deleteConversation(threadId)
+ threadDB.setPinned(threadID, isPinned)
+ val threadRecipient = getRecipientForThread(threadID) ?: return
+ if (threadRecipient.isLocalNumber) {
+ val user = configFactory.user ?: return
+ user.setNtsPriority(if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE)
+ } else if (threadRecipient.isContactRecipient) {
+ val contacts = configFactory.contacts ?: return
+ contacts.upsertContact(threadRecipient.address.serialize()) {
+ priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
+ }
+ } else if (threadRecipient.isGroupRecipient) {
+ val groups = configFactory.userGroups ?: return
+ if (threadRecipient.isClosedGroupRecipient) {
+ val sessionId = GroupUtil.doubleDecodeGroupId(threadRecipient.address.serialize())
+ val newGroupInfo = groups.getOrConstructLegacyGroupInfo(sessionId).copy (
+ priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
+ )
+ groups.set(newGroupInfo)
+ } else if (threadRecipient.isOpenGroupRecipient) {
+ val openGroup = getOpenGroup(threadID) ?: return
+ val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
+ val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
+ priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
+ )
+ groups.set(newGroupInfo)
+ }
+ }
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
+ override fun isPinned(threadID: Long): Boolean {
+ val threadDB = DatabaseComponent.get(context).threadDatabase()
+ return threadDB.isPinned(threadID)
+ }
+ override fun setThreadDate(threadId: Long, newDate: Long) {
+ val threadDb = DatabaseComponent.get(context).threadDatabase()
+ threadDb.setDate(threadId, newDate)
+ }
+
+ override fun deleteConversation(threadID: Long) {
+ val recipient = getRecipientForThread(threadID)
+ val threadDB = DatabaseComponent.get(context).threadDatabase()
+ val groupDB = DatabaseComponent.get(context).groupDatabase()
+ threadDB.deleteConversation(threadID)
+ if (recipient != null) {
+ if (recipient.isContactRecipient) {
+ if (recipient.isLocalNumber) return
+ val contacts = configFactory.contacts ?: return
+ contacts.upsertContact(recipient.address.serialize()) {
+ this.priority = PRIORITY_HIDDEN
+ }
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
+ } else if (recipient.isClosedGroupRecipient) {
+ // TODO: handle closed group
+ val volatile = configFactory.convoVolatile ?: return
+ val groups = configFactory.userGroups ?: return
+ val groupID = recipient.address.toGroupString()
+ val closedGroup = getGroup(groupID)
+ val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
+ if (closedGroup != null) {
+ groupDB.delete(groupID) // TODO: Should we delete the group? (seems odd to leave it)
+ volatile.eraseLegacyClosedGroup(groupPublicKey)
+ groups.eraseLegacyGroup(groupPublicKey)
+ } else {
+ Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
+ }
+ }
+ }
+ }
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
return PartAuthority.getAttachmentDataUri(attachmentId)
@@ -762,6 +1330,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
if (recipient.isBlocked) return
+ val threadId = getThreadId(recipient) ?: return
+
val mediaMessage = IncomingMediaMessage(
address,
sentTimestamp,
@@ -780,14 +1350,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
Optional.of(message)
)
- database.insertSecureDecryptedMessageInbox(mediaMessage, -1, runIncrement = true, runThreadUpdate = true)
+ database.insertSecureDecryptedMessageInbox(mediaMessage, threadId, runThreadUpdate = true)
}
override fun insertMessageRequestResponse(response: MessageRequestResponse) {
val userPublicKey = getUserPublicKey()
val senderPublicKey = response.sender!!
val recipientPublicKey = response.recipient!!
- if (userPublicKey == null || (userPublicKey != recipientPublicKey && userPublicKey != senderPublicKey)) return
+
+ if (
+ userPublicKey == null
+ || (userPublicKey != recipientPublicKey && userPublicKey != senderPublicKey)
+ // this is true if it is a sync message
+ || (userPublicKey == recipientPublicKey && userPublicKey == senderPublicKey)
+ ) return
+
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
val threadDB = DatabaseComponent.get(context).threadDatabase()
if (userPublicKey == senderPublicKey) {
@@ -799,7 +1376,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
val smsDb = DatabaseComponent.get(context).smsDatabase()
val sender = Recipient.from(context, fromSerialized(senderPublicKey), false)
- val threadId = threadDB.getOrCreateThreadIdFor(sender)
+ val threadId = getOrCreateThreadIdFor(sender.address)
val profile = response.profile
if (profile != null) {
val profileManager = SSKEnvironment.shared.profileManager
@@ -814,9 +1391,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val profileKeyChanged = (sender.profileKey == null || !MessageDigest.isEqual(sender.profileKey, newProfileKey))
if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) {
- profileManager.setProfileKey(context, sender, newProfileKey!!)
+ profileManager.setProfilePicture(context, sender, profile.profilePictureURL!!, newProfileKey!!)
profileManager.setUnidentifiedAccessMode(context, sender, Recipient.UnidentifiedAccessMode.UNKNOWN)
- profileManager.setProfilePictureURL(context, sender, profile.profilePictureURL!!)
}
}
threadDB.setHasSent(threadId, true)
@@ -873,16 +1449,28 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
Optional.absent(),
Optional.absent()
)
- mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runIncrement = true, runThreadUpdate = true)
+ mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runThreadUpdate = true)
}
}
+ override fun getRecipientApproved(address: Address): Boolean {
+ return DatabaseComponent.get(context).recipientDatabase().getApproved(address)
+ }
+
override fun setRecipientApproved(recipient: Recipient, approved: Boolean) {
DatabaseComponent.get(context).recipientDatabase().setApproved(recipient, approved)
+ if (recipient.isLocalNumber || !recipient.isContactRecipient) return
+ configFactory.contacts?.upsertContact(recipient.address.serialize()) {
+ this.approved = approved
+ }
}
override fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) {
DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe)
+ if (recipient.isLocalNumber || !recipient.isContactRecipient) return
+ configFactory.contacts?.upsertContact(recipient.address.serialize()) {
+ this.approvedMe = approvedMe
+ }
}
override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) {
@@ -1012,9 +1600,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms))
}
- override fun unblock(toUnblock: Iterable) {
+ override fun setBlocked(recipients: Iterable, isBlocked: Boolean, fromConfigUpdate: Boolean) {
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
- recipientDb.setBlocked(toUnblock, false)
+ recipientDb.setBlocked(recipients, isBlocked)
+ recipients.filter { it.isContactRecipient && !it.isLocalNumber }.forEach { recipient ->
+ configFactory.contacts?.upsertContact(recipient.address.serialize()) {
+ this.blocked = isBlocked
+ }
+ }
+ val contactsConfig = configFactory.contacts ?: return
+ if (contactsConfig.needsPush() && !fromConfigUpdate) {
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
+ }
}
override fun blockedContacts(): List {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
index 52d914af08..5044529981 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -64,7 +64,6 @@ import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
import java.io.Closeable;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
@@ -74,6 +73,11 @@ import java.util.Set;
public class ThreadDatabase extends Database {
+ public interface ConversationThreadUpdateListener {
+ void threadCreated(@NonNull Address address, long threadId);
+ void threadDeleted(@NonNull Address address, long threadId);
+ }
+
private static final String TAG = ThreadDatabase.class.getSimpleName();
private final Map addressCache = new HashMap<>();
@@ -141,10 +145,16 @@ public class ThreadDatabase extends Database {
"ADD COLUMN " + UNREAD_MENTION_COUNT + " INTEGER DEFAULT 0;";
}
+ private ConversationThreadUpdateListener updateListener;
+
public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
+ public void setUpdateListener(ConversationThreadUpdateListener updateListener) {
+ this.updateListener = updateListener;
+ }
+
private long createThreadForRecipient(Address address, boolean group, int distributionType) {
ContentValues contentValues = new ContentValues(4);
long date = SnodeAPI.getNowWithOffset();
@@ -207,10 +217,14 @@ public class ThreadDatabase extends Database {
}
private void deleteThread(long threadId) {
+ Recipient recipient = getRecipientForThreadId(threadId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
- db.delete(TABLE_NAME, ID_WHERE, new String[] {threadId + ""});
+ int numberRemoved = db.delete(TABLE_NAME, ID_WHERE, new String[] {threadId + ""});
addressCache.remove(threadId);
notifyConversationListListeners();
+ if (updateListener != null && numberRemoved > 0 && recipient != null) {
+ updateListener.threadDeleted(recipient.getAddress(), threadId);
+ }
}
private void deleteThreads(Set threadIds) {
@@ -278,7 +292,7 @@ public class ThreadDatabase extends Database {
DatabaseComponent.get(context).smsDatabase().deleteMessagesInThreadBeforeDate(threadId, lastTweetDate);
DatabaseComponent.get(context).mmsDatabase().deleteMessagesInThreadBeforeDate(threadId, lastTweetDate);
- update(threadId, false);
+ update(threadId, false, true);
notifyConversationListeners(threadId);
}
} finally {
@@ -291,10 +305,34 @@ public class ThreadDatabase extends Database {
Log.i("ThreadDatabase", "Trimming thread: " + threadId + " before :"+timestamp);
DatabaseComponent.get(context).smsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp);
DatabaseComponent.get(context).mmsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp);
- update(threadId, false);
+ update(threadId, false, true);
notifyConversationListeners(threadId);
}
+ public List setRead(long threadId, long lastReadTime) {
+
+ final List smsRecords = DatabaseComponent.get(context).smsDatabase().setMessagesRead(threadId, lastReadTime);
+ final List mmsRecords = DatabaseComponent.get(context).mmsDatabase().setMessagesRead(threadId, lastReadTime);
+
+ if (smsRecords.isEmpty() && mmsRecords.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ ContentValues contentValues = new ContentValues(2);
+ contentValues.put(READ, smsRecords.isEmpty() && mmsRecords.isEmpty());
+ contentValues.put(LAST_SEEN, lastReadTime);
+
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
+
+ notifyConversationListListeners();
+
+ return new LinkedList() {{
+ addAll(smsRecords);
+ addAll(mmsRecords);
+ }};
+ }
+
public List setRead(long threadId, boolean lastSeen) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(READ, 1);
@@ -319,30 +357,6 @@ public class ThreadDatabase extends Database {
}};
}
- public void incrementUnread(long threadId, int amount, int unreadMentionAmount) {
- SQLiteDatabase db = databaseHelper.getWritableDatabase();
- db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " +
- UNREAD_COUNT + " = " + UNREAD_COUNT + " + ?, " +
- UNREAD_MENTION_COUNT + " = " + UNREAD_MENTION_COUNT + " + ? WHERE " + ID + " = ?",
- new String[] {
- String.valueOf(amount),
- String.valueOf(unreadMentionAmount),
- String.valueOf(threadId)
- });
- }
-
- public void decrementUnread(long threadId, int amount, int unreadMentionAmount) {
- SQLiteDatabase db = databaseHelper.getWritableDatabase();
- db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " +
- UNREAD_COUNT + " = " + UNREAD_COUNT + " - ?, " +
- UNREAD_MENTION_COUNT + " = " + UNREAD_MENTION_COUNT + " - ? WHERE " + ID + " = ? AND " + UNREAD_COUNT + " > 0",
- new String[] {
- String.valueOf(amount),
- String.valueOf(unreadMentionAmount),
- String.valueOf(threadId)
- });
- }
-
public void setDistributionType(long threadId, int distributionType) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(TYPE, distributionType);
@@ -352,6 +366,14 @@ public class ThreadDatabase extends Database {
notifyConversationListListeners();
}
+ public void setDate(long threadId, long date) {
+ ContentValues contentValues = new ContentValues(1);
+ contentValues.put(DATE, date);
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ int updated = db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
+ if (updated > 0) notifyConversationListListeners();
+ }
+
public int getDistributionType(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{TYPE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
@@ -427,9 +449,9 @@ public class ThreadDatabase extends Database {
" ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS +
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
" ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID +
- " WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " + MESSAGE_COUNT + " = " + UNREAD_COUNT + " AND " +
- RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " +
+ " WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " +
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " +
+ RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " +
GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL";
cursor = db.rawQuery(query, null);
@@ -481,7 +503,7 @@ public class ThreadDatabase extends Database {
}
public Cursor getApprovedConversationList() {
- String where = "((" + MESSAGE_COUNT + " != 0 AND (" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%')) OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
+ String where = "((" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%') OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
"AND " + ARCHIVED + " = 0 ";
return getConversationList(where);
}
@@ -517,21 +539,50 @@ public class ThreadDatabase extends Database {
return db.rawQuery(query, null);
}
- public void setLastSeen(long threadId, long timestamp) {
- SQLiteDatabase db = databaseHelper.getWritableDatabase();
- ContentValues contentValues = new ContentValues(1);
- if (timestamp == -1) {
- contentValues.put(LAST_SEEN, SnodeAPI.getNowWithOffset());
- } else {
- contentValues.put(LAST_SEEN, timestamp);
- }
+ /**
+ * @param threadId
+ * @param timestamp
+ * @return true if we have set the last seen for the thread, false if there were no messages in the thread
+ */
+ public boolean setLastSeen(long threadId, long timestamp) {
+ // edge case where we set the last seen time for a conversation before it loads messages (joining community for example)
+ MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
+ Recipient forThreadId = getRecipientForThreadId(threadId);
+ if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isOpenGroupRecipient()) return false;
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+
+ ContentValues contentValues = new ContentValues(1);
+ long lastSeenTime = timestamp == -1 ? SnodeAPI.getNowWithOffset() : timestamp;
+ contentValues.put(LAST_SEEN, lastSeenTime);
+ db.beginTransaction();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)});
+ String smsCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.READ+" = 0";
+ String smsMentionCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.READ+" = 0 AND s."+SmsDatabase.HAS_MENTION+" = 1";
+ String smsReactionCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.REACTIONS_UNREAD+" = 1";
+ String mmsCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.READ+" = 0";
+ String mmsMentionCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.READ+" = 0 AND m."+MmsDatabase.HAS_MENTION+" = 1";
+ String mmsReactionCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.REACTIONS_UNREAD+" = 1";
+ String allSmsUnread = "(("+smsCountSubQuery+") + ("+smsReactionCountSubQuery+"))";
+ String allMmsUnread = "(("+mmsCountSubQuery+") + ("+mmsReactionCountSubQuery+"))";
+ String allUnread = "(("+allSmsUnread+") + ("+allMmsUnread+"))";
+ String allUnreadMention = "(("+smsMentionCountSubQuery+") + ("+mmsMentionCountSubQuery+"))";
+
+ String reflectUpdates = "UPDATE "+TABLE_NAME+" AS t SET "+UNREAD_COUNT+" = "+allUnread+", "+UNREAD_MENTION_COUNT+" = "+allUnreadMention+" WHERE "+ID+" = ?";
+ db.execSQL(reflectUpdates, new Object[]{threadId});
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ notifyConversationListeners(threadId);
notifyConversationListListeners();
+ return true;
}
- public void setLastSeen(long threadId) {
- setLastSeen(threadId, -1);
+ /**
+ * @param threadId
+ * @return true if we have set the last seen for the thread, false if there were no messages in the thread
+ */
+ public boolean setLastSeen(long threadId) {
+ return setLastSeen(threadId, -1);
}
public Pair getLastSeenAndHasSent(long threadId) {
@@ -634,13 +685,19 @@ public class ThreadDatabase extends Database {
try {
cursor = db.query(TABLE_NAME, new String[]{ID}, where, recipientsArg, null, null, null);
-
+ long threadId;
+ boolean created = false;
if (cursor != null && cursor.moveToFirst()) {
- return cursor.getLong(cursor.getColumnIndexOrThrow(ID));
+ threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
} else {
DatabaseComponent.get(context).recipientDatabase().setProfileSharing(recipient, true);
- return createThreadForRecipient(recipient.getAddress(), recipient.isGroupRecipient(), distributionType);
+ threadId = createThreadForRecipient(recipient.getAddress(), recipient.isGroupRecipient(), distributionType);
+ created = true;
}
+ if (created && updateListener != null) {
+ updateListener.threadCreated(recipient.getAddress(), threadId);
+ }
+ return threadId;
} finally {
if (cursor != null)
cursor.close();
@@ -679,13 +736,14 @@ public class ThreadDatabase extends Database {
new String[] {String.valueOf(threadId)});
notifyConversationListeners(threadId);
+ notifyConversationListListeners();
}
- public boolean update(long threadId, boolean unarchive) {
+ public boolean update(long threadId, boolean unarchive, boolean shouldDeleteOnEmpty) {
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
long count = mmsSmsDatabase.getConversationCount(threadId);
- boolean shouldDeleteEmptyThread = deleteThreadOnEmpty(threadId);
+ boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && deleteThreadOnEmpty(threadId);
if (count == 0 && shouldDeleteEmptyThread) {
deleteThread(threadId);
@@ -708,12 +766,10 @@ public class ThreadDatabase extends Database {
updateThread(threadId, count, getFormattedBodyFor(record), getAttachmentUriFor(record),
record.getTimestamp(), record.getDeliveryStatus(), record.getDeliveryReceiptCount(),
record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount());
- notifyConversationListListeners();
return false;
} else {
if (shouldDeleteEmptyThread) {
deleteThread(threadId);
- notifyConversationListListeners();
return true;
}
return false;
@@ -721,6 +777,8 @@ public class ThreadDatabase extends Database {
} finally {
if (reader != null)
reader.close();
+ notifyConversationListListeners();
+ notifyConversationListeners(threadId);
}
}
@@ -732,10 +790,32 @@ public class ThreadDatabase extends Database {
new String[] {String.valueOf(threadId)});
notifyConversationListeners(threadId);
+ notifyConversationListListeners();
}
- public void markAllAsRead(long threadId, boolean isGroupRecipient) {
- List messages = setRead(threadId, true);
+ public boolean isPinned(long threadId) {
+ SQLiteDatabase db = getReadableDatabase();
+ Cursor cursor = db.query(TABLE_NAME, new String[]{IS_PINNED}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getInt(0) == 1;
+ }
+ return false;
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ }
+
+ /**
+ * @param threadId
+ * @param isGroupRecipient
+ * @param lastSeenTime
+ * @return true if we have set the last seen for the thread, false if there were no messages in the thread
+ */
+ public boolean markAllAsRead(long threadId, boolean isGroupRecipient, long lastSeenTime, boolean force) {
+ MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
+ if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && !force) return false;
+ List messages = setRead(threadId, lastSeenTime);
if (isGroupRecipient) {
for (MarkedMessageInfo message: messages) {
MarkReadReceiver.scheduleDeletion(context, message.getExpirationInfo());
@@ -743,7 +823,8 @@ public class ThreadDatabase extends Database {
} else {
MarkReadReceiver.process(context, messages);
}
- ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, false, 0);
+ ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, threadId);
+ return setLastSeen(threadId, lastSeenTime);
}
private boolean deleteThreadOnEmpty(long threadId) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index 8a4473b409..89bda09948 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -11,7 +11,6 @@ import androidx.core.app.NotificationCompat;
import net.zetetic.database.sqlcipher.SQLiteConnection;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
-import net.zetetic.database.sqlcipher.SQLiteException;
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
import org.session.libsession.utilities.TextSecurePreferences;
@@ -19,6 +18,7 @@ import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.BlindedIdMappingDatabase;
+import org.thoughtcrime.securesms.database.ConfigDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase;
@@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.database.SessionJobDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
+import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities;
import java.io.File;
@@ -85,9 +86,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV38 = 59;
private static final int lokiV39 = 60;
private static final int lokiV40 = 61;
+ private static final int lokiV41 = 62;
+ private static final int lokiV42 = 63;
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
- private static final int DATABASE_VERSION = lokiV40;
+ private static final int DATABASE_VERSION = lokiV42;
private static final int MIN_DATABASE_VERSION = lokiV7;
private static final String CIPHER3_DATABASE_NAME = "signal.db";
public static final String DATABASE_NAME = "signal_v4.db";
@@ -147,7 +150,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
}
- private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) throws SQLiteException {
+ private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) {
return SQLiteDatabase.openDatabase(path, databaseSecret.asString(), null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() {
@Override
public void preKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); }
@@ -340,6 +343,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(ThreadDatabase.getUnreadMentionCountCommand());
db.execSQL(SmsDatabase.CREATE_HAS_MENTION_COMMAND);
db.execSQL(MmsDatabase.CREATE_HAS_MENTION_COMMAND);
+ db.execSQL(ConfigDatabase.CREATE_CONFIG_TABLE_COMMAND);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@@ -351,6 +355,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
executeStatements(db, ReactionDatabase.CREATE_INDEXS);
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
+ db.execSQL(RecipientDatabase.getAddWrapperHash());
}
@Override
@@ -583,6 +588,16 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(MmsDatabase.CREATE_HAS_MENTION_COMMAND);
}
+ if (oldVersion < lokiV41) {
+ db.execSQL(ConfigDatabase.CREATE_CONFIG_TABLE_COMMAND);
+ db.execSQL(ConfigurationMessageUtilities.DELETE_INACTIVE_GROUPS);
+ db.execSQL(ConfigurationMessageUtilities.DELETE_INACTIVE_ONE_TO_ONES);
+ }
+
+ if (oldVersion < lokiV42) {
+ db.execSQL(RecipientDatabase.getAddWrapperHash());
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt
index 6f26c6ae3a..936e4f287f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.dependencies
import dagger.Binds
import dagger.Module
+import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.session.libsession.utilities.AppTextSecurePreferences
@@ -19,4 +20,10 @@ abstract class AppModule {
@Binds
abstract fun bindConversationRepository(repository: DefaultConversationRepository): ConversationRepository
+}
+
+@EntryPoint
+@InstallIn(SingletonComponent::class)
+interface AppComponent {
+ fun getPrefs(): TextSecurePreferences
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt
new file mode 100644
index 0000000000..d664ffedb2
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt
@@ -0,0 +1,251 @@
+package org.thoughtcrime.securesms.dependencies
+
+import android.content.Context
+import android.os.Trace
+import network.loki.messenger.libsession_util.ConfigBase
+import network.loki.messenger.libsession_util.Contacts
+import network.loki.messenger.libsession_util.ConversationVolatileConfig
+import network.loki.messenger.libsession_util.UserGroupsConfig
+import network.loki.messenger.libsession_util.UserProfile
+import org.session.libsession.snode.SnodeAPI
+import org.session.libsession.utilities.ConfigFactoryProtocol
+import org.session.libsession.utilities.ConfigFactoryUpdateListener
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
+import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.database.ConfigDatabase
+import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
+import org.thoughtcrime.securesms.groups.GroupManager
+import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
+
+class ConfigFactory(
+ private val context: Context,
+ private val configDatabase: ConfigDatabase,
+ private val maybeGetUserInfo: () -> Pair?
+) :
+ ConfigFactoryProtocol {
+ companion object {
+ // This is a buffer period within which we will process messages which would result in a
+ // config change, any message which would normally result in a config change which was sent
+ // before `lastConfigMessage.timestamp - configChangeBufferPeriod` will not actually have
+ // it's changes applied (control text will still be added though)
+ val configChangeBufferPeriod: Long = (2 * 60 * 1000)
+ }
+
+ fun keyPairChanged() { // this should only happen restoring or clearing data
+ _userConfig?.free()
+ _contacts?.free()
+ _convoVolatileConfig?.free()
+ _userConfig = null
+ _contacts = null
+ _convoVolatileConfig = null
+ }
+
+ private val userLock = Object()
+ private var _userConfig: UserProfile? = null
+ private val contactsLock = Object()
+ private var _contacts: Contacts? = null
+ private val convoVolatileLock = Object()
+ private var _convoVolatileConfig: ConversationVolatileConfig? = null
+ private val userGroupsLock = Object()
+ private var _userGroups: UserGroupsConfig? = null
+
+ private val isConfigForcedOn by lazy { TextSecurePreferences.hasForcedNewConfig(context) }
+
+ private val listeners: MutableList = mutableListOf()
+ fun registerListener(listener: ConfigFactoryUpdateListener) {
+ listeners += listener
+ }
+
+ fun unregisterListener(listener: ConfigFactoryUpdateListener) {
+ listeners -= listener
+ }
+
+ private inline fun synchronizedWithLog(lock: Any, body: ()->T): T {
+ Trace.beginSection("synchronizedWithLog")
+ val result = synchronized(lock) {
+ body()
+ }
+ Trace.endSection()
+ return result
+ }
+
+ override val user: UserProfile?
+ get() = synchronizedWithLog(userLock) {
+ if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
+ if (_userConfig == null) {
+ val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
+ val userDump = configDatabase.retrieveConfigAndHashes(
+ SharedConfigMessage.Kind.USER_PROFILE.name,
+ publicKey
+ )
+ _userConfig = if (userDump != null) {
+ UserProfile.newInstance(secretKey, userDump)
+ } else {
+ ConfigurationMessageUtilities.generateUserProfileConfigDump()?.let { dump ->
+ UserProfile.newInstance(secretKey, dump)
+ } ?: UserProfile.newInstance(secretKey)
+ }
+ }
+ _userConfig
+ }
+
+ override val contacts: Contacts?
+ get() = synchronizedWithLog(contactsLock) {
+ if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
+ if (_contacts == null) {
+ val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
+ val contactsDump = configDatabase.retrieveConfigAndHashes(
+ SharedConfigMessage.Kind.CONTACTS.name,
+ publicKey
+ )
+ _contacts = if (contactsDump != null) {
+ Contacts.newInstance(secretKey, contactsDump)
+ } else {
+ ConfigurationMessageUtilities.generateContactConfigDump()?.let { dump ->
+ Contacts.newInstance(secretKey, dump)
+ } ?: Contacts.newInstance(secretKey)
+ }
+ }
+ _contacts
+ }
+
+ override val convoVolatile: ConversationVolatileConfig?
+ get() = synchronizedWithLog(convoVolatileLock) {
+ if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
+ if (_convoVolatileConfig == null) {
+ val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
+ val convoDump = configDatabase.retrieveConfigAndHashes(
+ SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name,
+ publicKey
+ )
+ _convoVolatileConfig = if (convoDump != null) {
+ ConversationVolatileConfig.newInstance(secretKey, convoDump)
+ } else {
+ ConfigurationMessageUtilities.generateConversationVolatileDump(context)
+ ?.let { dump ->
+ ConversationVolatileConfig.newInstance(secretKey, dump)
+ } ?: ConversationVolatileConfig.newInstance(secretKey)
+ }
+ }
+ _convoVolatileConfig
+ }
+
+ override val userGroups: UserGroupsConfig?
+ get() = synchronizedWithLog(userGroupsLock) {
+ if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
+ if (_userGroups == null) {
+ val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
+ val userGroupsDump = configDatabase.retrieveConfigAndHashes(
+ SharedConfigMessage.Kind.GROUPS.name,
+ publicKey
+ )
+ _userGroups = if (userGroupsDump != null) {
+ UserGroupsConfig.Companion.newInstance(secretKey, userGroupsDump)
+ } else {
+ ConfigurationMessageUtilities.generateUserGroupDump(context)?.let { dump ->
+ UserGroupsConfig.Companion.newInstance(secretKey, dump)
+ } ?: UserGroupsConfig.newInstance(secretKey)
+ }
+ }
+ _userGroups
+ }
+
+ override fun getUserConfigs(): List =
+ listOfNotNull(user, contacts, convoVolatile, userGroups)
+
+
+ private fun persistUserConfigDump(timestamp: Long) = synchronized(userLock) {
+ val dumped = user?.dump() ?: return
+ val (_, publicKey) = maybeGetUserInfo() ?: return
+ configDatabase.storeConfig(SharedConfigMessage.Kind.USER_PROFILE.name, publicKey, dumped, timestamp)
+ }
+
+ private fun persistContactsConfigDump(timestamp: Long) = synchronized(contactsLock) {
+ val dumped = contacts?.dump() ?: return
+ val (_, publicKey) = maybeGetUserInfo() ?: return
+ configDatabase.storeConfig(SharedConfigMessage.Kind.CONTACTS.name, publicKey, dumped, timestamp)
+ }
+
+ private fun persistConvoVolatileConfigDump(timestamp: Long) = synchronized(convoVolatileLock) {
+ val dumped = convoVolatile?.dump() ?: return
+ val (_, publicKey) = maybeGetUserInfo() ?: return
+ configDatabase.storeConfig(
+ SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name,
+ publicKey,
+ dumped,
+ timestamp
+ )
+ }
+
+ private fun persistUserGroupsConfigDump(timestamp: Long) = synchronized(userGroupsLock) {
+ val dumped = userGroups?.dump() ?: return
+ val (_, publicKey) = maybeGetUserInfo() ?: return
+ configDatabase.storeConfig(SharedConfigMessage.Kind.GROUPS.name, publicKey, dumped, timestamp)
+ }
+
+ override fun persist(forConfigObject: ConfigBase, timestamp: Long) {
+ try {
+ listeners.forEach { listener ->
+ listener.notifyUpdates(forConfigObject)
+ }
+ when (forConfigObject) {
+ is UserProfile -> persistUserConfigDump(timestamp)
+ is Contacts -> persistContactsConfigDump(timestamp)
+ is ConversationVolatileConfig -> persistConvoVolatileConfigDump(timestamp)
+ is UserGroupsConfig -> persistUserGroupsConfigDump(timestamp)
+ else -> throw UnsupportedOperationException("Can't support type of ${forConfigObject::class.simpleName} yet")
+ }
+ } catch (e: Exception) {
+ Log.e("Loki", "failed to persist ${forConfigObject.javaClass.simpleName}", e)
+ }
+ }
+
+ override fun conversationInConfig(
+ publicKey: String?,
+ groupPublicKey: String?,
+ openGroupId: String?,
+ visibleOnly: Boolean
+ ): Boolean {
+ if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return true
+
+ val (_, userPublicKey) = maybeGetUserInfo() ?: return true
+
+ if (openGroupId != null) {
+ val userGroups = userGroups ?: return false
+ val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context)
+ val openGroup = get(context).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return false
+
+ // Not handling the `hidden` behaviour for communities so just indicate the existence
+ return (userGroups.getCommunityInfo(openGroup.server, openGroup.room) != null)
+ }
+ else if (groupPublicKey != null) {
+ val userGroups = userGroups ?: return false
+
+ // Not handling the `hidden` behaviour for legacy groups so just indicate the existence
+ return (userGroups.getLegacyGroupInfo(groupPublicKey) != null)
+ }
+ else if (publicKey == userPublicKey) {
+ val user = user ?: return false
+
+ return (!visibleOnly || user.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN)
+ }
+ else if (publicKey != null) {
+ val contacts = contacts ?: return false
+ val targetContact = contacts.get(publicKey) ?: return false
+
+ return (!visibleOnly || targetContact.priority != ConfigBase.PRIORITY_HIDDEN)
+ }
+
+ return false
+ }
+
+ override fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean {
+ if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return true
+
+ val lastUpdateTimestampMs = configDatabase.retrieveConfigLastUpdateTimestamp(variant, publicKey)
+
+ // Ensure the change occurred after the last config message was handled (minus the buffer period)
+ return (changeTimestampMs >= (lastUpdateTimestampMs - ConfigFactory.configChangeBufferPeriod))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt
index 60d31a19d4..f2c046e0aa 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt
@@ -45,4 +45,5 @@ interface DatabaseComponent {
fun attachmentProvider(): MessageDataProvider
fun blindedIdMappingDatabase(): BlindedIdMappingDatabase
fun groupMemberDatabase(): GroupMemberDatabase
+ fun configDatabase(): ConfigDatabase
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt
index 3372e10330..524100190e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt
@@ -6,7 +6,6 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
-import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.session.libsession.database.MessageDataProvider
import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider
import org.thoughtcrime.securesms.crypto.AttachmentSecret
@@ -132,10 +131,18 @@ object DatabaseModule {
@Provides
@Singleton
- fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = Storage(context,openHelper)
+ fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper, configFactory: ConfigFactory, threadDatabase: ThreadDatabase): Storage {
+ val storage = Storage(context,openHelper, configFactory)
+ threadDatabase.setUpdateListener(storage)
+ return storage
+ }
@Provides
@Singleton
fun provideAttachmentProvider(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper): MessageDataProvider = DatabaseAttachmentProvider(context, openHelper)
+ @Provides
+ @Singleton
+ fun provideConfigDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper): ConfigDatabase = ConfigDatabase(context, openHelper)
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/InjectableType.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/InjectableType.java
deleted file mode 100644
index 033b3ef45a..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/InjectableType.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package org.thoughtcrime.securesms.dependencies;
-
-public interface InjectableType {
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt
new file mode 100644
index 0000000000..cd4b071338
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt
@@ -0,0 +1,36 @@
+package org.thoughtcrime.securesms.dependencies
+
+import android.content.Context
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import org.session.libsession.utilities.ConfigFactoryUpdateListener
+import org.session.libsession.utilities.TextSecurePreferences
+import org.thoughtcrime.securesms.crypto.KeyPairUtilities
+import org.thoughtcrime.securesms.database.ConfigDatabase
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object SessionUtilModule {
+
+ private fun maybeUserEdSecretKey(context: Context): ByteArray? {
+ val edKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return null
+ return edKey.secretKey.asBytes
+ }
+
+ @Provides
+ @Singleton
+ fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase): ConfigFactory =
+ ConfigFactory(context, configDatabase) {
+ val localUserPublicKey = TextSecurePreferences.getLocalNumber(context)
+ val secretKey = maybeUserEdSecretKey(context)
+ if (localUserPublicKey == null || secretKey == null) null
+ else secretKey to localUserPublicKey
+ }.apply {
+ registerListener(context as ConfigFactoryUpdateListener)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt
index 8b880d2189..74e2cac4c8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt
@@ -98,7 +98,7 @@ class NewMessageFragment : Fragment() {
private fun hideLoader() {
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
binding.loader.visibility = View.GONE
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt
new file mode 100644
index 0000000000..8b362d70d1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt
@@ -0,0 +1,64 @@
+package org.thoughtcrime.securesms.groups
+
+import android.content.Context
+import network.loki.messenger.libsession_util.ConfigBase
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
+import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
+import org.session.libsession.utilities.Address
+import org.session.libsession.utilities.GroupRecord
+import org.session.libsession.utilities.GroupUtil
+import org.session.libsession.utilities.recipients.Recipient
+import org.session.libsignal.crypto.ecc.DjbECPublicKey
+import org.thoughtcrime.securesms.ApplicationContext
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
+
+object ClosedGroupManager {
+
+ fun silentlyRemoveGroup(context: Context, threadId: Long, groupPublicKey: String, groupID: String, userPublicKey: String, delete: Boolean = true) {
+ val storage = MessagingModuleConfiguration.shared.storage
+ // Mark the group as inactive
+ storage.setActive(groupID, false)
+ storage.removeClosedGroupPublicKey(groupPublicKey)
+ // Remove the key pairs
+ storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
+ storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
+ // Notify the PN server
+ PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
+ // Stop polling
+ ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
+ storage.cancelPendingMessageSendJobs(threadId)
+ ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
+ if (delete) {
+ storage.deleteConversation(threadId)
+ }
+ }
+
+ fun ConfigFactory.removeLegacyGroup(group: GroupRecord): Boolean {
+ val groups = userGroups ?: return false
+ if (!group.isClosedGroup) return false
+ val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId())
+ return groups.eraseLegacyGroup(groupPublicKey)
+ }
+
+ fun ConfigFactory.updateLegacyGroup(groupRecipientSettings: Recipient.RecipientSettings, group: GroupRecord) {
+ val groups = userGroups ?: return
+ if (!group.isClosedGroup) return
+ val storage = MessagingModuleConfiguration.shared.storage
+ val threadId = storage.getThreadId(group.encodedId) ?: return
+ val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId())
+ val latestKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
+ val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey)
+ val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize))
+ val toSet = legacyInfo.copy(
+ members = latestMemberMap,
+ name = group.title,
+ disappearingTimer = groupRecipientSettings.expireMessages.toLong(),
+ priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
+ encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
+ encSecKey = latestKeyPair.privateKey.serialize()
+ )
+ groups.set(toSet)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt
index 62e762316b..9fee8adafc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt
@@ -16,6 +16,7 @@ import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task
@@ -28,16 +29,28 @@ import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.recipients.Recipient
+import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
+import org.thoughtcrime.securesms.database.Storage
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut
import java.io.IOException
+import javax.inject.Inject
+@AndroidEntryPoint
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
+
+ @Inject
+ lateinit var groupConfigFactory: ConfigFactory
+ @Inject
+ lateinit var storage: Storage
+
private val originalMembers = HashSet()
private val zombies = HashSet()
private val members = HashSet()
@@ -289,7 +302,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
isLoading = true
loaderContainer.fadeIn()
val promise: Promise = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
- MessageSender.explicitLeave(groupPublicKey!!, true)
+ MessageSender.explicitLeave(groupPublicKey!!, false)
} else {
task {
if (hasNameChanged) {
@@ -306,6 +319,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
promise.successUi {
loaderContainer.fadeOut()
isLoading = false
+ updateGroupConfig()
finish()
}.failUi { exception ->
val message = if (exception is MessageSender.Error) exception.description else "An error occurred"
@@ -316,5 +330,13 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
}
}
- class GroupMembers(val members: List, val zombieMembers: List) { }
+ private fun updateGroupConfig() {
+ val latestRecipient = storage.getRecipientSettings(Address.fromSerialized(groupID))
+ ?: return Log.w("Loki", "No recipient settings when trying to update group config")
+ val latestGroup = storage.getGroup(groupID)
+ ?: return Log.w("Loki", "No group record when trying to update group config")
+ groupConfigFactory.updateLegacyGroup(latestRecipient, latestGroup)
+ }
+
+ class GroupMembers(val members: List, val zombieMembers: List)
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java
index a3d0e6d252..d4c5acf4ed 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java
@@ -6,6 +6,7 @@ import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.DistributionTypes;
import org.session.libsession.utilities.GroupUtil;
@@ -16,11 +17,14 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.util.BitmapUtil;
+import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Set;
+import network.loki.messenger.libsession_util.UserGroupsConfig;
+
public class GroupManager {
public static long getOpenGroupThreadID(String id, @NonNull Context context) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt
index d37b17ef9f..ae59c3833e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt
@@ -55,7 +55,7 @@ class JoinCommunityFragment : Fragment() {
fun hideLoader() {
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
binding.loader.visibility = View.GONE
}
@@ -79,7 +79,7 @@ class JoinCommunityFragment : Fragment() {
val openGroupID = "$sanitizedServer.${openGroup.room}"
OpenGroupManager.add(sanitizedServer, openGroup.room, openGroup.serverPublicKey, requireContext())
val storage = MessagingModuleConfiguration.shared.storage
- storage.onOpenGroupAdded(sanitizedServer)
+ storage.onOpenGroupAdded(sanitizedServer, openGroup.room)
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, requireContext())
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt
index dbdf2615ae..2754c70f69 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt
@@ -9,8 +9,8 @@ import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
import org.session.libsignal.utilities.Log
-import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import java.util.concurrent.Executors
object OpenGroupManager {
@@ -40,7 +40,13 @@ object OpenGroupManager {
if (isPolling) { return }
isPolling = true
val storage = MessagingModuleConfiguration.shared.storage
- val servers = storage.getAllOpenGroups().values.map { it.server }.toSet()
+ val (serverGroups, toDelete) = storage.getAllOpenGroups().values.partition { storage.getThreadId(it) != null }
+ toDelete.forEach { openGroup ->
+ Log.w("Loki", "Need to delete a group")
+ delete(openGroup.server, openGroup.room, MessagingModuleConfiguration.shared.context)
+ }
+
+ val servers = serverGroups.map { it.server }.toSet()
synchronized(pollUpdaterLock) {
servers.forEach { server ->
pollers[server]?.stop() // Shouldn't be necessary
@@ -58,14 +64,14 @@ object OpenGroupManager {
}
@WorkerThread
- fun add(server: String, room: String, publicKey: String, context: Context): OpenGroupApi.RoomInfo? {
+ fun add(server: String, room: String, publicKey: String, context: Context): Pair {
val openGroupID = "$server.$room"
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
val storage = MessagingModuleConfiguration.shared.storage
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
// Check it it's added already
val existingOpenGroup = threadDB.getOpenGroupChat(threadID)
- if (existingOpenGroup != null) { return null }
+ if (existingOpenGroup != null) { return threadID to null }
// Clear any existing data if needed
storage.removeLastDeletionServerID(room, server)
storage.removeLastMessageServerID(room, server)
@@ -86,7 +92,7 @@ object OpenGroupManager {
pollInfo = info.toPollInfo(),
createGroupIfMissingWithPublicKey = publicKey
)
- return info
+ return threadID to info
}
fun restartPollerForServer(server: String) {
@@ -102,23 +108,27 @@ object OpenGroupManager {
}
}
+ @WorkerThread
fun delete(server: String, room: String, context: Context) {
val storage = MessagingModuleConfiguration.shared.storage
+ val configFactory = MessagingModuleConfiguration.shared.configFactory
val threadDB = DatabaseComponent.get(context).threadDatabase()
- val openGroupID = "$server.$room"
+ val openGroupID = "${server.removeSuffix("/")}.$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.getAllOpenGroups().filter { it.value.server == server }
- if (openGroups.count() == 1) {
+ if (openGroups.isNotEmpty()) {
synchronized(pollUpdaterLock) {
val poller = pollers[server]
poller?.stop()
pollers.remove(server)
}
}
+ configFactory.userGroups?.eraseCommunity(server, room)
+ configFactory.convoVolatile?.eraseCommunity(server, room)
// Delete
storage.removeLastDeletionServerID(room, server)
storage.removeLastMessageServerID(room, server)
@@ -126,19 +136,19 @@ object OpenGroupManager {
storage.removeLastOutboxMessageId(server)
val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase()
lokiThreadDB.removeOpenGroupChat(threadID)
- ThreadUtils.queue {
- threadDB.deleteConversation(threadID) // Must be invoked on a background thread
- GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
- }
+ storage.deleteConversation(threadID) // Must be invoked on a background thread
+ GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
+ @WorkerThread
fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? {
val url = HttpUrl.parse(urlAsString) ?: return null
val server = OpenGroup.getServer(urlAsString)
val room = url.pathSegments().firstOrNull() ?: return null
val publicKey = url.queryParameter("public_key") ?: return null
- return add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function
+ return add(server.toString().removeSuffix("/"), room, publicKey, context).second // assume migrated from calling function
}
fun updateOpenGroup(openGroup: OpenGroup, context: Context) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
index 7e9d2640a1..702bf33929 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
@@ -7,10 +7,15 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
import org.thoughtcrime.securesms.database.model.ThreadRecord
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.UiModeUtilities
+import org.thoughtcrime.securesms.util.getConversationUnread
+import javax.inject.Inject
+@AndroidEntryPoint
class ConversationOptionsBottomSheet(private val parentContext: Context) : BottomSheetDialogFragment(), View.OnClickListener {
private lateinit var binding: FragmentConversationBottomSheetBinding
//FIXME AC: Supplying a threadRecord directly into the field from an activity
@@ -19,6 +24,8 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
// if we want to use dialog fragments properly.
lateinit var thread: ThreadRecord
+ @Inject lateinit var configFactory: ConfigFactory
+
var onViewDetailsTapped: (() -> Unit?)? = null
var onCopyConversationId: (() -> Unit?)? = null
var onPinTapped: (() -> Unit)? = null
@@ -77,7 +84,7 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
binding.notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
binding.notificationsTextView.setOnClickListener(this)
binding.deleteTextView.setOnClickListener(this)
- binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0
+ binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0 || configFactory.convoVolatile?.getConversationUnread(thread) == true
binding.markAllAsReadTextView.setOnClickListener(this)
binding.pinTextView.isVisible = !thread.isPinned
binding.unpinTextView.isVisible = thread.isPinned
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
index c6a6e1f7f5..31b281c6de 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
@@ -6,12 +6,12 @@ import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.util.AttributeSet
import android.util.TypedValue
-import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
+import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewConversationBinding
import org.session.libsession.utilities.recipients.Recipient
@@ -19,12 +19,19 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.hig
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.dependencies.ConfigFactory
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.getAccentColor
+import org.thoughtcrime.securesms.util.getConversationUnread
import java.util.Locale
+import javax.inject.Inject
+@AndroidEntryPoint
class ConversationView : LinearLayout {
+
+ @Inject lateinit var configFactory: ConfigFactory
+
private val binding: ViewConversationBinding by lazy { ViewConversationBinding.bind(this) }
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
var thread: ThreadRecord? = null
@@ -58,7 +65,6 @@ class ConversationView : LinearLayout {
} else {
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
}
- binding.profilePictureView.root.glide = glide
val unreadCount = thread.unreadCount
if (thread.recipient.isBlocked) {
binding.accentView.setBackgroundResource(R.color.destructive)
@@ -71,7 +77,7 @@ class ConversationView : LinearLayout {
// This would also not trigger the disappearing message timer which may or may not be desirable
binding.accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE
}
- val formattedUnreadCount = if (thread.isRead) {
+ val formattedUnreadCount = if (unreadCount == 0) {
null
} else {
if (unreadCount < 10000) unreadCount.toString() else "9999+"
@@ -80,6 +86,7 @@ class ConversationView : LinearLayout {
val textSize = if (unreadCount < 1000) 12.0f else 10.0f
binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead)
+ || (configFactory.convoVolatile?.getConversationUnread(thread) == true)
binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup)
val senderDisplayName = getUserDisplayName(thread.recipient)
@@ -117,18 +124,18 @@ class ConversationView : LinearLayout {
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
}
- binding.profilePictureView.root.update(thread.recipient)
+ binding.profilePictureView.update(thread.recipient)
}
fun recycle() {
- binding.profilePictureView.root.recycle()
+ binding.profilePictureView.recycle()
}
private fun getUserDisplayName(recipient: Recipient): String? {
return if (recipient.isLocalNumber) {
context.getString(R.string.note_to_self)
} else {
- recipient.name // Internally uses the Contact API
+ recipient.toShortString() // Internally uses the Contact API
}
}
// endregion
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
index 0215040d37..72bd098f4f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
@@ -1,19 +1,22 @@
package org.thoughtcrime.securesms.home
+import android.Manifest
+import android.app.NotificationManager
import android.content.BroadcastReceiver
+import android.content.ClipData
+import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
-import android.content.ClipData
-import android.content.ClipboardManager
import android.os.Bundle
import android.text.SpannableString
import android.widget.Toast
import androidx.activity.viewModels
-import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -27,11 +30,14 @@ import kotlinx.coroutines.withContext
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityHomeBinding
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
+import network.loki.messenger.libsession_util.ConfigBase
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
+import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.sending_receiving.MessageSender
+import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.ProfilePictureModifiedEvent
@@ -41,7 +47,6 @@ import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.ApplicationContext
-import org.thoughtcrime.securesms.MuteDialog
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.start.NewConversationFragment
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
@@ -50,8 +55,10 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase
+import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter
@@ -62,7 +69,10 @@ import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.onboarding.SeedActivity
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
+import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.preferences.SettingsActivity
+import org.thoughtcrime.securesms.showSessionDialog
+import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.IP2Country
@@ -80,6 +90,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
SeedReminderViewDelegate,
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
+ companion object {
+ const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
+ }
+
+
private lateinit var binding: ActivityHomeBinding
private lateinit var glide: GlideRequests
private var broadcastReceiver: BroadcastReceiver? = null
@@ -87,8 +102,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
@Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
@Inject lateinit var recipientDatabase: RecipientDatabase
+ @Inject lateinit var storage: Storage
@Inject lateinit var groupDatabase: GroupDatabase
@Inject lateinit var textSecurePreferences: TextSecurePreferences
+ @Inject lateinit var configFactory: ConfigFactory
private val globalSearchViewModel by viewModels()
private val homeViewModel by viewModels()
@@ -97,7 +114,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
get() = textSecurePreferences.getLocalNumber()!!
private val homeAdapter: HomeAdapter by lazy {
- HomeAdapter(context = this, listener = this)
+ HomeAdapter(context = this, configFactory = configFactory, listener = this)
}
private val globalSearchAdapter = GlobalSearchAdapter { model ->
@@ -151,22 +168,23 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Set up Glide
glide = GlideApp.with(this)
// Set up toolbar buttons
- binding.profileButton.root.glide = glide
- binding.profileButton.root.setOnClickListener { openSettings() }
+ binding.profileButton.setOnClickListener { openSettings() }
binding.searchViewContainer.setOnClickListener {
binding.globalSearchInputLayout.requestFocus()
}
binding.sessionToolbar.disableClipping()
// Set up seed reminder view
- val hasViewedSeed = textSecurePreferences.getHasViewedSeed()
- if (!hasViewedSeed) {
- binding.seedReminderView.isVisible = true
- binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated
- binding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
- binding.seedReminderView.setProgress(80, false)
- binding.seedReminderView.delegate = this@HomeActivity
- } else {
- binding.seedReminderView.isVisible = false
+ lifecycleScope.launchWhenStarted {
+ val hasViewedSeed = textSecurePreferences.getHasViewedSeed()
+ if (!hasViewedSeed) {
+ binding.seedReminderView.isVisible = true
+ binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated
+ binding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
+ binding.seedReminderView.setProgress(80, false)
+ binding.seedReminderView.delegate = this@HomeActivity
+ } else {
+ binding.seedReminderView.isVisible = false
+ }
}
setupMessageRequestsBanner()
// Set up recycler view
@@ -176,6 +194,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.recyclerView.adapter = homeAdapter
binding.globalSearchRecycler.adapter = globalSearchAdapter
+ binding.configOutdatedView.setOnClickListener {
+ textSecurePreferences.setHasLegacyConfig(false)
+ updateLegacyConfigView()
+ }
+
// Set up empty state view
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
IP2Country.configureIfNeeded(this@HomeActivity)
@@ -192,6 +215,15 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
this.broadcastReceiver = broadcastReceiver
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
+ // subscribe to outdated config updates, this should be removed after long enough time for device migration
+ lifecycleScope.launch {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ TextSecurePreferences.events.filter { it == TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG }.collect {
+ updateLegacyConfigView()
+ }
+ }
+ }
+
lifecycleScope.launchWhenStarted {
launch(Dispatchers.IO) {
// Double check that the long poller is up
@@ -212,6 +244,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
}
}
+
// monitor the global search VM query
launch {
binding.globalSearchInputLayout.query
@@ -264,6 +297,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
}
EventBus.getDefault().register(this@HomeActivity)
+ if (intent.hasExtra(FROM_ONBOARDING)
+ && intent.getBooleanExtra(FROM_ONBOARDING, false)
+ && !(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()
+ ) {
+ Permissions.with(this)
+ .request(Manifest.permission.POST_NOTIFICATIONS)
+ .execute()
+ }
}
override fun onInputFocusChanged(hasFocus: Boolean) {
@@ -312,16 +353,26 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
}
+ private fun updateLegacyConfigView() {
+ binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset)
+ && textSecurePreferences.getHasLegacyConfig()
+ }
+
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.root.recycle() // clear cached image before update tje profilePictureView
- binding.profileButton.root.update()
+ binding.profileButton.recycle() // clear cached image before update tje profilePictureView
+ binding.profileButton.update()
if (textSecurePreferences.getHasViewedSeed()) {
binding.seedReminderView.isVisible = false
}
+
+ updateLegacyConfigView()
+
+ // TODO: remove this after enough updates that we can rely on ConfigBase.isNewConfigEnabled to always return true
+ // This will only run if we aren't using new configs, as they are schedule to sync when there are changes applied
if (textSecurePreferences.getConfigurationMessageSynced()) {
lifecycleScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
@@ -388,10 +439,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
private fun updateProfileButton() {
- binding.profileButton.root.publicKey = publicKey
- binding.profileButton.root.displayName = textSecurePreferences.getProfileName()
- binding.profileButton.root.recycle()
- binding.profileButton.root.update()
+ binding.profileButton.publicKey = publicKey
+ binding.profileButton.displayName = textSecurePreferences.getProfileName()
+ binding.profileButton.recycle()
+ binding.profileButton.update()
}
// endregion
@@ -488,39 +539,37 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
private fun blockConversation(thread: ThreadRecord) {
- AlertDialog.Builder(this)
- .setTitle(R.string.RecipientPreferenceActivity_block_this_contact_question)
- .setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
- lifecycleScope.launch(Dispatchers.IO) {
- recipientDatabase.setBlocked(thread.recipient, true)
- // TODO: Remove in UserConfig branch
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@HomeActivity)
- withContext(Dispatchers.Main) {
- binding.recyclerView.adapter!!.notifyDataSetChanged()
- dialog.dismiss()
- }
+ showSessionDialog {
+ title(R.string.RecipientPreferenceActivity_block_this_contact_question)
+ text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
+ button(R.string.RecipientPreferenceActivity_block) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ storage.setBlocked(listOf(thread.recipient), true)
+
+ withContext(Dispatchers.Main) {
+ binding.recyclerView.adapter!!.notifyDataSetChanged()
}
- }.show()
+ }
+ }
+ cancelButton()
+ }
}
private fun unblockConversation(thread: ThreadRecord) {
- AlertDialog.Builder(this)
- .setTitle(R.string.RecipientPreferenceActivity_unblock_this_contact_question)
- .setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
- lifecycleScope.launch(Dispatchers.IO) {
- recipientDatabase.setBlocked(thread.recipient, false)
- // TODO: Remove in UserConfig branch
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@HomeActivity)
- withContext(Dispatchers.Main) {
- binding.recyclerView.adapter!!.notifyDataSetChanged()
- dialog.dismiss()
- }
+ showSessionDialog {
+ title(R.string.RecipientPreferenceActivity_unblock_this_contact_question)
+ text(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
+ button(R.string.RecipientPreferenceActivity_unblock) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ storage.setBlocked(listOf(thread.recipient), false)
+
+ withContext(Dispatchers.Main) {
+ binding.recyclerView.adapter!!.notifyDataSetChanged()
}
- }.show()
+ }
+ }
+ cancelButton()
+ }
}
private fun setConversationMuted(thread: ThreadRecord, isMuted: Boolean) {
@@ -532,7 +581,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
}
} else {
- MuteDialog.show(this) { until: Long ->
+ showMuteDialog(this) { until ->
lifecycleScope.launch(Dispatchers.IO) {
recipientDatabase.setMuted(thread.recipient, until)
withContext(Dispatchers.Main) {
@@ -554,14 +603,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
lifecycleScope.launch(Dispatchers.IO) {
- threadDb.setPinned(threadId, pinned)
+ storage.setPinned(threadId, pinned)
homeViewModel.tryUpdateChannel()
}
}
private fun markAllAsRead(thread: ThreadRecord) {
ThreadUtils.queue {
- threadDb.markAllAsRead(thread.threadId, thread.recipient.isOpenGroupRecipient)
+ MessagingModuleConfiguration.shared.storage.markConversationAsRead(thread.threadId, SnodeAPI.nowWithOffset)
}
}
@@ -578,48 +627,41 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} else {
resources.getString(R.string.activity_home_delete_conversation_dialog_message)
}
- val dialog = AlertDialog.Builder(this)
- dialog.setMessage(message)
- dialog.setPositiveButton(R.string.yes) { _, _ ->
- lifecycleScope.launch(Dispatchers.Main) {
- val context = this@HomeActivity as Context
- // Cancel any outstanding jobs
- DatabaseComponent.get(context).sessionJobDatabase().cancelPendingMessageSendJobs(threadID)
- // Send a leave group message if this is an active closed group
- if (recipient.address.isClosedGroup && DatabaseComponent.get(context).groupDatabase().isActive(recipient.address.toGroupString())) {
- var isClosedGroup: Boolean
- var groupPublicKey: String?
- try {
- groupPublicKey = GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString()
- isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
- } catch (e: IOException) {
- groupPublicKey = null
- isClosedGroup = false
+
+ showSessionDialog {
+ text(message)
+ button(R.string.yes) {
+ lifecycleScope.launch(Dispatchers.Main) {
+ val context = this@HomeActivity
+ // Cancel any outstanding jobs
+ DatabaseComponent.get(context).sessionJobDatabase().cancelPendingMessageSendJobs(threadID)
+ // Send a leave group message if this is an active closed group
+ if (recipient.address.isClosedGroup && DatabaseComponent.get(context).groupDatabase().isActive(recipient.address.toGroupString())) {
+ try {
+ GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString()
+ .takeIf(DatabaseComponent.get(context).lokiAPIDatabase()::isClosedGroup)
+ ?.let { MessageSender.explicitLeave(it, false) }
+ } catch (_: IOException) {
+ }
}
- if (isClosedGroup) {
- MessageSender.explicitLeave(groupPublicKey!!, false)
+ // Delete the conversation
+ val v2OpenGroup = DatabaseComponent.get(context).lokiThreadDatabase().getOpenGroupChat(threadID)
+ if (v2OpenGroup != null) {
+ v2OpenGroup.apply { OpenGroupManager.delete(server, room, context) }
+ } else {
+ lifecycleScope.launch(Dispatchers.IO) {
+ threadDb.deleteConversation(threadID)
+ }
}
+ // Update the badge count
+ ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
+ // Notify the user
+ val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
+ Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
}
- // Delete the conversation
- val v2OpenGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadID)
- if (v2OpenGroup != null) {
- OpenGroupManager.delete(v2OpenGroup.server, v2OpenGroup.room, this@HomeActivity)
- } else {
- lifecycleScope.launch(Dispatchers.IO) {
- threadDb.deleteConversation(threadID)
- }
- }
- // Update the badge count
- ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
- // Notify the user
- val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
- Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
}
+ button(R.string.no)
}
- dialog.setNegativeButton(R.string.no) { _, _ ->
- // Do nothing
- }
- dialog.create().show()
}
private fun openSettings() {
@@ -633,17 +675,15 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
private fun hideMessageRequests() {
- AlertDialog.Builder(this)
- .setMessage("Hide message requests?")
- .setPositiveButton(R.string.yes) { _, _ ->
+ showSessionDialog {
+ text("Hide message requests?")
+ button(R.string.yes) {
textSecurePreferences.setHasHiddenMessageRequests()
setupMessageRequestsBanner()
homeViewModel.tryUpdateChannel()
}
- .setNegativeButton(R.string.no) { _, _ ->
- // Do nothing
- }
- .create().show()
+ button(R.string.no)
+ }
}
private fun showNewConversation() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt
index 4273794f5e..eaf242aae3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt
@@ -10,10 +10,12 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_ID
import network.loki.messenger.R
import org.thoughtcrime.securesms.database.model.ThreadRecord
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.mms.GlideRequests
class HomeAdapter(
private val context: Context,
+ private val configFactory: ConfigFactory,
private val listener: ConversationClickListener
) : RecyclerView.Adapter(), ListUpdateCallback {
@@ -29,7 +31,7 @@ class HomeAdapter(
get() = _data.toList()
set(newData) {
val previousData = _data.toList()
- val diff = HomeDiffUtil(previousData, newData, context)
+ val diff = HomeDiffUtil(previousData, newData, context, configFactory)
val diffResult = DiffUtil.calculateDiff(diff)
_data = newData
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt
index b883709c0a..0fe93d41de 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt
@@ -3,11 +3,14 @@ package org.thoughtcrime.securesms.home
import android.content.Context
import androidx.recyclerview.widget.DiffUtil
import org.thoughtcrime.securesms.database.model.ThreadRecord
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
+import org.thoughtcrime.securesms.util.getConversationUnread
class HomeDiffUtil(
private val old: List,
private val new: List,
- private val context: Context
+ private val context: Context,
+ private val configFactory: ConfigFactory
): DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size
@@ -42,7 +45,9 @@ class HomeDiffUtil(
oldItem.isFailed == newItem.isFailed &&
oldItem.isDelivered == newItem.isDelivered &&
oldItem.isSent == newItem.isSent &&
- oldItem.isPending == newItem.isPending
+ oldItem.isPending == newItem.isPending &&
+ oldItem.lastSeen == newItem.lastSeen &&
+ configFactory.convoVolatile?.getConversationUnread(newItem) != true
)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt
index 947bd89b4e..7ab7bfb508 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/PathStatusView.kt
@@ -9,9 +9,14 @@ import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import androidx.annotation.ColorInt
+import androidx.lifecycle.coroutineScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.session.libsession.snode.OnionRequestAPI
+import org.thoughtcrime.securesms.conversation.v2.ViewUtil
import org.thoughtcrime.securesms.util.getColorWithID
import org.thoughtcrime.securesms.util.toPx
@@ -29,6 +34,8 @@ class PathStatusView : View {
result
}
+ private var updateJob: Job? = null
+
constructor(context: Context) : super(context) {
initialize()
}
@@ -87,16 +94,21 @@ class PathStatusView : View {
private fun handlePathsBuiltEvent() { update() }
private fun update() {
- if (OnionRequestAPI.paths.isNotEmpty()) {
- setBackgroundResource(R.drawable.accent_dot)
- val hasPathsColor = context.getColor(R.color.accent_green)
- mainColor = hasPathsColor
- sessionShadowColor = hasPathsColor
- } else {
- setBackgroundResource(R.drawable.paths_building_dot)
- val pathsBuildingColor = resources.getColorWithID(R.color.paths_building, context.theme)
- mainColor = pathsBuildingColor
- sessionShadowColor = pathsBuildingColor
+ if (updateJob?.isActive != true) { // false or null
+ updateJob = ViewUtil.getActivityLifecycle(this)?.coroutineScope?.launchWhenStarted {
+ val paths = withContext(Dispatchers.IO) { OnionRequestAPI.paths }
+ if (paths.isNotEmpty()) {
+ setBackgroundResource(R.drawable.accent_dot)
+ val hasPathsColor = context.getColor(R.color.accent_green)
+ mainColor = hasPathsColor
+ sessionShadowColor = hasPathsColor
+ } else {
+ setBackgroundResource(R.drawable.paths_building_dot)
+ val pathsBuildingColor = resources.getColorWithID(R.color.paths_building, context.theme)
+ mainColor = pathsBuildingColor
+ sessionShadowColor = pathsBuildingColor
+ }
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
index bc9a9beced..ad8f2d0421 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
@@ -25,9 +25,7 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.IdPrefix
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.ThreadDatabase
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideApp
-import org.thoughtcrime.securesms.util.UiModeUtilities
import javax.inject.Inject
@AndroidEntryPoint
@@ -55,12 +53,12 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
with(binding) {
- profilePictureView.root.publicKey = publicKey
- profilePictureView.root.glide = GlideApp.with(this@UserDetailsBottomSheet)
- profilePictureView.root.isLarge = true
- profilePictureView.root.update(recipient)
+ profilePictureView.publicKey = publicKey
+ profilePictureView.isLarge = true
+ profilePictureView.update(recipient)
nameTextViewContainer.visibility = View.VISIBLE
nameTextViewContainer.setOnClickListener {
+ if (recipient.isOpenGroupInboxRecipient || recipient.isOpenGroupOutboxRecipient) return@setOnClickListener
nameTextViewContainer.visibility = View.INVISIBLE
nameEditTextContainer.visibility = View.VISIBLE
nicknameEditText.text = null
@@ -87,8 +85,14 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
}
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
- publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient && !threadRecipient.isOpenGroupInboxRecipient
- messageButton.isVisible = !threadRecipient.isOpenGroupRecipient || IdPrefix.fromValue(publicKey) == IdPrefix.BLINDED
+ nameEditIcon.isVisible = threadRecipient.isContactRecipient
+ && !threadRecipient.isOpenGroupInboxRecipient
+ && !threadRecipient.isOpenGroupOutboxRecipient
+
+ publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
+ && !threadRecipient.isOpenGroupInboxRecipient
+ && !threadRecipient.isOpenGroupOutboxRecipient
+ messageButton.isVisible = !threadRecipient.isOpenGroupRecipient || IdPrefix.fromValue(publicKey)?.isBlinded() == true
publicKeyTextView.text = publicKey
publicKeyTextView.setOnLongClickListener {
val clipboard =
@@ -130,10 +134,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
newNickName = nicknameEditText.text.toString()
}
val publicKey = recipient.address.serialize()
- val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
- val contact = contactDB.getContactWithSessionID(publicKey) ?: Contact(publicKey)
+ val storage = MessagingModuleConfiguration.shared.storage
+ val contact = storage.getContactWithSessionID(publicKey) ?: Contact(publicKey)
contact.nickname = newNickName
- contactDB.setContact(contact)
+ storage.setContact(contact)
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt
index fab8bca998..7cf953be24 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt
@@ -83,22 +83,20 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is ContentView) {
- holder.binding.searchResultProfilePicture.root.recycle()
+ holder.binding.searchResultProfilePicture.recycle()
}
}
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
- val binding = ViewGlobalSearchResultBinding.bind(view).apply {
- searchResultProfilePicture.root.glide = GlideApp.with(root)
- }
+ val binding = ViewGlobalSearchResultBinding.bind(view)
fun bindPayload(newQuery: String, model: Model) {
bindQuery(newQuery, model)
}
fun bind(query: String, model: Model) {
- binding.searchResultProfilePicture.root.recycle()
+ binding.searchResultProfilePicture.recycle()
when (model) {
is Model.GroupConversation -> bindModel(query, model)
is Model.Contact -> bindModel(query, model)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt
index 2c64ded866..5371bb71c9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt
@@ -12,6 +12,7 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation
+import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Header
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages
import org.thoughtcrime.securesms.util.DateUtils
@@ -76,6 +77,8 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) {
}
binding.searchResultSubtitle.text = getHighlight(query, membersString)
}
+ is Header, // do nothing for header
+ is SavedMessages -> Unit // do nothing for saved messages (displays note to self)
}
}
@@ -84,12 +87,12 @@ private fun getHighlight(query: String?, toSearch: String): Spannable? {
}
fun ContentView.bindModel(query: String?, model: GroupConversation) {
- binding.searchResultProfilePicture.root.isVisible = true
+ binding.searchResultProfilePicture.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.root.update(threadRecipient)
+ binding.searchResultProfilePicture.update(threadRecipient)
val nameString = model.groupRecord.title
binding.searchResultTitle.text = getHighlight(query, nameString)
@@ -105,14 +108,14 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) {
}
fun ContentView.bindModel(query: String?, model: ContactModel) {
- binding.searchResultProfilePicture.root.isVisible = true
+ binding.searchResultProfilePicture.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.root.update(recipient)
+ binding.searchResultProfilePicture.update(recipient)
val nameString = model.contact.getSearchName()
binding.searchResultTitle.text = getHighlight(query, nameString)
}
@@ -121,12 +124,12 @@ fun ContentView.bindModel(model: SavedMessages) {
binding.searchResultSubtitle.isVisible = false
binding.searchResultTimestamp.isVisible = false
binding.searchResultTitle.setText(R.string.note_to_self)
- binding.searchResultProfilePicture.root.isVisible = false
+ binding.searchResultProfilePicture.isVisible = false
binding.searchResultSavedMessages.isVisible = true
}
fun ContentView.bindModel(query: String?, model: Message) {
- binding.searchResultProfilePicture.root.isVisible = true
+ binding.searchResultProfilePicture.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultTimestamp.isVisible = true
// val hasUnreads = model.unread > 0
@@ -135,7 +138,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.sentTimestampMs)
- binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient)
+ binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient)
val textSpannable = SpannableStringBuilder()
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
// group chat, bind
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt
index 07da14b090..a85ea525ae 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt
@@ -154,7 +154,7 @@ class KeyboardPageSearchView @JvmOverloads constructor(
.setDuration(REVEAL_DURATION)
.alpha(0f)
.setListener(object : AnimationCompleteListener() {
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
visibility = INVISIBLE
}
})
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt
index 9a8d061297..af3d269c6a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt
@@ -34,7 +34,6 @@ class MessageRequestView : LinearLayout {
// region Updating
fun bind(thread: ThreadRecord, glide: GlideRequests) {
this.thread = thread
- binding.profilePictureView.root.glide = glide
val senderDisplayName = getUserDisplayName(thread.recipient)
?: thread.recipient.address.toString()
binding.displayNameTextView.text = senderDisplayName
@@ -44,12 +43,12 @@ class MessageRequestView : LinearLayout {
binding.snippetTextView.text = snippet
post {
- binding.profilePictureView.root.update(thread.recipient)
+ binding.profilePictureView.update(thread.recipient)
}
}
fun recycle() {
- binding.profilePictureView.root.recycle()
+ binding.profilePictureView.recycle()
}
private fun getUserDisplayName(recipient: Recipient): String? {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt
index 50ed4628ea..caecbcd87d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.messagerequests
-import android.app.AlertDialog
import android.content.Intent
import android.database.Cursor
import android.os.Bundle
@@ -20,6 +19,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
+import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.push
import javax.inject.Inject
@@ -49,7 +49,7 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat
adapter.glide = glide
binding.recyclerView.adapter = adapter
- binding.clearAllMessageRequestsButton.setOnClickListener { deleteAllAndBlock() }
+ binding.clearAllMessageRequestsButton.setOnClickListener { deleteAll() }
}
override fun onResume() {
@@ -77,34 +77,34 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat
}
override fun onBlockConversationClick(thread: ThreadRecord) {
- val dialog = AlertDialog.Builder(this)
- dialog.setTitle(R.string.RecipientPreferenceActivity_block_this_contact_question)
- .setMessage(R.string.message_requests_block_message)
- .setPositiveButton(R.string.recipient_preferences__block) { _, _ ->
- viewModel.blockMessageRequest(thread)
- LoaderManager.getInstance(this).restartLoader(0, null, this)
- }
- .setNegativeButton(R.string.no) { _, _ ->
- // Do nothing
- }
- dialog.create().show()
+ fun doBlock() {
+ viewModel.blockMessageRequest(thread)
+ LoaderManager.getInstance(this).restartLoader(0, null, this)
+ }
+
+ showSessionDialog {
+ title(R.string.RecipientPreferenceActivity_block_this_contact_question)
+ text(R.string.message_requests_block_message)
+ button(R.string.recipient_preferences__block) { doBlock() }
+ button(R.string.no)
+ }
}
override fun onDeleteConversationClick(thread: ThreadRecord) {
- val dialog = AlertDialog.Builder(this)
- dialog.setTitle(R.string.decline)
- .setMessage(resources.getString(R.string.message_requests_decline_message))
- .setPositiveButton(R.string.decline) { _,_ ->
- viewModel.deleteMessageRequest(thread)
- LoaderManager.getInstance(this).restartLoader(0, null, this)
- lifecycleScope.launch(Dispatchers.IO) {
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity)
- }
+ fun doDecline() {
+ viewModel.deleteMessageRequest(thread)
+ LoaderManager.getInstance(this).restartLoader(0, null, this)
+ lifecycleScope.launch(Dispatchers.IO) {
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity)
}
- .setNegativeButton(R.string.no) { _, _ ->
- // Do nothing
- }
- dialog.create().show()
+ }
+
+ showSessionDialog {
+ title(R.string.decline)
+ text(resources.getString(R.string.message_requests_decline_message))
+ button(R.string.decline) { doDecline() }
+ button(R.string.no)
+ }
}
private fun updateEmptyState() {
@@ -113,19 +113,19 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat
binding.clearAllMessageRequestsButton.isVisible = threadCount != 0
}
- private fun deleteAllAndBlock() {
- val dialog = AlertDialog.Builder(this)
- dialog.setMessage(resources.getString(R.string.message_requests_clear_all_message))
- dialog.setPositiveButton(R.string.yes) { _, _ ->
- viewModel.clearAllMessageRequests()
+ private fun deleteAll() {
+ fun doDeleteAllAndBlock() {
+ viewModel.clearAllMessageRequests(false)
LoaderManager.getInstance(this).restartLoader(0, null, this)
lifecycleScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity)
}
}
- dialog.setNegativeButton(R.string.no) { _, _ ->
- // Do nothing
+
+ showSessionDialog {
+ text(resources.getString(R.string.message_requests_clear_all_message))
+ button(R.string.yes) { doDeleteAllAndBlock() }
+ button(R.string.no)
}
- dialog.create().show()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt
index 89a841dc0a..10142cc8fc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt
@@ -48,6 +48,7 @@ class MessageRequestsAdapter(
private fun showPopupMenu(view: MessageRequestView) {
val popupMenu = PopupMenu(ContextThemeWrapper(context, R.style.PopupMenu_MessageRequests), view)
popupMenu.menuInflater.inflate(R.menu.menu_message_request, popupMenu.menu)
+ popupMenu.menu.findItem(R.id.menu_block_message_request)?.isVisible = !view.thread!!.recipient.isOpenGroupInboxRecipient
popupMenu.setOnMenuItemClickListener { menuItem ->
if (menuItem.itemId == R.id.menu_delete_message_request) {
listener.onDeleteConversationClick(view.thread!!)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt
index 2f448932dd..a3a7caf8d2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModel.kt
@@ -25,8 +25,8 @@ class MessageRequestsViewModel @Inject constructor(
repository.deleteMessageRequest(thread)
}
- fun clearAllMessageRequests() = viewModelScope.launch {
- repository.clearAllMessageRequests()
+ fun clearAllMessageRequests(block: Boolean) = viewModelScope.launch {
+ repository.clearAllMessageRequests(block)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
index 327492e95c..0157d8ad41 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
@@ -60,7 +60,6 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilit
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities;
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
import org.thoughtcrime.securesms.database.LokiThreadDatabase;
-import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
@@ -160,8 +159,9 @@ public class DefaultMessageNotifier implements MessageNotifier {
executor.cancel();
}
- private void cancelActiveNotifications(@NonNull Context context) {
+ private boolean cancelActiveNotifications(@NonNull Context context) {
NotificationManager notifications = ServiceUtil.getNotificationManager(context);
+ boolean hasNotifications = notifications.getActiveNotifications().length > 0;
notifications.cancel(SUMMARY_NOTIFICATION_ID);
try {
@@ -175,6 +175,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
Log.w(TAG, e);
notifications.cancelAll();
}
+ return hasNotifications;
}
private void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) {
@@ -240,10 +241,6 @@ public class DefaultMessageNotifier implements MessageNotifier {
!(recipient.isApproved() || threads.getLastSeenAndHasSent(threadId).second())) {
TextSecurePreferences.removeHasHiddenMessageRequests(context);
}
- if (isVisible && recipient != null) {
- List messageIds = threads.setRead(threadId, false);
- if (SessionMetaProtocol.shouldSendReadReceipt(recipient)) { MarkReadReceiver.process(context, messageIds); }
- }
if (!TextSecurePreferences.isNotificationsEnabled(context) ||
(recipient != null && recipient.isMuted()))
@@ -251,11 +248,21 @@ public class DefaultMessageNotifier implements MessageNotifier {
return;
}
- if (!isVisible && !homeScreenVisible) {
+ if ((!isVisible && !homeScreenVisible) || hasExistingNotifications(context)) {
updateNotification(context, signal, 0);
}
}
+ private boolean hasExistingNotifications(Context context) {
+ NotificationManager notifications = ServiceUtil.getNotificationManager(context);
+ try {
+ StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
+ return activeNotifications.length > 0;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
@Override
public void updateNotification(@NonNull Context context, boolean signal, int reminderCount)
{
@@ -267,8 +274,8 @@ public class DefaultMessageNotifier implements MessageNotifier {
if ((telcoCursor == null || telcoCursor.isAfterLast()) || !TextSecurePreferences.hasSeenWelcomeScreen(context))
{
- cancelActiveNotifications(context);
updateBadge(context, 0);
+ cancelActiveNotifications(context);
clearReminder(context);
return;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java
index 6075be65e7..309f2732f8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java
@@ -12,6 +12,8 @@ import androidx.core.app.NotificationManagerCompat;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
+import org.session.libsession.database.StorageProtocol;
+import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.messages.control.ReadReceipt;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.snode.SnodeAPI;
@@ -27,7 +29,6 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -52,18 +53,12 @@ public class MarkReadReceiver extends BroadcastReceiver {
new AsyncTask() {
@Override
protected Void doInBackground(Void... params) {
- List messageIdsCollection = new LinkedList<>();
-
+ long currentTime = SnodeAPI.getNowWithOffset();
for (long threadId : threadIds) {
Log.i(TAG, "Marking as read: " + threadId);
- List messageIds = DatabaseComponent.get(context).threadDatabase().setRead(threadId, true);
- messageIdsCollection.addAll(messageIds);
+ StorageProtocol storage = MessagingModuleConfiguration.getShared().getStorage();
+ storage.markConversationAsRead(threadId,currentTime, true);
}
-
- process(context, messageIdsCollection);
-
- ApplicationContext.getInstance(context).messageNotifier.updateNotification(context);
-
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt
index 31117ae94d..edd1bc274a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt
@@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
+import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
@@ -34,12 +35,19 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
+import javax.inject.Inject
+@AndroidEntryPoint
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
+
+ @Inject
+ lateinit var configFactory: ConfigFactory
+
private lateinit var binding: ActivityLinkDeviceBinding
internal val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
@@ -112,6 +120,7 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
val keyPairGenerationResult = KeyPairUtilities.generate(seed)
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
KeyPairUtilities.store(this@LinkDeviceActivity, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair)
+ configFactory.keyPairChanged()
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
val registrationID = KeyHelper.generateRegistrationId(false)
TextSecurePreferences.setLocalRegistrationId(this@LinkDeviceActivity, registrationID)
@@ -124,9 +133,8 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
.setAction(R.string.registration_activity__skip) { register(true) }
val skipJob = launch {
- delay(30_000L)
+ delay(15_000L)
snackBar.show()
- // show a dialog or something saying do you want to skip this bit?
}
// start polling and wait for updated message
ApplicationContext.getInstance(this@LinkDeviceActivity).apply {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt
index 9cf9c3d049..2de6269536 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.onboarding
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
-import android.app.AlertDialog
import android.content.Intent
import android.graphics.drawable.TransitionDrawable
import android.net.Uri
@@ -20,6 +19,7 @@ import org.session.libsession.utilities.ThemeUtil
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.home.HomeActivity
+import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.PNModeView
import org.thoughtcrime.securesms.util.disableClipping
@@ -151,18 +151,20 @@ class PNModeActivity : BaseActionBarActivity() {
private fun register() {
if (selectedOptionView == null) {
- val dialog = AlertDialog.Builder(this)
- dialog.setTitle(R.string.activity_pn_mode_no_option_picked_dialog_title)
- dialog.setPositiveButton(R.string.ok) { _, _ -> }
- dialog.create().show()
+ showSessionDialog {
+ title(R.string.activity_pn_mode_no_option_picked_dialog_title)
+ button(R.string.ok)
+ }
return
}
+
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView))
val application = ApplicationContext.getInstance(this)
application.startPollingIfNeeded()
application.registerForFCMIfNeeded(true)
val intent = Intent(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ intent.putExtra(HomeActivity.FROM_ONBOARDING, true)
show(intent)
}
// endregion
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt
index 5531fea491..051cd7542e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt
@@ -11,6 +11,7 @@ import android.text.style.ClickableSpan
import android.text.style.StyleSpan
import android.view.View
import android.widget.Toast
+import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
import org.session.libsession.snode.SnodeModule
@@ -23,10 +24,17 @@ import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
+import javax.inject.Inject
+@AndroidEntryPoint
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
+
+ @Inject
+ lateinit var configFactory: ConfigFactory
+
private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
internal val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
@@ -81,6 +89,7 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
val keyPairGenerationResult = KeyPairUtilities.generate(seed)
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
KeyPairUtilities.store(this, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair)
+ configFactory.keyPairChanged()
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
val registrationID = KeyHelper.generateRegistrationId(false)
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt
index b6fdaf9cf9..6e082e0008 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt
@@ -16,6 +16,7 @@ import android.text.style.StyleSpan
import android.view.View
import android.widget.Toast
import com.goterl.lazysodium.utils.KeyPair
+import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityRegisterBinding
import org.session.libsession.snode.SnodeModule
@@ -26,10 +27,17 @@ import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
+import javax.inject.Inject
+@AndroidEntryPoint
class RegisterActivity : BaseActionBarActivity() {
+
+ @Inject
+ lateinit var configFactory: ConfigFactory
+
private lateinit var binding: ActivityRegisterBinding
internal val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
@@ -119,6 +127,7 @@ class RegisterActivity : BaseActionBarActivity() {
database.clearReceivedMessageHashValues()
KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!)
+ configFactory.keyPairChanged()
val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey
val registrationID = KeyHelper.generateRegistrationId(false)
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java
index 2d7e6dae59..88ee67cb4d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java
@@ -162,15 +162,13 @@ public class Permissions {
request.onResult(requestedPermissions, grantResults, new boolean[requestedPermissions.length]);
}
- @SuppressWarnings("ConstantConditions")
private void executePermissionsRequestWithRationale(PermissionsRequest request) {
- AlertDialog dialog = RationaleDialog.createFor(permissionObject.getContext(), rationaleDialogMessage, rationalDialogHeader)
- .setPositiveButton(R.string.Permissions_continue, (d, which) -> executePermissionsRequest(request))
- .setNegativeButton(R.string.Permissions_not_now, (d, which) -> executeNoPermissionsRequest(request))
- .show();
- dialog.getWindow().setLayout((int)(permissionObject.getWindowWidth() * .75), ViewGroup.LayoutParams.WRAP_CONTENT);
- Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
- positiveButton.setContentDescription("Continue");
+ RationaleDialog.show(
+ permissionObject.getContext(),
+ rationaleDialogMessage,
+ () -> executePermissionsRequest(request),
+ () -> executeNoPermissionsRequest(request),
+ rationalDialogHeader);
}
private void executePermissionsRequest(PermissionsRequest request) {
@@ -257,7 +255,7 @@ public class Permissions {
resultListener.onResult(permissions, grantResults, shouldShowRationaleDialog);
}
- private static Intent getApplicationSettingsIntent(@NonNull Context context) {
+ static Intent getApplicationSettingsIntent(@NonNull Context context) {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", context.getPackageName(), null);
@@ -354,20 +352,8 @@ public class Permissions {
@Override
public void run() {
Context context = this.context.get();
-
- if (context != null) {
- AlertDialog alertDialog = new AlertDialog.Builder(context, R.style.ThemeOverlay_Session_AlertDialog)
- .setTitle(R.string.Permissions_permission_required)
- .setMessage(message)
- .setPositiveButton(R.string.Permissions_continue, (dialog, which) -> context.startActivity(getApplicationSettingsIntent(context)))
- .setNegativeButton(android.R.string.cancel, null)
- .create();
- Button positiveButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
- if (positiveButton != null) {
- positiveButton.setContentDescription(context.getString(R.string.AccessibilityId_continue));
- }
- alertDialog.show();
- }
+ if (context == null) return;
+ SettingsDialog.show(context, message);
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.java b/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.java
deleted file mode 100644
index a346d591ac..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.thoughtcrime.securesms.permissions;
-
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.graphics.Color;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.LinearLayout.LayoutParams;
-import android.widget.TextView;
-
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-
-import org.session.libsession.utilities.ViewUtil;
-
-import network.loki.messenger.R;
-
-public class RationaleDialog {
-
- public static AlertDialog.Builder createFor(@NonNull Context context, @NonNull String message, @DrawableRes int... drawables) {
- View view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null);
- view.setClipToOutline(true);
- ViewGroup header = view.findViewById(R.id.header_container);
- TextView text = view.findViewById(R.id.message);
-
- for (int i=0;i(R.id.header_container)
+ view.findViewById(R.id.message).text = message
+
+ fun addIcon(id: Int) {
+ ImageView(context).apply {
+ setImageDrawable(ResourcesCompat.getDrawable(context.resources, id, context.theme))
+ layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
+ }.also(header::addView)
+ }
+
+ fun addPlus() {
+ TextView(context).apply {
+ text = "+"
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 40f)
+ setTextColor(Color.WHITE)
+ layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
+ ViewUtil.dpToPx(context, 20).let { setMargins(it, 0, it, 0) }
+ }
+ }.also(header::addView)
+ }
+
+ drawables.firstOrNull()?.let(::addIcon)
+ drawables.drop(1).forEach { addPlus(); addIcon(it) }
+
+ return context.showSessionDialog {
+ view(view)
+ button(R.string.Permissions_continue) { onPositive.run() }
+ button(R.string.Permissions_not_now) { onNegative.run() }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/SettingsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/permissions/SettingsDialog.kt
new file mode 100644
index 0000000000..a4efd8d870
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/SettingsDialog.kt
@@ -0,0 +1,21 @@
+package org.thoughtcrime.securesms.permissions
+
+import android.content.Context
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.showSessionDialog
+
+class SettingsDialog {
+ companion object {
+ @JvmStatic
+ fun show(context: Context, message: String) {
+ context.showSessionDialog {
+ title(R.string.Permissions_permission_required)
+ text(message)
+ button(R.string.Permissions_continue, R.string.AccessibilityId_continue) {
+ context.startActivity(Permissions.getApplicationSettingsIntent(context))
+ }
+ cancelButton()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt
index a66dd7428c..16499cc4bc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.preferences
-import android.app.AlertDialog
import android.os.Bundle
import androidx.activity.viewModels
import androidx.core.view.isVisible
@@ -8,6 +7,7 @@ import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityBlockedContactsBinding
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
+import org.thoughtcrime.securesms.showSessionDialog
@AndroidEntryPoint
class BlockedContactsActivity: PassphraseRequiredActionBarActivity() {
@@ -19,17 +19,12 @@ class BlockedContactsActivity: PassphraseRequiredActionBarActivity() {
val adapter: BlockedContactsAdapter by lazy { BlockedContactsAdapter(viewModel) }
fun unblock() {
- // show dialog
- val title = viewModel.getTitle(this)
-
- val message = viewModel.getMessage(this)
-
- AlertDialog.Builder(this)
- .setTitle(title)
- .setMessage(message)
- .setPositiveButton(R.string.continue_2) { _, _ -> viewModel.unblock(this@BlockedContactsActivity) }
- .setNegativeButton(R.string.cancel) { _, _ -> }
- .show()
+ showSessionDialog {
+ title(viewModel.getTitle(this@BlockedContactsActivity))
+ text(viewModel.getMessage(this@BlockedContactsActivity))
+ button(R.string.continue_2) { viewModel.unblock() }
+ cancelButton()
+ }
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
@@ -51,4 +46,3 @@ class BlockedContactsActivity: PassphraseRequiredActionBarActivity() {
}
}
-
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt
index a75d53c4f1..e0b92bdbea 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt
@@ -38,7 +38,7 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
- holder.binding.profilePictureView.root.recycle()
+ holder.binding.profilePictureView.recycle()
}
class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
@@ -48,8 +48,7 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap
fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) {
binding.recipientName.text = selectable.item.name
- with (binding.profilePictureView.root) {
- glide = this@ViewHolder.glide
+ with (binding.profilePictureView) {
update(selectable.item)
}
binding.root.setOnClickListener { toggle(selectable) }
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsLayout.kt
deleted file mode 100644
index ed2970fbc4..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsLayout.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.thoughtcrime.securesms.preferences
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.FrameLayout
-
-class BlockedContactsLayout @JvmOverloads constructor(
- context: Context, attrs: AttributeSet? = null
-) : FrameLayout(context, attrs)
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsPreference.kt
index e985ba6d4f..48c7cc6dc8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsPreference.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsPreference.kt
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.preferences
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
-import android.view.View
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceViewHolder
@@ -16,8 +15,7 @@ class BlockedContactsPreference @JvmOverloads constructor(
super.onBindViewHolder(holder)
holder.itemView.setOnClickListener {
- val intent = Intent(context, BlockedContactsActivity::class.java)
- context.startActivity(intent)
+ Intent(context, BlockedContactsActivity::class.java).let(context::startActivity)
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt
index acbba1ebb2..dbe09668c5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt
@@ -63,13 +63,9 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage)
return _state
}
- fun unblock(context: Context) {
- storage.unblock(state.selectedItems)
+ fun unblock() {
+ storage.setBlocked(state.selectedItems, false)
_state.value = state.copy(selectedItems = emptySet())
- // TODO: Remove in UserConfig branch
- GlobalScope.launch(Dispatchers.IO) {
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
- }
}
fun select(selectedItem: Recipient, isSelected: Boolean) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/CallToggleListener.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/CallToggleListener.kt
new file mode 100644
index 0000000000..ea747798c8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/CallToggleListener.kt
@@ -0,0 +1,45 @@
+package org.thoughtcrime.securesms.preferences
+
+import android.Manifest
+import androidx.fragment.app.Fragment
+import androidx.preference.Preference
+import network.loki.messenger.R
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsession.utilities.TextSecurePreferences.Companion.setBooleanPreference
+import org.thoughtcrime.securesms.permissions.Permissions
+import org.thoughtcrime.securesms.showSessionDialog
+
+internal class CallToggleListener(
+ private val context: Fragment,
+ private val setCallback: (Boolean) -> Unit
+) : Preference.OnPreferenceChangeListener {
+
+ override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
+ if (newValue == false) return true
+
+ // check if we've shown the info dialog and check for microphone permissions
+ context.showSessionDialog {
+ title(R.string.dialog_voice_video_title)
+ text(R.string.dialog_voice_video_message)
+ button(R.string.dialog_link_preview_enable_button_title, R.string.AccessibilityId_enable) { requestMicrophonePermission() }
+ cancelButton()
+ }
+
+ return false
+ }
+
+ private fun requestMicrophonePermission() {
+ Permissions.with(context)
+ .request(Manifest.permission.RECORD_AUDIO)
+ .onAllGranted {
+ setBooleanPreference(
+ context.requireContext(),
+ TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED,
+ true
+ )
+ setCallback(true)
+ }
+ .onAnyDenied { setCallback(false) }
+ .execute()
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ChangeUiModeDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ChangeUiModeDialog.kt
deleted file mode 100644
index 3d5b9e2e99..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ChangeUiModeDialog.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.thoughtcrime.securesms.preferences
-
-import android.app.Dialog
-import android.os.Bundle
-import androidx.fragment.app.DialogFragment
-
-class ChangeUiModeDialog : DialogFragment() {
-
- companion object {
- const val TAG = "ChangeUiModeDialog"
- }
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val context = requireContext()
- return android.app.AlertDialog.Builder(context)
- .setTitle("TODO: remove this")
- .show()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt
index fa3be71307..37a54a4afc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt
@@ -1,9 +1,12 @@
package org.thoughtcrime.securesms.preferences
+import android.app.Dialog
+import android.os.Bundle
import android.view.LayoutInflater
-import androidx.appcompat.app.AlertDialog
+import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible
+import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import kotlinx.coroutines.Dispatchers
@@ -15,10 +18,10 @@ import network.loki.messenger.databinding.DialogClearAllDataBinding
import org.session.libsession.snode.SnodeAPI
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
+import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
-class ClearAllDataDialog : BaseDialog() {
+class ClearAllDataDialog : DialogFragment() {
private lateinit var binding: DialogClearAllDataBinding
enum class Steps {
@@ -35,7 +38,11 @@ class ClearAllDataDialog : BaseDialog() {
updateUI()
}
- override fun setContentView(builder: AlertDialog.Builder) {
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
+ view(createView())
+ }
+
+ private fun createView(): View {
binding = DialogClearAllDataBinding.inflate(LayoutInflater.from(requireContext()))
val device = RadioOption("deviceOnly", requireContext().getString(R.string.dialog_clear_all_data_clear_device_only))
val network = RadioOption("deviceAndNetwork", requireContext().getString(R.string.dialog_clear_all_data_clear_device_and_network))
@@ -62,8 +69,7 @@ class ClearAllDataDialog : BaseDialog() {
Steps.DELETING -> { /* do nothing intentionally */ }
}
}
- builder.setView(binding.root)
- builder.setCancelable(false)
+ return binding.root
}
private fun updateUI() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java
index badcbe66b8..8c3e6190ed 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java
@@ -24,8 +24,6 @@ import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.components.CustomDefaultPreference;
import org.thoughtcrime.securesms.conversation.v2.ViewUtil;
-import org.thoughtcrime.securesms.preferences.widgets.ColorPickerPreference;
-import org.thoughtcrime.securesms.preferences.widgets.ColorPickerPreferenceDialogFragmentCompat;
import network.loki.messenger.R;
@@ -60,9 +58,7 @@ public abstract class CorrectedPreferenceFragment extends PreferenceFragmentComp
public void onDisplayPreferenceDialog(Preference preference) {
DialogFragment dialogFragment = null;
- if (preference instanceof ColorPickerPreference) {
- dialogFragment = ColorPickerPreferenceDialogFragmentCompat.newInstance(preference.getKey());
- } else if (preference instanceof CustomDefaultPreference) {
+ if (preference instanceof CustomDefaultPreference) {
dialogFragment = CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.newInstance(preference.getKey());
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ListPreferenceDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ListPreferenceDialog.kt
index 2ba48f6e41..6f0998eecb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ListPreferenceDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ListPreferenceDialog.kt
@@ -1,41 +1,24 @@
package org.thoughtcrime.securesms.preferences
import android.content.Context
-import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.preference.ListPreference
-import network.loki.messenger.databinding.DialogListPreferenceBinding
+import org.thoughtcrime.securesms.showSessionDialog
fun listPreferenceDialog(
context: Context,
listPreference: ListPreference,
- dialogListener: () -> Unit
-) : AlertDialog {
+ onChange: () -> Unit
+) : AlertDialog = listPreference.run {
+ context.showSessionDialog {
+ val index = entryValues.indexOf(value)
+ val options = entries.map(CharSequence::toString).toTypedArray()
- val builder = AlertDialog.Builder(context)
-
- val binding = DialogListPreferenceBinding.inflate(LayoutInflater.from(context))
- binding.titleTextView.text = listPreference.dialogTitle
- binding.messageTextView.text = listPreference.dialogMessage
-
- builder.setView(binding.root)
-
- val dialog = builder.show()
-
- val valueIndex = listPreference.findIndexOfValue(listPreference.value)
- RadioOptionAdapter(valueIndex) {
- listPreference.value = it.value
- dialog.dismiss()
- dialogListener()
- }
- .apply {
- listPreference.entryValues.zip(listPreference.entries) { value, title ->
- RadioOption(value.toString(), title.toString())
- }.let(this::submitList)
+ title(dialogTitle)
+ text(dialogMessage)
+ singleChoiceItems(options, index) {
+ listPreference.setValueIndex(it)
+ onChange()
}
- .let { binding.recyclerView.adapter = it }
-
- binding.closeButton.setOnClickListener { dialog.dismiss() }
-
- return dialog
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.java
deleted file mode 100644
index ac03efa362..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.java
+++ /dev/null
@@ -1,203 +0,0 @@
-package org.thoughtcrime.securesms.preferences;
-
-import android.Manifest;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.KeyguardManager;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.Settings;
-import android.widget.Button;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.view.ContextThemeWrapper;
-import androidx.fragment.app.Fragment;
-import androidx.preference.Preference;
-
-import org.session.libsession.utilities.TextSecurePreferences;
-import org.thoughtcrime.securesms.ApplicationContext;
-import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
-import org.thoughtcrime.securesms.permissions.Permissions;
-import org.thoughtcrime.securesms.service.KeyCachingService;
-import org.thoughtcrime.securesms.util.CallNotificationBuilder;
-import org.thoughtcrime.securesms.util.IntentUtils;
-
-import kotlin.jvm.functions.Function1;
-import network.loki.messenger.BuildConfig;
-import network.loki.messenger.R;
-
-public class PrivacySettingsPreferenceFragment extends ListSummaryPreferenceFragment {
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- }
-
- @Override
- public void onCreate(Bundle paramBundle) {
- super.onCreate(paramBundle);
-
- this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener());
-
- this.findPreference(TextSecurePreferences.READ_RECEIPTS_PREF).setOnPreferenceChangeListener(new ReadReceiptToggleListener());
- this.findPreference(TextSecurePreferences.TYPING_INDICATORS).setOnPreferenceChangeListener(new TypingIndicatorsToggleListener());
- this.findPreference(TextSecurePreferences.LINK_PREVIEWS).setOnPreferenceChangeListener(new LinkPreviewToggleListener());
- this.findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED).setOnPreferenceChangeListener(new CallToggleListener(this, this::setCall));
-
- initializeVisibility();
- }
-
- private Void setCall(boolean isEnabled) {
- ((SwitchPreferenceCompat)findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)).setChecked(isEnabled);
- if (isEnabled && !CallNotificationBuilder.areNotificationsEnabled(requireActivity())) {
- // show a dialog saying that calls won't work properly if you don't have notifications on at a system level
- new AlertDialog.Builder(new ContextThemeWrapper(requireActivity(), R.style.ThemeOverlay_Session_AlertDialog))
- .setTitle(R.string.CallNotificationBuilder_system_notification_title)
- .setMessage(R.string.CallNotificationBuilder_system_notification_message)
- .setPositiveButton(R.string.activity_notification_settings_title, (d, w) -> {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- Intent settingsIntent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID);
- if (IntentUtils.isResolvable(requireContext(), settingsIntent)) {
- startActivity(settingsIntent);
- }
- } else {
- Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .setData(Uri.parse("package:"+BuildConfig.APPLICATION_ID));
- if (IntentUtils.isResolvable(requireContext(), settingsIntent)) {
- startActivity(settingsIntent);
- }
- }
- d.dismiss();
- })
- .setNeutralButton(R.string.dismiss, (d, w) -> {
- // do nothing, user might have broken notifications
- d.dismiss();
- })
- .show();
- }
- return null;
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
- }
-
- @Override
- public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
- addPreferencesFromResource(R.xml.preferences_app_protection);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- }
-
- private void initializeVisibility() {
- if (TextSecurePreferences.isPasswordDisabled(getContext())) {
- KeyguardManager keyguardManager = (KeyguardManager)getContext().getSystemService(Context.KEYGUARD_SERVICE);
- if (!keyguardManager.isKeyguardSecure()) {
- ((SwitchPreferenceCompat)findPreference(TextSecurePreferences.SCREEN_LOCK)).setChecked(false);
- findPreference(TextSecurePreferences.SCREEN_LOCK).setEnabled(false);
- }
- } else {
- findPreference(TextSecurePreferences.SCREEN_LOCK).setVisible(false);
- findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setVisible(false);
- }
- }
-
- private class ScreenLockListener implements Preference.OnPreferenceChangeListener {
- @Override
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- boolean enabled = (Boolean)newValue;
-
- TextSecurePreferences.setScreenLockEnabled(getContext(), enabled);
-
- Intent intent = new Intent(getContext(), KeyCachingService.class);
- intent.setAction(KeyCachingService.LOCK_TOGGLED_EVENT);
- getContext().startService(intent);
- return true;
- }
- }
-
- private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener {
- @Override
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- return true;
- }
- }
-
- private class TypingIndicatorsToggleListener implements Preference.OnPreferenceChangeListener {
- @Override
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- boolean enabled = (boolean)newValue;
-
- if (!enabled) {
- ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().clear();
- }
-
- return true;
- }
- }
-
- private class LinkPreviewToggleListener implements Preference.OnPreferenceChangeListener {
- @Override
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- return true;
- }
- }
-
- private class CallToggleListener implements Preference.OnPreferenceChangeListener {
-
- private final Fragment context;
- private final Function1 setCallback;
-
- private CallToggleListener(Fragment context, Function1 setCallback) {
- this.context = context;
- this.setCallback = setCallback;
- }
-
- private void requestMicrophonePermission() {
- Permissions.with(context)
- .request(Manifest.permission.RECORD_AUDIO)
- .onAllGranted(() -> {
- TextSecurePreferences.setBooleanPreference(context.requireContext(), TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED, true);
- setCallback.invoke(true);
- })
- .onAnyDenied(() -> setCallback.invoke(false))
- .execute();
- }
-
- @Override
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- boolean val = (boolean) newValue;
- if (val) {
- // check if we've shown the info dialog and check for microphone permissions
- AlertDialog dialog = new AlertDialog.Builder(new ContextThemeWrapper(context.requireContext(), R.style.ThemeOverlay_Session_AlertDialog))
- .setTitle(R.string.dialog_voice_video_title)
- .setMessage(R.string.dialog_voice_video_message)
- .setPositiveButton(R.string.dialog_link_preview_enable_button_title, (d, w) -> {
- requestMicrophonePermission();
- })
- .setNegativeButton(R.string.cancel, (d, w) -> {
-
- })
- .show();
- Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
- positiveButton.setContentDescription("Enable");
- return false;
- } else {
- return true;
- }
- }
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt
new file mode 100644
index 0000000000..eaf48f8688
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt
@@ -0,0 +1,114 @@
+package org.thoughtcrime.securesms.preferences
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import androidx.preference.Preference
+import network.loki.messenger.BuildConfig
+import network.loki.messenger.R
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsession.utilities.TextSecurePreferences.Companion.isPasswordDisabled
+import org.session.libsession.utilities.TextSecurePreferences.Companion.setScreenLockEnabled
+import org.thoughtcrime.securesms.ApplicationContext
+import org.thoughtcrime.securesms.components.SwitchPreferenceCompat
+import org.thoughtcrime.securesms.permissions.Permissions
+import org.thoughtcrime.securesms.service.KeyCachingService
+import org.thoughtcrime.securesms.showSessionDialog
+import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.areNotificationsEnabled
+import org.thoughtcrime.securesms.util.IntentUtils
+
+class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
+ override fun onCreate(paramBundle: Bundle?) {
+ super.onCreate(paramBundle)
+ findPreference(TextSecurePreferences.SCREEN_LOCK)!!
+ .onPreferenceChangeListener = ScreenLockListener()
+ findPreference(TextSecurePreferences.TYPING_INDICATORS)!!
+ .onPreferenceChangeListener = TypingIndicatorsToggleListener()
+ findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!!
+ .onPreferenceChangeListener = CallToggleListener(this) { setCall(it) }
+ initializeVisibility()
+ }
+
+ private fun setCall(isEnabled: Boolean) {
+ (findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED) as SwitchPreferenceCompat?)!!.isChecked =
+ isEnabled
+ if (isEnabled && !areNotificationsEnabled(requireActivity())) {
+ // show a dialog saying that calls won't work properly if you don't have notifications on at a system level
+ showSessionDialog {
+ title(R.string.CallNotificationBuilder_system_notification_title)
+ text(R.string.CallNotificationBuilder_system_notification_message)
+ button(R.string.activity_notification_settings_title) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
+ .putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID)
+ } else {
+ Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID))
+ }
+ .apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
+ .takeIf { IntentUtils.isResolvable(requireContext(), it) }.let {
+ startActivity(it)
+ }
+ }
+ button(R.string.dismiss)
+ }
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
+ }
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ addPreferencesFromResource(R.xml.preferences_app_protection)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ }
+
+ private fun initializeVisibility() {
+ if (isPasswordDisabled(requireContext())) {
+ val keyguardManager =
+ requireContext().getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
+ if (!keyguardManager.isKeyguardSecure) {
+ findPreference(TextSecurePreferences.SCREEN_LOCK)!!.isChecked = false
+ findPreference(TextSecurePreferences.SCREEN_LOCK)!!.isEnabled = false
+ }
+ } else {
+ findPreference(TextSecurePreferences.SCREEN_LOCK)!!.isVisible = false
+ findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT)!!.isVisible = false
+ }
+ }
+
+ private inner class ScreenLockListener : Preference.OnPreferenceChangeListener {
+ override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
+ val enabled = newValue as Boolean
+ setScreenLockEnabled(context!!, enabled)
+ val intent = Intent(context, KeyCachingService::class.java)
+ intent.action = KeyCachingService.LOCK_TOGGLED_EVENT
+ context!!.startService(intent)
+ return true
+ }
+ }
+
+ private inner class TypingIndicatorsToggleListener : Preference.OnPreferenceChangeListener {
+ override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
+ val enabled = newValue as Boolean
+ if (!enabled) {
+ ApplicationContext.getInstance(requireContext()).typingStatusRepository.clear()
+ }
+ return true
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt
index e7bfd60d3f..bae5f19605 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt
@@ -1,38 +1,34 @@
package org.thoughtcrime.securesms.preferences
+import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
-import android.view.LayoutInflater
+import android.os.Bundle
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
import network.loki.messenger.R
-import network.loki.messenger.databinding.DialogSeedBinding
import org.session.libsignal.crypto.MnemonicCodec
import org.session.libsignal.utilities.hexEncodedPrivateKey
+import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
-
-class SeedDialog : BaseDialog() {
+class SeedDialog: DialogFragment() {
private val seed by lazy {
- var hexEncodedSeed = IdentityKeyUtil.retrieve(requireContext(), IdentityKeyUtil.LOKI_SEED)
- if (hexEncodedSeed == null) {
- hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(requireContext()).hexEncodedPrivateKey // Legacy account
- }
- val loadFileContents: (String) -> String = { fileName ->
- MnemonicUtilities.loadFileContents(requireContext(), fileName)
- }
- MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
+ val hexEncodedSeed = IdentityKeyUtil.retrieve(requireContext(), IdentityKeyUtil.LOKI_SEED)
+ ?: IdentityKeyUtil.getIdentityKeyPair(requireContext()).hexEncodedPrivateKey // Legacy account
+
+ MnemonicCodec { fileName -> MnemonicUtilities.loadFileContents(requireContext(), fileName) }
+ .encode(hexEncodedSeed, MnemonicCodec.Language.Configuration.english)
}
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogSeedBinding.inflate(LayoutInflater.from(requireContext()))
- binding.seedTextView.text = seed
- binding.closeButton.setOnClickListener { dismiss() }
- binding.copyButton.setOnClickListener { copySeed() }
- builder.setView(binding.root)
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
+ title(R.string.dialog_seed_title)
+ text(R.string.dialog_seed_explanation)
+ text(seed, R.style.SessionIDTextView)
+ button(R.string.copy, R.string.AccessibilityId_copy_recovery_phrase) { copySeed() }
+ button(R.string.close) { dismiss() }
}
private fun copySeed() {
@@ -42,4 +38,4 @@ class SeedDialog : BaseDialog() {
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
dismiss()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
index 5a03cebc37..5f24855760 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
@@ -2,7 +2,10 @@ package org.thoughtcrime.securesms.preferences
import android.Manifest
import android.app.Activity
-import android.content.*
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
@@ -16,16 +19,19 @@ import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
+import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.BuildConfig
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySettingsBinding
+import network.loki.messenger.libsession_util.util.UserPic
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.avatars.AvatarHelper
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.snode.SnodeAPI
import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.utilities.*
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
@@ -33,6 +39,7 @@ import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection
import org.thoughtcrime.securesms.components.ProfilePictureView
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.home.PathActivity
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import org.thoughtcrime.securesms.mms.GlideApp
@@ -40,6 +47,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
+import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@@ -48,9 +56,14 @@ import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import java.io.File
import java.security.SecureRandom
-import java.util.Date
+import javax.inject.Inject
+@AndroidEntryPoint
class SettingsActivity : PassphraseRequiredActionBarActivity() {
+
+ @Inject
+ lateinit var configFactory: ConfigFactory
+
private lateinit var binding: ActivitySettingsBinding
private var displayNameEditActionMode: ActionMode? = null
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
@@ -75,8 +88,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val displayName = getDisplayName()
glide = GlideApp.with(this)
with(binding) {
- setupProfilePictureView(profilePictureView.root)
- profilePictureView.root.setOnClickListener { showEditProfilePictureUI() }
+ setupProfilePictureView(profilePictureView)
+ profilePictureView.setOnClickListener { showEditProfilePictureUI() }
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
btnGroupNameDisplay.text = displayName
publicKeyTextView.text = hexEncodedPublicKey
@@ -101,7 +114,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
private fun setupProfilePictureView(view: ProfilePictureView) {
- view.glide = glide
view.apply {
publicKey = hexEncodedPublicKey
displayName = getDisplayName()
@@ -204,35 +216,44 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val promises = mutableListOf>()
if (displayName != null) {
TextSecurePreferences.setProfileName(this, displayName)
+ configFactory.user?.setName(displayName)
}
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
if (isUpdatingProfilePicture) {
if (profilePicture != null) {
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
} else {
- TextSecurePreferences.setLastProfilePictureUpload(this, System.currentTimeMillis())
- TextSecurePreferences.setProfilePictureURL(this, null)
+ MessagingModuleConfiguration.shared.storage.clearUserPic()
}
}
val compoundPromise = all(promises)
compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below
+ val userConfig = configFactory.user
if (isUpdatingProfilePicture) {
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
- TextSecurePreferences.setLastProfilePictureUpload(this, Date().time)
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
+ // new config
+ val url = TextSecurePreferences.getProfilePictureURL(this)
+ val profileKey = ProfileKeyUtil.getProfileKey(this)
+ if (profilePicture == null) {
+ userConfig?.setPic(UserPic.DEFAULT)
+ } else if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
+ userConfig?.setPic(UserPic(url, profileKey))
+ }
}
- if (profilePicture != null || displayName != null) {
- ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
+ if (userConfig != null && userConfig.needsDump()) {
+ configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
}
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
}
compoundPromise.alwaysUi {
if (displayName != null) {
binding.btnGroupNameDisplay.text = displayName
}
if (isUpdatingProfilePicture) {
- binding.profilePictureView.root.recycle() // Clear the cached image before updating
- binding.profilePictureView.root.update()
+ binding.profilePictureView.recycle() // Clear the cached image before updating
+ binding.profilePictureView.update()
}
binding.loader.isVisible = false
}
@@ -264,19 +285,15 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
private fun showEditProfilePictureUI() {
- AlertDialog.Builder(this)
- .setTitle(R.string.activity_settings_set_display_picture)
- .setView(R.layout.dialog_change_avatar)
- .setPositiveButton(R.string.activity_settings_upload) { _, _ ->
- startAvatarSelection()
+ showSessionDialog {
+ title(R.string.activity_settings_set_display_picture)
+ view(R.layout.dialog_change_avatar)
+ button(R.string.activity_settings_upload) { startAvatarSelection() }
+ if (TextSecurePreferences.getProfileAvatarId(context) != 0) {
+ button(R.string.activity_settings_remove) { removeAvatar() }
}
- .setNegativeButton(R.string.cancel) { _, _ -> }
- .apply {
- if (TextSecurePreferences.getProfileAvatarId(context) != 0) {
- setNeutralButton(R.string.activity_settings_remove) { _, _ -> removeAvatar() }
- }
- }
- .show().apply {
+ cancelButton()
+ }.apply {
val profilePic = findViewById(R.id.profile_picture_view)
?.also(::setupProfilePictureView)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt
index 1bd8373247..e3be429e3a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt
@@ -1,17 +1,18 @@
package org.thoughtcrime.securesms.preferences
+import android.app.Dialog
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
+import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
-import android.view.LayoutInflater
import android.webkit.MimeTypeMap
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
@@ -20,11 +21,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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.createSessionDialog
import org.thoughtcrime.securesms.util.FileProviderUtil
import org.thoughtcrime.securesms.util.StreamUtil
import java.io.File
@@ -33,21 +33,15 @@ import java.io.IOException
import java.util.Objects
import java.util.concurrent.TimeUnit
-class ShareLogsDialog : BaseDialog() {
+class ShareLogsDialog : DialogFragment() {
private var shareJob: Job? = null
- override fun setContentView(builder: AlertDialog.Builder) {
- val binding = DialogShareLogsBinding.inflate(LayoutInflater.from(requireContext()))
- binding.cancelButton.setOnClickListener {
- dismiss()
- }
- binding.shareButton.setOnClickListener {
- // start the export and share
- shareLogs()
- }
- builder.setView(binding.root)
- builder.setCancelable(false)
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
+ title(R.string.dialog_share_logs_title)
+ text(R.string.dialog_share_logs_explanation)
+ button(R.string.share) { shareLogs() }
+ cancelButton { dismiss() }
}
private fun shareLogs() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreference.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreference.java
deleted file mode 100644
index 1cccf1d524..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreference.java
+++ /dev/null
@@ -1,251 +0,0 @@
-package org.thoughtcrime.securesms.preferences.widgets;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import androidx.core.content.ContextCompat;
-import androidx.core.content.res.TypedArrayUtils;
-import androidx.preference.DialogPreference;
-import androidx.preference.PreferenceViewHolder;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.widget.ImageView;
-
-import com.takisoft.colorpicker.ColorPickerDialog;
-import com.takisoft.colorpicker.ColorPickerDialog.Size;
-import com.takisoft.colorpicker.ColorStateDrawable;
-
-import network.loki.messenger.R;
-
-public class ColorPickerPreference extends DialogPreference {
-
- private static final String TAG = ColorPickerPreference.class.getSimpleName();
-
- private int[] colors;
- private CharSequence[] colorDescriptions;
- private int color;
- private int columns;
- private int size;
- private boolean sortColors;
-
- private ImageView colorWidget;
- private OnPreferenceChangeListener listener;
-
- public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerPreference, defStyleAttr, 0);
-
- int colorsId = a.getResourceId(R.styleable.ColorPickerPreference_colors, R.array.color_picker_default_colors);
-
- if (colorsId != 0) {
- colors = context.getResources().getIntArray(colorsId);
- }
-
- colorDescriptions = a.getTextArray(R.styleable.ColorPickerPreference_colorDescriptions);
- color = a.getColor(R.styleable.ColorPickerPreference_currentColor, 0);
- columns = a.getInt(R.styleable.ColorPickerPreference_columns, 3);
- size = a.getInt(R.styleable.ColorPickerPreference_colorSize, 2);
- sortColors = a.getBoolean(R.styleable.ColorPickerPreference_sortColors, false);
-
- a.recycle();
-
- setWidgetLayoutResource(R.layout.preference_widget_color_swatch);
- }
-
- public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- @SuppressLint("RestrictedApi")
- public ColorPickerPreference(Context context, AttributeSet attrs) {
- this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.dialogPreferenceStyle,
- android.R.attr.dialogPreferenceStyle));
- }
-
- public ColorPickerPreference(Context context) {
- this(context, null);
- }
-
- @Override
- public void setOnPreferenceChangeListener(OnPreferenceChangeListener listener) {
- super.setOnPreferenceChangeListener(listener);
- this.listener = listener;
- }
-
- @Override
- public void onBindViewHolder(PreferenceViewHolder holder) {
- super.onBindViewHolder(holder);
-
- colorWidget = (ImageView) holder.findViewById(R.id.color_picker_widget);
- setColorOnWidget(color);
- }
-
- private void setColorOnWidget(int color) {
- if (colorWidget == null) {
- return;
- }
-
- Drawable[] colorDrawable = new Drawable[]
- {ContextCompat.getDrawable(getContext(), R.drawable.colorpickerpreference_pref_swatch)};
- colorWidget.setImageDrawable(new ColorStateDrawable(colorDrawable, color));
- }
-
- /**
- * Returns the current color.
- *
- * @return The current color.
- */
- public int getColor() {
- return color;
- }
-
- /**
- * Sets the current color.
- *
- * @param color The current color.
- */
- public void setColor(int color) {
- setInternalColor(color, false);
- }
-
- /**
- * Returns all of the available colors.
- *
- * @return The available colors.
- */
- public int[] getColors() {
- return colors;
- }
-
- /**
- * Sets the available colors.
- *
- * @param colors The available colors.
- */
- public void setColors(int[] colors) {
- this.colors = colors;
- }
-
- /**
- * Returns whether the available colors should be sorted automatically based on their HSV
- * values.
- *
- * @return Whether the available colors should be sorted automatically based on their HSV
- * values.
- */
- public boolean isSortColors() {
- return sortColors;
- }
-
- /**
- * Sets whether the available colors should be sorted automatically based on their HSV
- * values. The sorting does not modify the order of the original colors supplied via
- * {@link #setColors(int[])} or the XML attribute {@code app:colors}.
- *
- * @param sortColors Whether the available colors should be sorted automatically based on their
- * HSV values.
- */
- public void setSortColors(boolean sortColors) {
- this.sortColors = sortColors;
- }
-
- /**
- * Returns the available colors' descriptions that can be used by accessibility services.
- *
- * @return The available colors' descriptions.
- */
- public CharSequence[] getColorDescriptions() {
- return colorDescriptions;
- }
-
- /**
- * Sets the available colors' descriptions that can be used by accessibility services.
- *
- * @param colorDescriptions The available colors' descriptions.
- */
- public void setColorDescriptions(CharSequence[] colorDescriptions) {
- this.colorDescriptions = colorDescriptions;
- }
-
- /**
- * Returns the number of columns to be used in the picker dialog for displaying the available
- * colors. If the value is less than or equals to 0, the number of columns will be determined
- * automatically by the system using FlexboxLayoutManager.
- *
- * @return The number of columns to be used in the picker dialog.
- * @see com.google.android.flexbox.FlexboxLayoutManager
- */
- public int getColumns() {
- return columns;
- }
-
- /**
- * Sets the number of columns to be used in the picker dialog for displaying the available
- * colors. If the value is less than or equals to 0, the number of columns will be determined
- * automatically by the system using FlexboxLayoutManager.
- *
- * @param columns The number of columns to be used in the picker dialog. Use 0 to set it to
- * 'auto' mode.
- * @see com.google.android.flexbox.FlexboxLayoutManager
- */
- public void setColumns(int columns) {
- this.columns = columns;
- }
-
- /**
- * Returns the size of the color swatches in the dialog. It can be either
- * {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}.
- *
- * @return The size of the color swatches in the dialog.
- * @see ColorPickerDialog#SIZE_SMALL
- * @see ColorPickerDialog#SIZE_LARGE
- */
- @Size
- public int getSize() {
- return size;
- }
-
- /**
- * Sets the size of the color swatches in the dialog. It can be either
- * {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}.
- *
- * @param size The size of the color swatches in the dialog. It can be either
- * {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}.
- * @see ColorPickerDialog#SIZE_SMALL
- * @see ColorPickerDialog#SIZE_LARGE
- */
- public void setSize(@Size int size) {
- this.size = size;
- }
-
- private void setInternalColor(int color, boolean force) {
- int oldColor = getPersistedInt(0);
-
- boolean changed = oldColor != color;
-
- if (changed || force) {
- this.color = color;
-
- persistInt(color);
-
- setColorOnWidget(color);
-
- if (listener != null) listener.onPreferenceChange(this, color);
- notifyChanged();
- }
- }
-
- @Override
- protected Object onGetDefaultValue(TypedArray a, int index) {
- return a.getString(index);
- }
-
- @Override
- protected void onSetInitialValue(boolean restoreValue, Object defaultValueObj) {
- final String defaultValue = (String) defaultValueObj;
- setInternalColor(restoreValue ? getPersistedInt(0) : (!TextUtils.isEmpty(defaultValue) ? Color.parseColor(defaultValue) : 0), true);
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreferenceDialogFragmentCompat.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreferenceDialogFragmentCompat.java
deleted file mode 100644
index 964f439ba1..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ColorPickerPreferenceDialogFragmentCompat.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package org.thoughtcrime.securesms.preferences.widgets;
-
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.preference.PreferenceDialogFragmentCompat;
-
-import com.takisoft.colorpicker.ColorPickerDialog;
-import com.takisoft.colorpicker.OnColorSelectedListener;
-
-public class ColorPickerPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat implements OnColorSelectedListener {
-
- private int pickedColor;
-
- public static ColorPickerPreferenceDialogFragmentCompat newInstance(String key) {
- ColorPickerPreferenceDialogFragmentCompat fragment = new ColorPickerPreferenceDialogFragmentCompat();
- Bundle b = new Bundle(1);
- b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key);
- fragment.setArguments(b);
- return fragment;
- }
-
-
- @NonNull
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- ColorPickerPreference pref = getColorPickerPreference();
-
- ColorPickerDialog.Params params = new ColorPickerDialog.Params.Builder(getContext())
- .setSelectedColor(pref.getColor())
- .setColors(pref.getColors())
- .setColorContentDescriptions(pref.getColorDescriptions())
- .setSize(pref.getSize())
- .setSortColors(pref.isSortColors())
- .setColumns(pref.getColumns())
- .build();
-
- ColorPickerDialog dialog = new ColorPickerDialog(getActivity(), this, params);
- dialog.setTitle(pref.getDialogTitle());
-
- return dialog;
- }
-
- @Override
- public void onDialogClosed(boolean positiveResult) {
- ColorPickerPreference preference = getColorPickerPreference();
-
- if (positiveResult) {
- preference.setColor(pickedColor);
- }
- }
-
- @Override
- public void onColorSelected(int color) {
- this.pickedColor = color;
-
- super.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
- }
-
- ColorPickerPreference getColorPickerPreference() {
- return (ColorPickerPreference) getPreference();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java
index f1cbea16c3..1c05e68bdf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java
@@ -144,7 +144,6 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter
- suspend fun clearAllMessageRequests(): ResultOf
+ suspend fun clearAllMessageRequests(block: Boolean): ResultOf
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf
@@ -82,8 +84,10 @@ class DefaultConversationRepository @Inject constructor(
private val mmsDb: MmsDatabase,
private val mmsSmsDb: MmsSmsDatabase,
private val recipientDb: RecipientDatabase,
+ private val storage: Storage,
private val lokiMessageDb: LokiMessageDatabase,
- private val sessionJobDb: SessionJobDatabase
+ private val sessionJobDb: SessionJobDatabase,
+ private val configFactory: ConfigFactory
) : ConversationRepository {
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
@@ -125,8 +129,9 @@ class DefaultConversationRepository @Inject constructor(
}
}
+ // This assumes that recipient.isContactRecipient is true
override fun setBlocked(recipient: Recipient, blocked: Boolean) {
- recipientDb.setBlocked(recipient, blocked)
+ storage.setBlocked(listOf(recipient), blocked)
}
override fun deleteLocally(recipient: Recipient, message: MessageRecord) {
@@ -139,7 +144,7 @@ class DefaultConversationRepository @Inject constructor(
}
override fun setApproved(recipient: Recipient, isApproved: Boolean) {
- recipientDb.setApproved(recipient, isApproved)
+ storage.setRecipientApproved(recipient, isApproved)
}
override suspend fun deleteForEveryone(
@@ -250,29 +255,33 @@ class DefaultConversationRepository @Inject constructor(
override suspend fun deleteThread(threadId: Long): ResultOf {
sessionJobDb.cancelPendingMessageSendJobs(threadId)
- threadDb.deleteConversation(threadId)
+ storage.deleteConversation(threadId)
return ResultOf.Success(Unit)
}
override suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf {
sessionJobDb.cancelPendingMessageSendJobs(thread.threadId)
- threadDb.deleteConversation(thread.threadId)
+ storage.deleteConversation(thread.threadId)
return ResultOf.Success(Unit)
}
- override suspend fun clearAllMessageRequests(): ResultOf {
+ override suspend fun clearAllMessageRequests(block: Boolean): ResultOf {
threadDb.readerFor(threadDb.unapprovedConversationList).use { reader ->
while (reader.next != null) {
deleteMessageRequest(reader.current)
+ val recipient = reader.current.recipient
+ if (block) {
+ setBlocked(recipient, true)
+ }
}
}
return ResultOf.Success(Unit)
}
override suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf = suspendCoroutine { continuation ->
- recipientDb.setApproved(recipient, true)
+ storage.setRecipientApproved(recipient, true)
val message = MessageRequestResponse(true)
- MessageSender.send(message, Destination.from(recipient.address))
+ MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = recipient.isLocalNumber)
.success {
threadDb.setHasSent(threadId, true)
continuation.resume(ResultOf.Success(Unit))
@@ -283,7 +292,7 @@ class DefaultConversationRepository @Inject constructor(
override fun declineMessageRequest(threadId: Long) {
sessionJobDb.cancelPendingMessageSendJobs(threadId)
- threadDb.deleteConversation(threadId)
+ storage.deleteConversation(threadId)
}
override fun hasReceived(threadId: Long): Boolean {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java
index f42b55b5fe..85d8c8f436 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java
@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.service;
import android.content.Context;
import org.jetbrains.annotations.NotNull;
+import org.session.libsession.database.StorageProtocol;
+import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
@@ -15,6 +17,7 @@ import org.session.libsignal.messages.SignalServiceGroup;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.database.MmsDatabase;
+import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
@@ -35,12 +38,14 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
private final SmsDatabase smsDatabase;
private final MmsDatabase mmsDatabase;
+ private final MmsSmsDatabase mmsSmsDatabase;
private final Context context;
public ExpiringMessageManager(Context context) {
this.context = context.getApplicationContext();
this.smsDatabase = DatabaseComponent.get(context).smsDatabase();
this.mmsDatabase = DatabaseComponent.get(context).mmsDatabase();
+ this.mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
executor.execute(new LoadTask());
executor.execute(new ProcessTask());
@@ -79,12 +84,11 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
}
if (message.getId() != null) {
- DatabaseComponent.get(context).smsDatabase().deleteMessage(message.getId());
+ smsDatabase.deleteMessage(message.getId());
}
}
private void insertIncomingExpirationTimerMessage(ExpirationTimerUpdate message) {
- MmsDatabase database = DatabaseComponent.get(context).mmsDatabase();
String senderPublicKey = message.getSender();
Long sentTimestamp = message.getSentTimestamp();
@@ -106,6 +110,10 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
Address groupAddress = Address.fromSerialized(groupID);
recipient = Recipient.from(context, groupAddress, false);
}
+ Long threadId = MessagingModuleConfiguration.getShared().getStorage().getThreadId(recipient);
+ if (threadId == null) {
+ return;
+ }
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1,
duration * 1000L, true,
@@ -120,10 +128,10 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
Optional.absent(),
Optional.absent());
//insert the timer update message
- database.insertSecureDecryptedMessageInbox(mediaMessage, -1, true, true);
+ mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, threadId, true);
//set the timer to the conversation
- DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
+ MessagingModuleConfiguration.getShared().getStorage().setExpirationTimer(recipient.getAddress().serialize(), duration);
} catch (IOException | MmsException ioe) {
Log.e("Loki", "Failed to insert expiration update message.");
@@ -131,28 +139,30 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
}
private void insertOutgoingExpirationTimerMessage(ExpirationTimerUpdate message) {
- MmsDatabase database = DatabaseComponent.get(context).mmsDatabase();
Long sentTimestamp = message.getSentTimestamp();
String groupId = message.getGroupPublicKey();
int duration = message.getDuration();
- Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : message.getRecipient());
- Recipient recipient = Recipient.from(context, address, false);
+ Address address;
try {
- OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId);
- database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp, true);
-
if (groupId != null) {
- // we need the group ID as recipient for setExpireMessages below
- recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId)), false);
+ address = Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId));
+ } else {
+ address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : message.getRecipient());
}
- //set the timer to the conversation
- DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
+ Recipient recipient = Recipient.from(context, address, false);
+ StorageProtocol storage = MessagingModuleConfiguration.getShared().getStorage();
+ message.setThreadID(storage.getOrCreateThreadIdFor(address));
+
+ OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId);
+ mmsDatabase.insertSecureDecryptedMessageOutbox(timerUpdateMessage, message.getThreadID(), sentTimestamp, true);
+ //set the timer to the conversation
+ MessagingModuleConfiguration.getShared().getStorage().setExpirationTimer(recipient.getAddress().serialize(), duration);
} catch (MmsException | IOException ioe) {
- Log.e("Loki", "Failed to insert expiration update message.");
+ Log.e("Loki", "Failed to insert expiration update message.", ioe);
}
}
@@ -163,7 +173,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
@Override
public void startAnyExpiration(long timestamp, @NotNull String author) {
- MessageRecord messageRecord = DatabaseComponent.get(context).mmsSmsDatabase().getMessageFor(timestamp, author);
+ MessageRecord messageRecord = mmsSmsDatabase.getMessageFor(timestamp, author);
if (messageRecord != null) {
boolean mms = messageRecord.isMms();
Recipient recipient = messageRecord.getRecipient();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt
index f9f5524efa..8b1975865d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt
@@ -1,16 +1,23 @@
package org.thoughtcrime.securesms.sskenvironment
import android.content.Context
+import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.JobQueue
-import org.session.libsession.utilities.SSKEnvironment
-import org.session.libsession.utilities.recipients.Recipient
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
+import org.session.libsession.messaging.utilities.SessionId
+import org.session.libsession.utilities.SSKEnvironment
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsession.utilities.recipients.Recipient
+import org.session.libsignal.utilities.IdPrefix
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
+import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
-class ProfileManager : SSKEnvironment.ProfileManagerProtocol {
+class ProfileManager(private val context: Context, private val configFactory: ConfigFactory) : SSKEnvironment.ProfileManagerProtocol {
override fun setNickname(context: Context, recipient: Recipient, nickname: String?) {
+ if (recipient.isLocalNumber) return
val sessionID = recipient.address.serialize()
val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()
var contact = contactDatabase.getContactWithSessionID(sessionID)
@@ -20,10 +27,12 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol {
contact.nickname = nickname
contactDatabase.setContact(contact)
}
+ contactUpdatedInternal(contact)
}
- override fun setName(context: Context, recipient: Recipient, name: String) {
+ override fun setName(context: Context, recipient: Recipient, name: String?) {
// New API
+ if (recipient.isLocalNumber) return
val sessionID = recipient.address.serialize()
val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()
var contact = contactDatabase.getContactWithSessionID(sessionID)
@@ -37,40 +46,69 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol {
val database = DatabaseComponent.get(context).recipientDatabase()
database.setProfileName(recipient, name)
recipient.notifyListeners()
+ contactUpdatedInternal(contact)
}
- override fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String) {
- val job = RetrieveProfileAvatarJob(profilePictureURL, recipient.address)
- JobQueue.shared.add(job)
+ override fun setProfilePicture(
+ context: Context,
+ recipient: Recipient,
+ profilePictureURL: String?,
+ profileKey: ByteArray?
+ ) {
+ val hasPendingDownload = DatabaseComponent
+ .get(context)
+ .sessionJobDatabase()
+ .getAllJobs(RetrieveProfileAvatarJob.KEY).any {
+ (it.value as? RetrieveProfileAvatarJob)?.recipientAddress == recipient.address
+ }
+ val resolved = recipient.resolve()
+ DatabaseComponent.get(context).storage().setProfilePicture(
+ recipient = resolved,
+ newProfileKey = profileKey,
+ newProfilePicture = profilePictureURL
+ )
val sessionID = recipient.address.serialize()
val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()
var contact = contactDatabase.getContactWithSessionID(sessionID)
if (contact == null) contact = Contact(sessionID)
contact.threadID = DatabaseComponent.get(context).storage().getThreadId(recipient.address)
- if (contact.profilePictureURL != profilePictureURL) {
+ if (!contact.profilePictureEncryptionKey.contentEquals(profileKey) || contact.profilePictureURL != profilePictureURL) {
+ contact.profilePictureEncryptionKey = profileKey
contact.profilePictureURL = profilePictureURL
contactDatabase.setContact(contact)
}
- }
-
- override fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray) {
- // New API
- val sessionID = recipient.address.serialize()
- val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()
- var contact = contactDatabase.getContactWithSessionID(sessionID)
- if (contact == null) contact = Contact(sessionID)
- contact.threadID = DatabaseComponent.get(context).storage().getThreadId(recipient.address)
- if (!contact.profilePictureEncryptionKey.contentEquals(profileKey)) {
- contact.profilePictureEncryptionKey = profileKey
- contactDatabase.setContact(contact)
+ contactUpdatedInternal(contact)
+ if (!hasPendingDownload) {
+ val job = RetrieveProfileAvatarJob(profilePictureURL, recipient.address)
+ JobQueue.shared.add(job)
}
- // Old API
- val database = DatabaseComponent.get(context).recipientDatabase()
- database.setProfileKey(recipient, profileKey)
}
override fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode) {
val database = DatabaseComponent.get(context).recipientDatabase()
database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode)
}
+
+ override fun contactUpdatedInternal(contact: Contact): String? {
+ val contactConfig = configFactory.contacts ?: return null
+ if (contact.sessionID == TextSecurePreferences.getLocalNumber(context)) return null
+ val sessionId = SessionId(contact.sessionID)
+ if (sessionId.prefix != IdPrefix.STANDARD) return null // only internally store standard session IDs
+ contactConfig.upsertContact(contact.sessionID) {
+ this.name = contact.name.orEmpty()
+ this.nickname = contact.nickname.orEmpty()
+ val url = contact.profilePictureURL
+ val key = contact.profilePictureEncryptionKey
+ if (!url.isNullOrEmpty() && key != null && key.size == 32) {
+ this.profilePicture = UserPic(url, key)
+ } else if (url.isNullOrEmpty() && key == null) {
+ this.profilePicture = UserPic.DEFAULT
+ }
+ }
+ if (contactConfig.needsPush()) {
+ ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
+ }
+ return contactConfig.get(contact.sessionID)?.hashCode()?.toString()
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt
new file mode 100644
index 0000000000..55bc1be62e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt
@@ -0,0 +1,63 @@
+package org.thoughtcrime.securesms.ui
+
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.Colors
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+val colorDestructive = Color(0xffFF453A)
+
+const val classicDark0 = 0xff111111
+const val classicDark1 = 0xff1B1B1B
+const val classicDark2 = 0xff2D2D2D
+const val classicDark3 = 0xff414141
+const val classicDark4 = 0xff767676
+const val classicDark5 = 0xffA1A2A1
+const val classicDark6 = 0xffFFFFFF
+
+const val classicLight0 = 0xff000000
+const val classicLight1 = 0xff6D6D6D
+const val classicLight2 = 0xffA1A2A1
+const val classicLight3 = 0xffDFDFDF
+const val classicLight4 = 0xffF0F0F0
+const val classicLight5 = 0xffF9F9F9
+const val classicLight6 = 0xffFFFFFF
+
+const val oceanDark0 = 0xff000000
+const val oceanDark1 = 0xff1A1C28
+const val oceanDark2 = 0xff252735
+const val oceanDark3 = 0xff2B2D40
+const val oceanDark4 = 0xff3D4A5D
+const val oceanDark5 = 0xffA6A9CE
+const val oceanDark6 = 0xff5CAACC
+const val oceanDark7 = 0xffFFFFFF
+
+const val oceanLight0 = 0xff000000
+const val oceanLight1 = 0xff19345D
+const val oceanLight2 = 0xff6A6E90
+const val oceanLight3 = 0xff5CAACC
+const val oceanLight4 = 0xffB3EDF2
+const val oceanLight5 = 0xffE7F3F4
+const val oceanLight6 = 0xffECFAFB
+const val oceanLight7 = 0xffFCFFFF
+
+val ocean_accent = Color(0xff57C9FA)
+
+val oceanLights = arrayOf(oceanLight0, oceanLight1, oceanLight2, oceanLight3, oceanLight4, oceanLight5, oceanLight6, oceanLight7)
+val oceanDarks = arrayOf(oceanDark0, oceanDark1, oceanDark2, oceanDark3, oceanDark4, oceanDark5, oceanDark6, oceanDark7)
+val classicLights = arrayOf(classicLight0, classicLight1, classicLight2, classicLight3, classicLight4, classicLight5, classicLight6)
+val classicDarks = arrayOf(classicDark0, classicDark1, classicDark2, classicDark3, classicDark4, classicDark5, classicDark6)
+
+val oceanLightColors = oceanLights.map(::Color)
+val oceanDarkColors = oceanDarks.map(::Color)
+val classicLightColors = classicLights.map(::Color)
+val classicDarkColors = classicDarks.map(::Color)
+
+val blackAlpha40 = Color.Black.copy(alpha = 0.4f)
+
+@Composable
+fun transparentButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)
+
+@Composable
+fun destructiveButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = colorDestructive)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
new file mode 100644
index 0000000000..1724bde8a6
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
@@ -0,0 +1,182 @@
+package org.thoughtcrime.securesms.ui
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ButtonColors
+import androidx.compose.material.Card
+import androidx.compose.material.Colors
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.google.accompanist.pager.HorizontalPagerIndicator
+import kotlinx.coroutines.launch
+import network.loki.messenger.R
+import org.session.libsession.utilities.recipients.Recipient
+import org.thoughtcrime.securesms.components.ProfilePictureView
+
+@Composable
+fun ItemButton(
+ text: String,
+ @DrawableRes icon: Int,
+ colors: ButtonColors = transparentButtonColors(),
+ contentDescription: String = text,
+ onClick: () -> Unit
+) {
+ TextButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(60.dp),
+ colors = colors,
+ onClick = onClick,
+ shape = RectangleShape,
+ ) {
+ Box(modifier = Modifier
+ .width(80.dp)
+ .fillMaxHeight()) {
+ Icon(
+ painter = painterResource(id = icon),
+ contentDescription = contentDescription,
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ Text(text, modifier = Modifier.fillMaxWidth())
+ }
+}
+
+@Composable
+fun Cell(content: @Composable () -> Unit) {
+ CellWithPaddingAndMargin(padding = 0.dp) { content() }
+}
+@Composable
+fun CellNoMargin(content: @Composable () -> Unit) {
+ CellWithPaddingAndMargin(padding = 0.dp, margin = 0.dp) { content() }
+}
+
+@Composable
+fun CellWithPaddingAndMargin(
+ padding: Dp = 24.dp,
+ margin: Dp = 32.dp,
+ content: @Composable () -> Unit
+) {
+ Card(
+ backgroundColor = MaterialTheme.colors.cellColor,
+ shape = RoundedCornerShape(16.dp),
+ elevation = 0.dp,
+ modifier = Modifier
+ .wrapContentHeight()
+ .fillMaxWidth()
+ .padding(horizontal = margin),
+ ) {
+ Box(Modifier.padding(padding)) { content() }
+ }
+}
+
+private val Colors.cellColor: Color
+ @Composable
+ get() = LocalExtraColors.current.settingsBackground
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) {
+ if (pagerState.pageCount >= 2) Card(
+ shape = RoundedCornerShape(50.dp),
+ backgroundColor = Color.Black.copy(alpha = 0.4f),
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(8.dp)
+ ) {
+ Box(modifier = Modifier.padding(8.dp)) {
+ HorizontalPagerIndicator(
+ pagerState = pagerState,
+ pageCount = pagerState.pageCount,
+ activeColor = Color.White,
+ inactiveColor = classicDarkColors[5])
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun RowScope.CarouselPrevButton(pagerState: PagerState) {
+ CarouselButton(pagerState, pagerState.canScrollBackward, R.drawable.ic_prev, -1)
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun RowScope.CarouselNextButton(pagerState: PagerState) {
+ CarouselButton(pagerState, pagerState.canScrollForward, R.drawable.ic_next, 1)
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun RowScope.CarouselButton(
+ pagerState: PagerState,
+ enabled: Boolean,
+ @DrawableRes id: Int,
+ delta: Int
+) {
+ if (pagerState.pageCount <= 1) Spacer(modifier = Modifier.width(32.dp))
+ else {
+ val animationScope = rememberCoroutineScope()
+ IconButton(
+ modifier = Modifier
+ .width(40.dp)
+ .align(Alignment.CenterVertically),
+ enabled = enabled,
+ onClick = { animationScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + delta) } }) {
+ Icon(
+ painter = painterResource(id = id),
+ contentDescription = "",
+ )
+ }
+ }
+}
+
+@Composable
+fun Divider() {
+ androidx.compose.material.Divider(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+}
+
+@Composable
+fun RowScope.Avatar(recipient: Recipient) {
+ Box(
+ modifier = Modifier
+ .width(60.dp)
+ .align(Alignment.CenterVertically)
+ ) {
+ AndroidView(
+ factory = {
+ ProfilePictureView(it).apply { update(recipient) }
+ },
+ modifier = Modifier
+ .width(46.dp)
+ .height(46.dp)
+ )
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt
new file mode 100644
index 0000000000..44ff4a42d8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt
@@ -0,0 +1,34 @@
+package org.thoughtcrime.securesms.ui
+
+import androidx.annotation.StringRes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+
+/**
+ * Compatibility class to allow ViewModels to use strings and string resources interchangeably.
+ */
+sealed class GetString {
+ @Composable
+ abstract fun string(): String
+ data class FromString(val string: String): GetString() {
+ @Composable
+ override fun string(): String = string
+ }
+ data class FromResId(@StringRes val resId: Int): GetString() {
+ @Composable
+ override fun string(): String = stringResource(resId)
+
+ }
+}
+
+fun GetString(@StringRes resId: Int) = GetString.FromResId(resId)
+fun GetString(string: String) = GetString.FromString(string)
+
+
+/**
+ * Represents some text with an associated title.
+ */
+data class TitledText(val title: GetString, val text: String) {
+ constructor(title: String, text: String): this(GetString(title), text)
+ constructor(@StringRes title: Int, text: String): this(GetString(title), text)
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt
new file mode 100644
index 0000000000..64bbd21d8d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt
@@ -0,0 +1,76 @@
+package org.thoughtcrime.securesms.ui
+
+import android.content.Context
+import androidx.annotation.AttrRes
+import androidx.appcompat.view.ContextThemeWrapper
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
+import com.google.android.material.color.MaterialColors
+import network.loki.messenger.R
+
+val LocalExtraColors = staticCompositionLocalOf { error("No Custom Attribute value provided") }
+
+
+data class ExtraColors(
+ val settingsBackground: Color,
+)
+
+/**
+ * Converts current Theme to Compose Theme.
+ */
+@Composable
+fun AppTheme(
+ content: @Composable () -> Unit
+) {
+ val extraColors = LocalContext.current.run {
+ ExtraColors(
+ settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground),
+ )
+ }
+
+ CompositionLocalProvider(LocalExtraColors provides extraColors) {
+ AppCompatTheme {
+ content()
+ }
+ }
+}
+
+fun Context.getColorFromTheme(@AttrRes attr: Int, defaultValue: Int = 0x0): Color =
+ MaterialColors.getColor(this, attr, defaultValue).let(::Color)
+
+/**
+ * Set the theme and a background for Compose Previews.
+ */
+@Composable
+fun PreviewTheme(
+ themeResId: Int,
+ content: @Composable () -> Unit
+) {
+ CompositionLocalProvider(
+ LocalContext provides ContextThemeWrapper(LocalContext.current, themeResId)
+ ) {
+ AppTheme {
+ Box(modifier = Modifier.background(color = MaterialTheme.colors.background)) {
+ content()
+ }
+ }
+ }
+}
+
+class ThemeResPreviewParameterProvider : PreviewParameterProvider {
+ override val values = sequenceOf(
+ R.style.Classic_Dark,
+ R.style.Classic_Light,
+ R.style.Ocean_Dark,
+ R.style.Ocean_Light,
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt
index d5b361ecd6..5ff823a15c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt
@@ -7,10 +7,10 @@ import android.view.View
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
+import androidx.fragment.app.DialogFragment
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.BaseActionBarActivity
-import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
fun BaseActionBarActivity.setUpActionBarSessionLogo(hideBackButton: Boolean = false) {
val actionbar = supportActionBar!!
@@ -66,7 +66,7 @@ interface ActivityDispatcher {
fun get(context: Context) = context.getSystemService(SERVICE) as? ActivityDispatcher
}
fun dispatchIntent(body: (Context)->Intent?)
- fun showDialog(baseDialog: BaseDialog, tag: String? = null)
+ fun showDialog(dialogFragment: DialogFragment, tag: String? = null)
}
fun TextSecurePreferences.themeState(): ThemeState {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt
index 56c0a55dda..0ba63fc549 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt
@@ -1,12 +1,10 @@
package org.thoughtcrime.securesms.util
import android.app.Notification
-import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
-import android.os.Build
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
@@ -32,15 +30,7 @@ class CallNotificationBuilder {
@JvmStatic
fun areNotificationsEnabled(context: Context): Boolean {
val notificationManager = NotificationManagerCompat.from(context)
- return when {
- !notificationManager.areNotificationsEnabled() -> false
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
- notificationManager.notificationChannels.firstOrNull { channel ->
- channel.importance == NotificationManager.IMPORTANCE_NONE
- } == null
- }
- else -> true
- }
+ return notificationManager.areNotificationsEnabled()
}
@JvmStatic
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt
index fd462417d9..297014d86c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt
@@ -1,18 +1,66 @@
package org.thoughtcrime.securesms.util
import android.content.Context
+import network.loki.messenger.libsession_util.ConfigBase
+import network.loki.messenger.libsession_util.Contacts
+import network.loki.messenger.libsession_util.ConversationVolatileConfig
+import network.loki.messenger.libsession_util.UserGroupsConfig
+import network.loki.messenger.libsession_util.UserProfile
+import network.loki.messenger.libsession_util.util.BaseCommunityInfo
+import network.loki.messenger.libsession_util.util.Contact
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import network.loki.messenger.libsession_util.util.GroupInfo
+import network.loki.messenger.libsession_util.util.UserPic
import nl.komponents.kovenant.Promise
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.messaging.jobs.ConfigurationSyncJob
+import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.sending_receiving.MessageSender
+import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
+import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsession.utilities.WindowDebouncer
+import org.session.libsignal.crypto.ecc.DjbECPublicKey
+import org.session.libsignal.utilities.Hex
+import org.session.libsignal.utilities.IdPrefix
+import org.session.libsignal.utilities.toHexString
+import org.thoughtcrime.securesms.database.GroupDatabase
+import org.thoughtcrime.securesms.database.ThreadDatabase
+import org.thoughtcrime.securesms.dependencies.DatabaseComponent
+import java.util.Timer
object ConfigurationMessageUtilities {
+ private val debouncer = WindowDebouncer(3000, Timer())
+
+ private fun scheduleConfigSync(userPublicKey: String) {
+ debouncer.publish {
+ // don't schedule job if we already have one
+ val storage = MessagingModuleConfiguration.shared.storage
+ val ourDestination = Destination.Contact(userPublicKey)
+ val currentStorageJob = storage.getConfigSyncJob(ourDestination)
+ if (currentStorageJob != null) {
+ (currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true)
+ return@publish
+ }
+ val newConfigSync = ConfigurationSyncJob(ourDestination)
+ JobQueue.shared.add(newConfigSync)
+ }
+ }
+
@JvmStatic
fun syncConfigurationIfNeeded(context: Context) {
+ // add if check here to schedule new config job process and return early
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
+ val forcedConfig = TextSecurePreferences.hasForcedNewConfig(context)
+ val currentTime = SnodeAPI.nowWithOffset
+ if (ConfigBase.isNewConfigEnabled(forcedConfig, currentTime)) {
+ scheduleConfigSync(userPublicKey)
+ return
+ }
val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context)
val now = System.currentTimeMillis()
if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return
@@ -35,7 +83,16 @@ object ConfigurationMessageUtilities {
}
fun forceSyncConfigurationNowIfNeeded(context: Context): Promise {
- val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit)
+ // add if check here to schedule new config job process and return early
+ val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofFail(NullPointerException("User Public Key is null"))
+ val forcedConfig = TextSecurePreferences.hasForcedNewConfig(context)
+ val currentTime = SnodeAPI.nowWithOffset
+ if (ConfigBase.isNewConfigEnabled(forcedConfig, currentTime)) {
+ // schedule job if none exist
+ // don't schedule job if we already have one
+ scheduleConfigSync(userPublicKey)
+ return Promise.ofSuccess(Unit)
+ }
val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
!recipient.isGroupRecipient && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty()
}.map { recipient ->
@@ -50,9 +107,179 @@ object ConfigurationMessageUtilities {
)
}
val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit)
- val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey)))
+ val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey)), isSyncMessage = true)
TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis())
return promise
}
+ private fun maybeUserSecretKey() = MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes
+
+ fun generateUserProfileConfigDump(): ByteArray? {
+ val storage = MessagingModuleConfiguration.shared.storage
+ val ownPublicKey = storage.getUserPublicKey() ?: return null
+ val config = ConfigurationMessage.getCurrent(listOf()) ?: return null
+ val secretKey = maybeUserSecretKey() ?: return null
+ val profile = UserProfile.newInstance(secretKey)
+ profile.setName(config.displayName)
+ val picUrl = config.profilePicture
+ val picKey = config.profileKey
+ if (!picUrl.isNullOrEmpty() && picKey.isNotEmpty()) {
+ profile.setPic(UserPic(picUrl, picKey))
+ }
+ val ownThreadId = storage.getThreadId(Address.fromSerialized(ownPublicKey))
+ profile.setNtsPriority(
+ if (ownThreadId != null)
+ if (storage.isPinned(ownThreadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
+ else ConfigBase.PRIORITY_HIDDEN
+ )
+ val dump = profile.dump()
+ profile.free()
+ return dump
+ }
+
+ fun generateContactConfigDump(): ByteArray? {
+ val secretKey = maybeUserSecretKey() ?: return null
+ val storage = MessagingModuleConfiguration.shared.storage
+ val localUserKey = storage.getUserPublicKey() ?: return null
+ val contactsWithSettings = storage.getAllContacts().filter { recipient ->
+ recipient.sessionID != localUserKey && recipient.sessionID.startsWith(IdPrefix.STANDARD.value)
+ && storage.getThreadId(recipient.sessionID) != null
+ }.map { contact ->
+ val address = Address.fromSerialized(contact.sessionID)
+ val thread = storage.getThreadId(address)
+ val isPinned = if (thread != null) {
+ storage.isPinned(thread)
+ } else false
+
+ Triple(contact, storage.getRecipientSettings(address)!!, isPinned)
+ }
+ val contactConfig = Contacts.newInstance(secretKey)
+ for ((contact, settings, isPinned) in contactsWithSettings) {
+ val url = contact.profilePictureURL
+ val key = contact.profilePictureEncryptionKey
+ val userPic = if (url.isNullOrEmpty() || key?.isNotEmpty() != true) {
+ null
+ } else {
+ UserPic(url, key)
+ }
+
+ val contactInfo = Contact(
+ id = contact.sessionID,
+ name = contact.name.orEmpty(),
+ nickname = contact.nickname.orEmpty(),
+ blocked = settings.isBlocked,
+ approved = settings.isApproved,
+ approvedMe = settings.hasApprovedMe(),
+ profilePicture = userPic ?: UserPic.DEFAULT,
+ priority = if (isPinned) 1 else 0,
+ expiryMode = if (settings.expireMessages == 0) ExpiryMode.NONE else ExpiryMode.AfterRead(settings.expireMessages.toLong())
+ )
+ contactConfig.set(contactInfo)
+ }
+ val dump = contactConfig.dump()
+ contactConfig.free()
+ if (dump.isEmpty()) return null
+ return dump
+ }
+
+ fun generateConversationVolatileDump(context: Context): ByteArray? {
+ val secretKey = maybeUserSecretKey() ?: return null
+ val storage = MessagingModuleConfiguration.shared.storage
+ val convoConfig = ConversationVolatileConfig.newInstance(secretKey)
+ val threadDb = DatabaseComponent.get(context).threadDatabase()
+ threadDb.approvedConversationList.use { cursor ->
+ val reader = threadDb.readerFor(cursor)
+ var current = reader.next
+ while (current != null) {
+ val recipient = current.recipient
+ val contact = when {
+ recipient.isOpenGroupRecipient -> {
+ val openGroup = storage.getOpenGroup(current.threadId) ?: continue
+ val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue
+ convoConfig.getOrConstructCommunity(base, room, pubKey)
+ }
+ recipient.isClosedGroupRecipient -> {
+ val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
+ convoConfig.getOrConstructLegacyGroup(groupPublicKey)
+ }
+ recipient.isContactRecipient -> {
+ if (recipient.isLocalNumber) null // this is handled by the user profile NTS data
+ else if (recipient.isOpenGroupInboxRecipient) null // specifically exclude
+ else if (!recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) null
+ else convoConfig.getOrConstructOneToOne(recipient.address.serialize())
+ }
+ else -> null
+ }
+ if (contact == null) {
+ current = reader.next
+ continue
+ }
+ contact.lastRead = current.lastSeen
+ contact.unread = false
+ convoConfig.set(contact)
+ current = reader.next
+ }
+ }
+
+ val dump = convoConfig.dump()
+ convoConfig.free()
+ if (dump.isEmpty()) return null
+ return dump
+ }
+
+ fun generateUserGroupDump(context: Context): ByteArray? {
+ val secretKey = maybeUserSecretKey() ?: return null
+ val storage = MessagingModuleConfiguration.shared.storage
+ val groupConfig = UserGroupsConfig.newInstance(secretKey)
+ val allOpenGroups = storage.getAllOpenGroups().values.mapNotNull { openGroup ->
+ val (baseUrl, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return@mapNotNull null
+ val pubKeyHex = Hex.toStringCondensed(pubKey)
+ val baseInfo = BaseCommunityInfo(baseUrl, room, pubKeyHex)
+ val threadId = storage.getThreadId(openGroup) ?: return@mapNotNull null
+ val isPinned = storage.isPinned(threadId)
+ GroupInfo.CommunityGroupInfo(baseInfo, if (isPinned) 1 else 0)
+ }
+
+ val allLgc = storage.getAllGroups(includeInactive = false).filter {
+ it.isClosedGroup && it.isActive && it.members.size > 1
+ }.mapNotNull { group ->
+ val groupAddress = Address.fromSerialized(group.encodedId)
+ val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupAddress.serialize()).toHexString()
+ val recipient = storage.getRecipientSettings(groupAddress) ?: return@mapNotNull null
+ val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return@mapNotNull null
+ val threadId = storage.getThreadId(group.encodedId)
+ val isPinned = threadId?.let { storage.isPinned(threadId) } ?: false
+ val admins = group.admins.map { it.serialize() to true }.toMap()
+ val members = group.members.filterNot { it.serialize() !in admins.keys }.map { it.serialize() to false }.toMap()
+ GroupInfo.LegacyGroupInfo(
+ sessionId = groupPublicKey,
+ name = group.title,
+ members = admins + members,
+ priority = if (isPinned) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
+ encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
+ encSecKey = encryptionKeyPair.privateKey.serialize(),
+ disappearingTimer = recipient.expireMessages.toLong(),
+ joinedAt = (group.formationTimestamp / 1000L)
+ )
+ }
+ (allOpenGroups + allLgc).forEach { groupInfo ->
+ groupConfig.set(groupInfo)
+ }
+ val dump = groupConfig.dump()
+ groupConfig.free()
+ if (dump.isEmpty()) return null
+ return dump
+ }
+
+ @JvmField
+ val DELETE_INACTIVE_GROUPS: String = """
+ DELETE FROM ${GroupDatabase.TABLE_NAME} WHERE ${GroupDatabase.GROUP_ID} IN (SELECT ${ThreadDatabase.ADDRESS} FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%');
+ DELETE FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.ADDRESS} IN (SELECT ${ThreadDatabase.ADDRESS} FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%');
+ """.trimIndent()
+
+ @JvmField
+ val DELETE_INACTIVE_ONE_TO_ONES: String = """
+ DELETE FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.OPEN_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.OPEN_GROUP_INBOX_PREFIX}%';
+ """.trimIndent()
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java
index 874440f5de..66c838cc1d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java
@@ -67,7 +67,8 @@ public class DateUtils extends android.text.format.DateUtils {
}
public static String getDisplayFormattedTimeSpanString(final Context c, final Locale locale, final long timestamp) {
- if (isWithin(timestamp, 1, TimeUnit.MINUTES)) {
+ // If the timestamp is invalid (ie. 0) then assume we're waiting on data and just use the 'Now' copy
+ if (timestamp == 0 || isWithin(timestamp, 1, TimeUnit.MINUTES)) {
return c.getString(R.string.DateUtils_just_now);
} else if (isToday(timestamp)) {
return getFormattedDateTime(timestamp, getHourFormat(c), locale);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt b/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt
index 08b81e5cb7..c7d53c1fef 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt
@@ -7,6 +7,7 @@ import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
+import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.annotation.ColorInt
@@ -55,16 +56,21 @@ object GlowViewUtilities {
animation.start()
}
- fun animateShadowColorChange(view: GlowView, @ColorInt startColor: Int, @ColorInt endColor: Int) {
+ fun animateShadowColorChange(
+ view: GlowView,
+ @ColorInt startColor: Int,
+ @ColorInt endColor: Int,
+ duration: Long = 250
+ ) {
val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor)
- animation.duration = 250
+ animation.duration = duration
+ animation.interpolator = AccelerateDecelerateInterpolator()
animation.addUpdateListener { animator ->
val color = animator.animatedValue as Int
view.sessionShadowColor = color
}
animation.start()
}
-
}
class PNModeView : LinearLayout, GlowView {
@@ -223,3 +229,59 @@ class InputBarButtonImageViewContainer : RelativeLayout, GlowView {
}
// endregion
}
+
+class MessageBubbleView : androidx.constraintlayout.widget.ConstraintLayout, GlowView {
+ @ColorInt override var mainColor: Int = 0
+ set(newValue) { field = newValue; paint.color = newValue }
+ @ColorInt override var sessionShadowColor: Int = 0
+ set(newValue) {
+ field = newValue
+ shadowPaint.setShadowLayer(toPx(10, resources).toFloat(), 0.0f, 0.0f, newValue)
+
+ if (numShadowRenders == 0) {
+ numShadowRenders = 1
+ }
+
+ invalidate()
+ }
+ var cornerRadius: Float = 0f
+ var numShadowRenders: Int = 0
+
+ private val paint: Paint by lazy {
+ val result = Paint()
+ result.style = Paint.Style.FILL
+ result.isAntiAlias = true
+ result
+ }
+
+ private val shadowPaint: Paint by lazy {
+ val result = Paint()
+ result.style = Paint.Style.FILL
+ result.isAntiAlias = true
+ result
+ }
+
+ // region Lifecycle
+ constructor(context: Context) : super(context) { }
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { }
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { }
+
+ init {
+ setWillNotDraw(false)
+ }
+ // endregion
+
+ // region Updating
+ override fun onDraw(c: Canvas) {
+ val w = width.toFloat()
+ val h = height.toFloat()
+
+ (0 until numShadowRenders).forEach {
+ c.drawRoundRect(0f, 0f, w, h, cornerRadius, cornerRadius, shadowPaint)
+ }
+
+ c.drawRoundRect(0f, 0f, w, h, cornerRadius, cornerRadius, paint)
+ super.onDraw(c)
+ }
+ // endregion
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt
index 10d507a538..06fda29306 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt
@@ -7,8 +7,6 @@ import org.session.libsession.messaging.messages.signal.IncomingTextMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
-import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
-import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient
@@ -21,7 +19,6 @@ import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.GroupManager
import java.security.SecureRandom
-import java.util.*
import kotlin.random.asKotlinRandom
object MockDataGenerator {
@@ -139,7 +136,6 @@ object MockDataGenerator {
false
),
(timestampNow - (index * 5000)),
- false,
false
)
}
@@ -235,8 +231,9 @@ object MockDataGenerator {
// Add the group to the user's set of public keys to poll for and store the key pair
val encryptionKeyPair = Curve.generateKeyPair()
- storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, randomGroupPublicKey)
+ storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, randomGroupPublicKey, System.currentTimeMillis())
storage.setExpirationTimer(groupId, 0)
+ storage.createInitialConfigGroup(randomGroupPublicKey, groupName, GroupUtil.createConfigMemberMap(members, setOf(adminUserId)), System.currentTimeMillis(), encryptionKeyPair)
// Add the group created message
if (userSessionId == adminUserId) {
@@ -269,7 +266,6 @@ object MockDataGenerator {
false
),
(timestampNow - (index * 5000)),
- false,
false
)
}
@@ -395,7 +391,6 @@ object MockDataGenerator {
false
),
(timestampNow - (index * 5000)),
- false,
false
)
} else {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt
index 59658f12a0..8b219849a0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.util
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
-import android.content.DialogInterface.OnClickListener
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
@@ -12,12 +11,12 @@ import android.provider.MediaStore
import android.text.TextUtils
import android.webkit.MimeTypeMap
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
import network.loki.messenger.R
import org.session.libsession.utilities.task.ProgressDialogAsyncTask
import org.session.libsignal.utilities.ExternalStorageUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.mms.PartAuthority
+import org.thoughtcrime.securesms.showSessionDialog
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@@ -30,7 +29,12 @@ import java.util.concurrent.TimeUnit
* Saves attachment files to an external storage using [MediaStore] API.
* Requires [android.Manifest.permission.WRITE_EXTERNAL_STORAGE] on API 28 and below.
*/
-class SaveAttachmentTask : ProgressDialogAsyncTask> {
+class SaveAttachmentTask @JvmOverloads constructor(context: Context, count: Int = 1) :
+ ProgressDialogAsyncTask>(
+ context,
+ context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count),
+ context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count)
+ ) {
companion object {
@JvmStatic
@@ -41,30 +45,25 @@ class SaveAttachmentTask : ProgressDialogAsyncTask Unit = {}) {
+ context.showSessionDialog {
+ title(R.string.ConversationFragment_save_to_sd_card)
+ iconAttribute(R.attr.dialog_alert_icon)
+ text(context.resources.getQuantityString(
R.plurals.ConversationFragment_saving_n_media_to_storage_warning,
count,
count))
- builder.setPositiveButton(R.string.yes, onAcceptListener)
- builder.setNegativeButton(R.string.no, null)
- builder.show()
+ button(R.string.yes) { onAcceptListener() }
+ button(R.string.no)
+ }
}
}
private val contextReference: WeakReference
- private val attachmentCount: Int
+ private val attachmentCount: Int = count
- @JvmOverloads
- constructor(context: Context, count: Int = 1): super(context,
- context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count),
- context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count)) {
+ init {
this.contextReference = WeakReference(context)
- this.attachmentCount = count
}
override fun doInBackground(vararg attachments: Attachment?): Pair {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt
index 05b6fe86f8..c10e1b635d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/SessionMetaProtocol.kt
@@ -49,11 +49,11 @@ object SessionMetaProtocol {
@JvmStatic
fun shouldSendReadReceipt(recipient: Recipient): Boolean {
- return !recipient.isGroupRecipient && recipient.isApproved
+ return !recipient.isGroupRecipient && recipient.isApproved && !recipient.isBlocked
}
@JvmStatic
fun shouldSendTypingIndicator(recipient: Recipient): Boolean {
- return !recipient.isGroupRecipient && recipient.isApproved
+ return !recipient.isGroupRecipient && recipient.isApproved && !recipient.isBlocked
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt
new file mode 100644
index 0000000000..b15d82a33e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt
@@ -0,0 +1,22 @@
+package org.thoughtcrime.securesms.util
+
+import network.loki.messenger.libsession_util.ConversationVolatileConfig
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.utilities.GroupUtil
+import org.session.libsignal.utilities.IdPrefix
+import org.thoughtcrime.securesms.database.model.ThreadRecord
+
+fun ConversationVolatileConfig.getConversationUnread(thread: ThreadRecord): Boolean {
+ val recipient = thread.recipient
+ if (recipient.isContactRecipient
+ && recipient.isOpenGroupInboxRecipient
+ && recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) {
+ return getOneToOne(recipient.address.serialize())?.unread == true
+ } else if (recipient.isClosedGroupRecipient) {
+ return getLegacyClosedGroup(GroupUtil.doubleDecodeGroupId(recipient.address.toGroupString()))?.unread == true
+ } else if (recipient.isOpenGroupRecipient) {
+ val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(thread.threadId) ?: return false
+ return getCommunity(openGroup.server, openGroup.room)?.unread == true
+ }
+ return false
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt
index 8345473490..dfd4ffe419 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt
@@ -58,7 +58,7 @@ fun View.fadeIn(duration: Long = 150) {
fun View.fadeOut(duration: Long = 150) {
animate().setDuration(duration).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
visibility = View.GONE
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt
index d96de5eedb..894de9de64 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt
@@ -303,7 +303,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
sdpMLineIndexes = sdpMLineIndexes,
sdpMids = sdpMids,
currentCallId
- ), currentRecipient.address)
+ ), currentRecipient.address, isSyncMessage = currentRecipient.isLocalNumber)
}
}
}
@@ -437,7 +437,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
pendingIncomingIceUpdates.clear()
val answerMessage = CallMessage.answer(answer.description, callId)
Log.i("Loki", "Posting new answer")
- MessageSender.sendNonDurably(answerMessage, recipient.address)
+ MessageSender.sendNonDurably(answerMessage, recipient.address, isSyncMessage = recipient.isLocalNumber)
} else {
Promise.ofFail(Exception("Couldn't reconnect from current state"))
}
@@ -481,11 +481,11 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
connection.setLocalDescription(answer)
val answerMessage = CallMessage.answer(answer.description, callId)
val userAddress = storage.getUserPublicKey() ?: return Promise.ofFail(NullPointerException("No user public key"))
- MessageSender.sendNonDurably(answerMessage, Address.fromSerialized(userAddress))
+ MessageSender.sendNonDurably(answerMessage, Address.fromSerialized(userAddress), isSyncMessage = true)
val sendAnswerMessage = MessageSender.sendNonDurably(CallMessage.answer(
answer.description,
callId
- ), recipient.address)
+ ), recipient.address, isSyncMessage = recipient.isLocalNumber)
insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_INCOMING, false)
@@ -535,13 +535,13 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
Log.d("Loki", "Sending pre-offer")
return MessageSender.sendNonDurably(CallMessage.preOffer(
callId
- ), recipient.address).bind {
+ ), recipient.address, isSyncMessage = recipient.isLocalNumber).bind {
Log.d("Loki", "Sent pre-offer")
Log.d("Loki", "Sending offer")
MessageSender.sendNonDurably(CallMessage.offer(
offer.description,
callId
- ), recipient.address).success {
+ ), recipient.address, isSyncMessage = recipient.isLocalNumber).success {
Log.d("Loki", "Sent offer")
}.fail {
Log.e("Loki", "Failed to send offer", it)
@@ -555,8 +555,8 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
val recipient = recipient ?: return
val userAddress = storage.getUserPublicKey() ?: return
stateProcessor.processEvent(Event.DeclineCall) {
- MessageSender.sendNonDurably(CallMessage.endCall(callId), Address.fromSerialized(userAddress))
- MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address)
+ MessageSender.sendNonDurably(CallMessage.endCall(callId), Address.fromSerialized(userAddress), isSyncMessage = true)
+ MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address, isSyncMessage = recipient.isLocalNumber)
insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_MISSED)
}
}
@@ -575,7 +575,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
val buffer = DataChannel.Buffer(ByteBuffer.wrap(HANGUP_JSON.toString().encodeToByteArray()), false)
channel.send(buffer)
}
- MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address)
+ MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address, isSyncMessage = recipient.isLocalNumber)
}
}
@@ -726,7 +726,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
})
connection.setLocalDescription(offer)
- MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId), recipient.address)
+ MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId), recipient.address, isSyncMessage = recipient.isLocalNumber)
}
}
diff --git a/app/src/main/res/color/prominent_button_color.xml b/app/src/main/res/color/prominent_button_color.xml
index 8f2e692fda..39985565d1 100644
--- a/app/src/main/res/color/prominent_button_color.xml
+++ b/app/src/main/res/color/prominent_button_color.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_expand.xml b/app/src/main/res/drawable/ic_expand.xml
new file mode 100644
index 0000000000..3b2b816a45
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_message_details__refresh.xml b/app/src/main/res/drawable/ic_message_details__refresh.xml
new file mode 100644
index 0000000000..2aabe6fbe3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_message_details__refresh.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_message_details__reply.xml b/app/src/main/res/drawable/ic_message_details__reply.xml
new file mode 100644
index 0000000000..c9e1591a53
--- /dev/null
+++ b/app/src/main/res/drawable/ic_message_details__reply.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_message_details__trash.xml b/app/src/main/res/drawable/ic_message_details__trash.xml
new file mode 100644
index 0000000000..85d4216958
--- /dev/null
+++ b/app/src/main/res/drawable/ic_message_details__trash.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_next.xml b/app/src/main/res/drawable/ic_next.xml
new file mode 100644
index 0000000000..1e72d86cb6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_next.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_prev.xml b/app/src/main/res/drawable/ic_prev.xml
new file mode 100644
index 0000000000..f720261670
--- /dev/null
+++ b/app/src/main/res/drawable/ic_prev.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml
index d56b399fc3..5afde1e296 100644
--- a/app/src/main/res/layout/activity_conversation_v2.xml
+++ b/app/src/main/res/layout/activity_conversation_v2.xml
@@ -216,6 +216,19 @@
+
+
-
diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml
index a18661f890..bf308612c5 100644
--- a/app/src/main/res/layout/activity_home.xml
+++ b/app/src/main/res/layout/activity_home.xml
@@ -27,7 +27,7 @@
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp">
-
+ android:layout_height="@dimen/path_status_view_size"
+ android:layout_alignEnd="@+id/profileButton"
+ android:layout_alignBottom="@+id/profileButton" />
+ android:layout_height="?actionBarSize"
+ android:layout_marginHorizontal="@dimen/medium_spacing"
+ android:visibility="gone">
+
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true" />
+
+
+
+
+
+ android:clipChildren="false"
+ android:focusable="false">
+ tools:listitem="@layout/view_global_search_result" />
-
-
-
-
-
+ android:text="@string/blocked_contacts_title"
+ android:textColor="@color/destructive"
+ android:textSize="16sp"
+ android:textStyle="bold"
+/>
diff --git a/app/src/main/res/layout/dialog_blocked.xml b/app/src/main/res/layout/dialog_blocked.xml
deleted file mode 100644
index 4ad01ec8ee..0000000000
--- a/app/src/main/res/layout/dialog_blocked.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_change_avatar.xml b/app/src/main/res/layout/dialog_change_avatar.xml
index 8cb97bcf6a..98ba77ac0c 100644
--- a/app/src/main/res/layout/dialog_change_avatar.xml
+++ b/app/src/main/res/layout/dialog_change_avatar.xml
@@ -45,7 +45,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_join_open_group.xml b/app/src/main/res/layout/dialog_join_open_group.xml
deleted file mode 100644
index 7c422a1e33..0000000000
--- a/app/src/main/res/layout/dialog_join_open_group.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_link_preview.xml b/app/src/main/res/layout/dialog_link_preview.xml
deleted file mode 100644
index 0344439f55..0000000000
--- a/app/src/main/res/layout/dialog_link_preview.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_open_url.xml b/app/src/main/res/layout/dialog_open_url.xml
deleted file mode 100644
index ec0befef21..0000000000
--- a/app/src/main/res/layout/dialog_open_url.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_seed.xml b/app/src/main/res/layout/dialog_seed.xml
deleted file mode 100644
index fac8ffb62f..0000000000
--- a/app/src/main/res/layout/dialog_seed.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_share_logs.xml b/app/src/main/res/layout/dialog_share_logs.xml
deleted file mode 100644
index adb3a9e2d8..0000000000
--- a/app/src/main/res/layout/dialog_share_logs.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_call_bottom_sheet.xml b/app/src/main/res/layout/fragment_call_bottom_sheet.xml
index 7b79d855f9..6ec1c76136 100644
--- a/app/src/main/res/layout/fragment_call_bottom_sheet.xml
+++ b/app/src/main/res/layout/fragment_call_bottom_sheet.xml
@@ -13,7 +13,7 @@
app:behavior_hideable="true"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
-
-
-
-
diff --git a/app/src/main/res/layout/view_conversation.xml b/app/src/main/res/layout/view_conversation.xml
index d66a1722bc..12a7a8ac8c 100644
--- a/app/src/main/res/layout/view_conversation.xml
+++ b/app/src/main/res/layout/view_conversation.xml
@@ -14,7 +14,7 @@
android:layout_height="match_parent"
android:background="?colorAccent" />
-
-
-
-
-
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_user.xml b/app/src/main/res/layout/view_user.xml
index a9330ae646..177b3ff6c9 100644
--- a/app/src/main/res/layout/view_user.xml
+++ b/app/src/main/res/layout/view_user.xml
@@ -15,7 +15,7 @@
android:gravity="center_vertical"
android:paddingHorizontal="@dimen/medium_spacing">
-
+
+
+
+
+
+
-
-
-
+ android:orientation="horizontal">
-
+
-
+
+
+
+
-
-
+
@string/arrays__use_custom
-
- - @string/arrays__mute_for_one_hour
- - @string/arrays__mute_for_two_hours
- - @string/arrays__mute_for_one_day
- - @string/arrays__mute_for_seven_days
- - @string/arrays__mute_forever
-
-
- @string/arrays__name_and_message
- @string/arrays__name_only
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d7948c4f2d..2d46f68605 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,11 +4,15 @@
Yes
No
Delete
+ Resend
+ Reply
Ban
Please wait…
Save
+ Image
Note to Self
Version %s
+ Expand
Create session ID
@@ -516,6 +520,12 @@
To:
From:
With:
+ File Id:
+ File Type:
+ File Size:
+ Resolution:
+ Duration:
+
Create passphrase
Select contacts
@@ -1020,4 +1030,12 @@
Failed to send
Search GIFs?
Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.
+ Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.
+
+ There are no messages in %s.
+ You have no messages in Note to Self.
+ You have no messages from %s.\nSend a message to start the conversation!
+
+ Unread Messages
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index c3ef7c2bc9..2928fc7718 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -71,6 +71,7 @@
+
+
+
+
diff --git a/app/src/main/res/xml/preferences_app_protection.xml b/app/src/main/res/xml/preferences_app_protection.xml
index 0a7a15f70e..12607ee769 100644
--- a/app/src/main/res/xml/preferences_app_protection.xml
+++ b/app/src/main/res/xml/preferences_app_protection.xml
@@ -12,9 +12,8 @@
android:title="@string/preferences_app_protection__screen_lock"
android:summary="@string/preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint" />
-
diff --git a/app/src/sharedTest/java/org/thoughtcrime/securesms/LiveDataTestUtil.kt b/app/src/sharedTest/java/org/thoughtcrime/securesms/LiveDataTestUtil.kt
index 03155a910c..a8cff6341c 100644
--- a/app/src/sharedTest/java/org/thoughtcrime/securesms/LiveDataTestUtil.kt
+++ b/app/src/sharedTest/java/org/thoughtcrime/securesms/LiveDataTestUtil.kt
@@ -22,7 +22,7 @@ fun LiveData.getOrAwaitValue(
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer {
- override fun onChanged(o: T?) {
+ override fun onChanged(o: T) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt
index fdaff22d8f..361c5a4382 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt
+++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt
@@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.first
import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.anyLong
@@ -31,7 +30,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
private lateinit var recipient: Recipient
private val viewModel: ConversationViewModel by lazy {
- ConversationViewModel(threadId, edKeyPair, repository, storage)
+ ConversationViewModel(threadId, edKeyPair, mock(), repository, storage)
}
@Before
diff --git a/app/src/test/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModelTest.kt
index 737c3bec39..8cdb36b6b3 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModelTest.kt
+++ b/app/src/test/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsViewModelTest.kt
@@ -26,9 +26,9 @@ class MessageRequestsViewModelTest : BaseViewModelTest() {
@Test
fun `should clear all message requests`() = runBlockingTest {
- viewModel.clearAllMessageRequests()
+ viewModel.clearAllMessageRequests(block = false)
- verify(repository).clearAllMessageRequests()
+ verify(repository).clearAllMessageRequests(block = false)
}
}
\ No newline at end of file
diff --git a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java b/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java
index 5e3467b70c..c20ce39fb1 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java
+++ b/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java
@@ -9,7 +9,7 @@ import junit.framework.TestCase;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.junit.MockitoJUnitRunner;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.RecipientExporter;
diff --git a/build.gradle b/build.gradle
index 7e7e14f00f..7d9857aaf3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,9 +8,14 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.google.gms:google-services:$googleServicesVersion"
classpath files('libs/gradle-witness.jar')
+ classpath "com.squareup:javapoet:1.13.0"
}
}
+plugins{
+ id("com.google.dagger.hilt.android") version "2.44" apply false
+}
+
allprojects {
repositories {
google()
@@ -51,7 +56,7 @@ allprojects {
project.ext {
androidMinimumSdkVersion = 23
- androidTargetSdkVersion = 31
- androidCompileSdkVersion = 32
+ androidTargetSdkVersion = 33
+ androidCompileSdkVersion = 33
}
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index fa51fdbca0..1d7bc62d40 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,26 +1,40 @@
-android.useAndroidX=true
+## For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+#
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx1024m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+#
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+#Mon Jun 26 09:56:43 AEST 2023
android.enableJetifier=true
-org.gradle.jvmargs=-Xmx8g
gradlePluginVersion=7.3.1
-googleServicesVersion=4.3.12
-kotlinVersion=1.6.21
-coroutinesVersion=1.6.4
-kotlinxJsonVersion=1.3.3
-lifecycleVersion=2.5.1
-daggerVersion=2.40.1
-glideVersion=4.11.0
-kovenantVersion=3.3.0
-curve25519Version=0.6.0
-protobufVersion=2.5.0
-okhttpVersion=3.12.1
-jacksonDatabindVersion=2.9.8
-appcompatVersion=1.5.1
-materialVersion=1.7.0
-preferenceVersion=1.2.0
-coreVersion=1.8.0
+org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
+org.gradle.unsafe.configuration-cache=true
+googleServicesVersion=4.3.12
+kotlinVersion=1.8.21
+android.useAndroidX=true
+appcompatVersion=1.6.1
+coreVersion=1.8.0
+coroutinesVersion=1.6.4
+curve25519Version=0.6.0
+daggerVersion=2.46.1
+glideVersion=4.11.0
+jacksonDatabindVersion=2.9.8
junitVersion=4.13.2
-mockitoKotlinVersion=4.0.0
-testCoreVersion=1.4.0
-pagingVersion=3.0.0
\ No newline at end of file
+kotlinxJsonVersion=1.3.3
+kovenantVersion=3.3.0
+lifecycleVersion=2.5.1
+materialVersion=1.8.0
+mockitoKotlinVersion=4.1.0
+okhttpVersion=3.12.1
+pagingVersion=3.0.0
+preferenceVersion=1.2.0
+protobufVersion=2.5.0
+testCoreVersion=1.5.0
diff --git a/libsession-util/.gitignore b/libsession-util/.gitignore
new file mode 100644
index 0000000000..606666622e
--- /dev/null
+++ b/libsession-util/.gitignore
@@ -0,0 +1,2 @@
+/build
+/.cxx/
diff --git a/libsession-util/build.gradle b/libsession-util/build.gradle
new file mode 100644
index 0000000000..e4f9ed56f3
--- /dev/null
+++ b/libsession-util/build.gradle
@@ -0,0 +1,47 @@
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'network.loki.messenger.libsession_util'
+ compileSdkVersion androidCompileSdkVersion
+
+ defaultConfig {
+ minSdkVersion androidMinimumSdkVersion
+ targetSdkVersion androidCompileSdkVersion
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ externalNativeBuild {
+ cmake {
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+ externalNativeBuild {
+ cmake {
+ path "src/main/cpp/CMakeLists.txt"
+ version "3.22.1"
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+ testImplementation 'junit:junit:4.13.2'
+ implementation(project(":libsignal"))
+ implementation "com.google.protobuf:protobuf-java:$protobufVersion"
+ androidTestImplementation 'androidx.test.ext:junit:1.1.4'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
+}
\ No newline at end of file
diff --git a/libsession-util/libsession-util b/libsession-util/libsession-util
new file mode 160000
index 0000000000..7eb8702835
--- /dev/null
+++ b/libsession-util/libsession-util
@@ -0,0 +1 @@
+Subproject commit 7eb87028355bfc89950102c52d5b2927a25b2e22
diff --git a/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt b/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt
new file mode 100644
index 0000000000..952c357851
--- /dev/null
+++ b/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt
@@ -0,0 +1,584 @@
+package network.loki.messenger.libsession_util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import network.loki.messenger.libsession_util.util.*
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert.*
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.session.libsignal.utilities.Hex
+import org.session.libsignal.utilities.Log
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class InstrumentedTests {
+
+ val seed =
+ Hex.fromStringCondensed("0123456789abcdef0123456789abcdef00000000000000000000000000000000")
+
+ private val keyPair: KeyPair
+ get() {
+ return Sodium.ed25519KeyPair(seed)
+ }
+
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("network.loki.messenger.libsession_util.test", appContext.packageName)
+ }
+
+ @Test
+ fun jni_test_sodium_kp_ed_curve() {
+ val kp = keyPair
+ val curvePkBytes = Sodium.ed25519PkToCurve25519(kp.pubKey)
+
+ val edPk = kp.pubKey
+ val curvePk = curvePkBytes
+
+ assertArrayEquals(Hex.fromStringCondensed("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"), edPk)
+ assertArrayEquals(Hex.fromStringCondensed("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"), curvePk)
+ assertArrayEquals(kp.secretKey.take(32).toByteArray(), seed)
+ }
+
+ @Test
+ fun testDirtyEmptyString() {
+ val contacts = Contacts.newInstance(keyPair.secretKey)
+ val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000"
+ val contact = contacts.getOrConstruct(definitelyRealId)
+ contacts.set(contact)
+ assertTrue(contacts.dirty())
+ contacts.set(contact.copy(name = "test"))
+ assertTrue(contacts.dirty())
+ val push = contacts.push()
+ contacts.confirmPushed(push.seqNo, "abc123")
+ contacts.dump()
+ contacts.set(contact.copy(name = "test2"))
+ contacts.set(contact.copy(name = "test"))
+ assertTrue(contacts.dirty())
+ }
+
+ @Test
+ fun jni_contacts() {
+ val contacts = Contacts.newInstance(keyPair.secretKey)
+ val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000"
+ assertNull(contacts.get(definitelyRealId))
+
+ // Should be an uninitialized contact apart from ID
+ val c = contacts.getOrConstruct(definitelyRealId)
+ assertEquals(definitelyRealId, c.id)
+ assertTrue(c.name.isEmpty())
+ assertTrue(c.nickname.isEmpty())
+ assertFalse(c.approved)
+ assertFalse(c.approvedMe)
+ assertFalse(c.blocked)
+ assertEquals(UserPic.DEFAULT, c.profilePicture)
+
+ assertFalse(contacts.needsPush())
+ assertFalse(contacts.needsDump())
+ assertEquals(0, contacts.push().seqNo)
+
+ c.name = "Joe"
+ c.nickname = "Joey"
+ c.approved = true
+ c.approvedMe = true
+
+ contacts.set(c)
+
+ val cSaved = contacts.get(definitelyRealId)!!
+ assertEquals("Joe", cSaved.name)
+ assertEquals("Joey", cSaved.nickname)
+ assertTrue(cSaved.approved)
+ assertTrue(cSaved.approvedMe)
+ assertFalse(cSaved.blocked)
+ assertEquals(UserPic.DEFAULT, cSaved.profilePicture)
+
+ val push1 = contacts.push()
+
+ assertEquals(1, push1.seqNo)
+ contacts.confirmPushed(push1.seqNo, "fakehash1")
+ assertFalse(contacts.needsPush())
+ assertTrue(contacts.needsDump())
+
+ val contacts2 = Contacts.newInstance(keyPair.secretKey, contacts.dump())
+ assertFalse(contacts.needsDump())
+ assertFalse(contacts2.needsPush())
+ assertFalse(contacts2.needsDump())
+
+ val anotherId = "051111111111111111111111111111111111111111111111111111111111111111"
+ val c2 = contacts2.getOrConstruct(anotherId)
+ contacts2.set(c2)
+ val push2 = contacts2.push()
+ assertEquals(2, push2.seqNo)
+ contacts2.confirmPushed(push2.seqNo, "fakehash2")
+ assertFalse(contacts2.needsPush())
+
+ contacts.merge("fakehash2" to push2.config)
+
+
+ assertFalse(contacts.needsPush())
+ assertEquals(push2.seqNo, contacts.push().seqNo)
+
+ val contactList = contacts.all().toList()
+ assertEquals(definitelyRealId, contactList[0].id)
+ assertEquals(anotherId, contactList[1].id)
+ assertEquals("Joey", contactList[0].nickname)
+ assertEquals("", contactList[1].nickname)
+
+ contacts.erase(definitelyRealId)
+
+ val thirdId ="052222222222222222222222222222222222222222222222222222222222222222"
+ val third = Contact(
+ id = thirdId,
+ nickname = "Nickname 3",
+ approved = true,
+ blocked = true,
+ profilePicture = UserPic("http://example.com/huge.bmp", "qwertyuio01234567890123456789012".encodeToByteArray()),
+ expiryMode = ExpiryMode.NONE
+ )
+ contacts2.set(third)
+ assertTrue(contacts.needsPush())
+ assertTrue(contacts2.needsPush())
+ val toPush = contacts.push()
+ val toPush2 = contacts2.push()
+ assertEquals(toPush.seqNo, toPush2.seqNo)
+ assertThat(toPush2.config, not(equals(toPush.config)))
+
+ contacts.confirmPushed(toPush.seqNo, "fakehash3a")
+ contacts2.confirmPushed(toPush2.seqNo, "fakehash3b")
+
+ contacts.merge("fakehash3b" to toPush2.config)
+ contacts2.merge("fakehash3a" to toPush.config)
+
+ assertTrue(contacts.needsPush())
+ assertTrue(contacts2.needsPush())
+
+ val mergePush = contacts.push()
+ val mergePush2 = contacts2.push()
+
+ assertEquals(mergePush.seqNo, mergePush2.seqNo)
+ assertArrayEquals(mergePush.config, mergePush2.config)
+
+ assertTrue(mergePush.obsoleteHashes.containsAll(listOf("fakehash3b", "fakehash3a")))
+ assertTrue(mergePush2.obsoleteHashes.containsAll(listOf("fakehash3b", "fakehash3a")))
+
+ }
+
+ @Test
+ fun jni_accessible() {
+ val userProfile = UserProfile.newInstance(keyPair.secretKey)
+ assertNotNull(userProfile)
+ userProfile.free()
+ }
+
+ @Test
+ fun jni_user_profile_c_api() {
+ val edSk = keyPair.secretKey
+ val userProfile = UserProfile.newInstance(edSk)
+
+ // these should be false as empty config
+ assertFalse(userProfile.needsPush())
+ assertFalse(userProfile.needsDump())
+
+ // Since it's empty there shouldn't be a name
+ assertNull(userProfile.getName())
+
+ // Don't need to push yet so this is just for testing
+ val (_, seqNo) = userProfile.push() // disregarding encrypted
+ assertEquals("UserProfile", userProfile.encryptionDomain())
+ assertEquals(0, seqNo)
+
+ // This should also be unset:
+ assertEquals(UserPic.DEFAULT, userProfile.getPic())
+
+ // Now let's go set a profile name and picture:
+ // not sending keylen like c api so cutting off the NOTSECRET in key for testing purposes
+ userProfile.setName("Kallie")
+ val newUserPic = UserPic("http://example.org/omg-pic-123.bmp", "secret78901234567890123456789012".encodeToByteArray())
+ userProfile.setPic(newUserPic)
+ userProfile.setNtsPriority(9)
+
+ // Retrieve them just to make sure they set properly:
+ assertEquals("Kallie", userProfile.getName())
+ val pic = userProfile.getPic()
+ assertEquals("http://example.org/omg-pic-123.bmp", pic.url)
+ assertEquals("secret78901234567890123456789012", pic.key.decodeToString())
+
+ // Since we've made changes, we should need to push new config to the swarm, *and* should need
+ // to dump the updated state:
+ assertTrue(userProfile.needsPush())
+ assertTrue(userProfile.needsDump())
+ val (newToPush, newSeqNo) = userProfile.push()
+
+ val expHash0 =
+ Hex.fromStringCondensed("ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
+
+ val expectedPush1Decrypted = ("d" +
+ "1:#"+ "i1e" +
+ "1:&"+ "d"+
+ "1:+"+ "i9e"+
+ "1:n"+ "6:Kallie"+
+ "1:p"+ "34:http://example.org/omg-pic-123.bmp"+
+ "1:q"+ "32:secret78901234567890123456789012"+
+ "e"+
+ "1:<"+ "l"+
+ "l"+ "i0e"+ "32:").encodeToByteArray() + expHash0 + ("de"+ "e"+
+ "e"+
+ "1:="+ "d"+
+ "1:+" +"0:"+
+ "1:n" +"0:"+
+ "1:p" +"0:"+
+ "1:q" +"0:"+
+ "e"+
+ "e").encodeToByteArray()
+
+ assertEquals(1, newSeqNo)
+ // We haven't dumped, so still need to dump:
+ assertTrue(userProfile.needsDump())
+ // We did call push but we haven't confirmed it as stored yet, so this will still return true:
+ assertTrue(userProfile.needsPush())
+
+ val dump = userProfile.dump()
+ // (in a real client we'd now store this to disk)
+ assertFalse(userProfile.needsDump())
+ val expectedDump = ("d" +
+ "1:!"+ "i2e" +
+ "1:$").encodeToByteArray() + expectedPush1Decrypted.size.toString().encodeToByteArray() +
+ ":".encodeToByteArray() + expectedPush1Decrypted +
+ "1:(0:1:)le".encodeToByteArray()+
+ "e".encodeToByteArray()
+
+ assertArrayEquals(expectedDump, dump)
+
+ userProfile.confirmPushed(newSeqNo, "fakehash1")
+
+ val newConf = UserProfile.newInstance(edSk)
+
+ val accepted = newConf.merge("fakehash1" to newToPush)
+ assertEquals(1, accepted)
+
+ assertTrue(newConf.needsDump())
+ assertFalse(newConf.needsPush())
+ val _ignore = newConf.dump()
+ assertFalse(newConf.needsDump())
+
+
+ userProfile.setName("Raz")
+ newConf.setName("Nibbler")
+ newConf.setPic(UserPic("http://new.example.com/pic", "qwertyuio01234567890123456789012".encodeToByteArray()))
+
+ val conf = userProfile.push()
+ val conf2 = newConf.push()
+
+ userProfile.confirmPushed(conf.seqNo, "fakehash2")
+ newConf.confirmPushed(conf2.seqNo, "fakehash3")
+
+ userProfile.dump()
+
+ assertFalse(conf.config.contentEquals(conf2.config))
+
+ newConf.merge("fakehash2" to conf.config)
+ userProfile.merge("fakehash3" to conf2.config)
+
+ assertTrue(newConf.needsPush())
+ assertTrue(userProfile.needsPush())
+
+ val newSeq1 = userProfile.push()
+
+ assertEquals(3, newSeq1.seqNo)
+
+ userProfile.confirmPushed(newSeq1.seqNo, "fakehash4")
+
+ // assume newConf push gets rejected as it was last to write and clear previous config by hash on oxenss
+ newConf.merge("fakehash4" to newSeq1.config)
+
+ val newSeqMerge = newConf.push()
+
+ newConf.confirmPushed(newSeqMerge.seqNo, "fakehash5")
+
+ assertEquals("Raz", newConf.getName())
+ assertEquals(3, newSeqMerge.seqNo)
+
+ // userProfile device polls and merges
+ userProfile.merge("fakehash5" to newSeqMerge.config)
+
+ val userConfigMerge = userProfile.push()
+
+ assertEquals(3, userConfigMerge.seqNo)
+
+ assertEquals("Raz", newConf.getName())
+ assertEquals("Raz", userProfile.getName())
+
+ userProfile.free()
+ newConf.free()
+ }
+
+ @Test
+ fun merge_resolves_conflicts() {
+ val kp = keyPair
+ val a = UserProfile.newInstance(kp.secretKey)
+ val b = UserProfile.newInstance(kp.secretKey)
+ a.setName("A")
+ val (aPush, aSeq) = a.push()
+ a.confirmPushed(aSeq, "hashfroma")
+ b.setName("B")
+ // polls and sees invalid state, has to merge
+ b.merge("hashfroma" to aPush)
+ val (bPush, bSeq) = b.push()
+ b.confirmPushed(bSeq, "hashfromb")
+ assertEquals("B", b.getName())
+ assertEquals(1, aSeq)
+ assertEquals(2, bSeq)
+ a.merge("hashfromb" to bPush)
+ assertEquals(2, a.push().seqNo)
+ }
+
+ @Test
+ fun jni_setting_getting() {
+ val userProfile = UserProfile.newInstance(keyPair.secretKey)
+ val newName = "test"
+ println("Name being set via JNI call: $newName")
+ userProfile.setName(newName)
+ val nameFromNative = userProfile.getName()
+ assertEquals(newName, nameFromNative)
+ println("Name received by JNI call: $nameFromNative")
+ assertTrue(userProfile.dirty())
+ userProfile.free()
+ }
+
+ @Test
+ fun jni_remove_all_test() {
+ val convos = ConversationVolatileConfig.newInstance(keyPair.secretKey)
+ assertEquals(0 /* number removed */, convos.eraseAll { true /* 'erase' every item */ })
+
+ val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000"
+ val definitelyRealConvo = Conversation.OneToOne(definitelyRealId, System.currentTimeMillis(), false)
+ convos.set(definitelyRealConvo)
+
+ val anotherDefinitelyReadId = "051111111111111111111111111111111111111111111111111111111111111111"
+ val anotherDefinitelyRealConvo = Conversation.OneToOne(anotherDefinitelyReadId, System.currentTimeMillis(), false)
+ convos.set(anotherDefinitelyRealConvo)
+
+ assertEquals(2, convos.sizeOneToOnes())
+
+ val numErased = convos.eraseAll { convo ->
+ convo is Conversation.OneToOne && convo.sessionId == definitelyRealId
+ }
+ assertEquals(1, numErased)
+ assertEquals(1, convos.sizeOneToOnes())
+ }
+
+ @Test
+ fun test_open_group_urls() {
+ val (base1, room1, pk1) = BaseCommunityInfo.parseFullUrl(
+ "https://example.com/" +
+ "someroom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ )!!
+
+ val (base2, room2, pk2) = BaseCommunityInfo.parseFullUrl(
+ "HTTPS://EXAMPLE.COM/" +
+ "someroom?public_key=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"
+ )!!
+
+ val (base3, room3, pk3) = BaseCommunityInfo.parseFullUrl(
+ "HTTPS://EXAMPLE.COM/r/" +
+ "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
+ )!!
+
+ val (base4, room4, pk4) = BaseCommunityInfo.parseFullUrl(
+ "http://example.com/r/" +
+ "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
+ )!!
+
+ val (base5, room5, pk5) = BaseCommunityInfo.parseFullUrl(
+ "HTTPS://EXAMPLE.com:443/r/" +
+ "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
+ )!!
+
+ val (base6, room6, pk6) = BaseCommunityInfo.parseFullUrl(
+ "HTTP://EXAMPLE.com:80/r/" +
+ "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
+ )!!
+
+ val (base7, room7, pk7) = BaseCommunityInfo.parseFullUrl(
+ "http://example.com:80/r/" +
+ "someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8"
+ )!!
+ val (base8, room8, pk8) = BaseCommunityInfo.parseFullUrl(
+ "http://example.com:80/r/" +
+ "someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo"
+ )!!
+
+ assertEquals("https://example.com", base1)
+ assertEquals("http://example.com", base4)
+ assertEquals(base1, base2)
+ assertEquals(base1, base3)
+ assertNotEquals(base1, base4)
+ assertEquals(base1, base5)
+ assertEquals(base4, base6)
+ assertEquals(base4, base7)
+ assertEquals(base4, base8)
+ assertEquals("someroom", room1)
+ assertEquals("someroom", room2)
+ assertEquals("someroom", room3)
+ assertEquals("someroom", room4)
+ assertEquals("someroom", room5)
+ assertEquals("someroom", room6)
+ assertEquals("someroom", room7)
+ assertEquals("someroom", room8)
+ assertEquals(Hex.toStringCondensed(pk1), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
+ assertEquals(Hex.toStringCondensed(pk2), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
+ assertEquals(Hex.toStringCondensed(pk3), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
+ assertEquals(Hex.toStringCondensed(pk4), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
+ assertEquals(Hex.toStringCondensed(pk5), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
+ assertEquals(Hex.toStringCondensed(pk6), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
+ assertEquals(Hex.toStringCondensed(pk7), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
+ assertEquals(Hex.toStringCondensed(pk8), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
+
+ }
+
+ @Test
+ fun test_conversations() {
+ val convos = ConversationVolatileConfig.newInstance(keyPair.secretKey)
+ val definitelyRealId = "055000000000000000000000000000000000000000000000000000000000000000"
+ assertNull(convos.getOneToOne(definitelyRealId))
+ assertTrue(convos.empty())
+ assertEquals(0, convos.size())
+
+ val c = convos.getOrConstructOneToOne(definitelyRealId)
+
+ assertEquals(definitelyRealId, c.sessionId)
+ assertEquals(0, c.lastRead)
+
+ assertFalse(convos.needsPush())
+ assertFalse(convos.needsDump())
+ assertEquals(0, convos.push().seqNo)
+
+ val nowMs = System.currentTimeMillis()
+
+ c.lastRead = nowMs
+
+ convos.set(c)
+
+ assertNull(convos.getLegacyClosedGroup(definitelyRealId))
+ assertNotNull(convos.getOneToOne(definitelyRealId))
+ assertEquals(nowMs, convos.getOneToOne(definitelyRealId)?.lastRead)
+
+ assertTrue(convos.needsPush())
+ assertTrue(convos.needsDump())
+
+ val openGroupPubKey = Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
+
+ val og = convos.getOrConstructCommunity("http://Example.ORG:5678", "SudokuRoom", openGroupPubKey)
+ val ogCommunity = og.baseCommunityInfo
+
+ assertEquals("http://example.org:5678", ogCommunity.baseUrl) // Note: lower-case
+ assertEquals("sudokuroom", ogCommunity.room) // Note: lower-case
+ assertEquals(64, ogCommunity.pubKeyHex.length)
+ assertEquals("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ogCommunity.pubKeyHex)
+
+ og.unread = true
+
+ convos.set(og)
+
+ val (_, seqNo) = convos.push()
+
+ assertEquals(1, seqNo)
+
+ convos.confirmPushed(seqNo, "fakehash1")
+
+ assertTrue(convos.needsDump())
+ assertFalse(convos.needsPush())
+
+ val convos2 = ConversationVolatileConfig.newInstance(keyPair.secretKey, convos.dump())
+ assertFalse(convos.needsPush())
+ assertFalse(convos.needsDump())
+ assertEquals(1, convos.push().seqNo)
+ assertFalse(convos.needsDump())
+
+ val x1 = convos2.getOneToOne(definitelyRealId)!!
+ assertEquals(nowMs, x1.lastRead)
+ assertEquals(definitelyRealId, x1.sessionId)
+ assertEquals(false, x1.unread)
+
+ val x2 = convos2.getCommunity("http://EXAMPLE.org:5678", "sudokuRoom")!!
+ val x2Info = x2.baseCommunityInfo
+ assertEquals("http://example.org:5678", x2Info.baseUrl)
+ assertEquals("sudokuroom", x2Info.room)
+ assertEquals(x2Info.pubKeyHex, Hex.toStringCondensed(openGroupPubKey))
+ assertTrue(x2.unread)
+
+ val anotherId = "051111111111111111111111111111111111111111111111111111111111111111"
+ val c2 = convos.getOrConstructOneToOne(anotherId)
+ c2.unread = true
+ convos2.set(c2)
+
+ val c3 = convos.getOrConstructLegacyGroup(
+ "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ )
+ c3.lastRead = nowMs - 50
+ convos2.set(c3)
+
+ assertTrue(convos2.needsPush())
+
+ val (toPush2, seqNo2) = convos2.push()
+ assertEquals(2, seqNo2)
+
+ convos2.confirmPushed(seqNo2, "fakehash2")
+ convos.merge("fakehash2" to toPush2)
+
+ assertFalse(convos.needsPush())
+ assertEquals(seqNo2, convos.push().seqNo)
+
+ val seen = mutableListOf()
+ for ((ind, conv) in listOf(convos, convos2).withIndex()) {
+ Log.e("Test","Testing seen from convo #$ind")
+ seen.clear()
+ assertEquals(4, conv.size())
+ assertEquals(2, conv.sizeOneToOnes())
+ assertEquals(1, conv.sizeCommunities())
+ assertEquals(1, conv.sizeLegacyClosedGroups())
+ assertFalse(conv.empty())
+ val allConvos = conv.all()
+ for (convo in allConvos) {
+ when (convo) {
+ is Conversation.OneToOne -> seen.add("1-to-1: ${convo.sessionId}")
+ is Conversation.Community -> seen.add("og: ${convo.baseCommunityInfo.baseUrl}/r/${convo.baseCommunityInfo.room}")
+ is Conversation.LegacyGroup -> seen.add("cl: ${convo.groupId}")
+ }
+ }
+
+ assertTrue(seen.contains("1-to-1: 051111111111111111111111111111111111111111111111111111111111111111"))
+ assertTrue(seen.contains("1-to-1: 055000000000000000000000000000000000000000000000000000000000000000"))
+ assertTrue(seen.contains("og: http://example.org:5678/r/sudokuroom"))
+ assertTrue(seen.contains("cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"))
+ assertTrue(seen.size == 4) // for some reason iterative checks aren't working in test cases
+ }
+
+ assertFalse(convos.needsPush())
+ convos.eraseOneToOne("052000000000000000000000000000000000000000000000000000000000000000")
+ assertFalse(convos.needsPush())
+ convos.eraseOneToOne("055000000000000000000000000000000000000000000000000000000000000000")
+ assertTrue(convos.needsPush())
+
+ assertEquals(1, convos.allOneToOnes().size)
+ assertEquals("051111111111111111111111111111111111111111111111111111111111111111",
+ convos.allOneToOnes().map(Conversation.OneToOne::sessionId).first()
+ )
+ assertEquals(1, convos.allCommunities().size)
+ assertEquals("http://example.org:5678",
+ convos.allCommunities().map { it.baseCommunityInfo.baseUrl }.first()
+ )
+ assertEquals(1, convos.allLegacyClosedGroups().size)
+ assertEquals("05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
+ convos.allLegacyClosedGroups().map(Conversation.LegacyGroup::groupId).first()
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/AndroidManifest.xml b/libsession-util/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..65483324a6
--- /dev/null
+++ b/libsession-util/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/CMakeLists.txt b/libsession-util/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000000..d01523a24e
--- /dev/null
+++ b/libsession-util/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,66 @@
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html
+
+# Sets the minimum version of CMake required to build the native library.
+
+cmake_minimum_required(VERSION 3.18.1)
+
+# Declares and names the project.
+
+project("session_util")
+
+# Compiles in C++17 mode
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+set(CMAKE_BUILD_TYPE Release)
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+
+set(STATIC_BUNDLE ON)
+add_subdirectory(../../../libsession-util libsession)
+
+set(SOURCES
+ user_profile.cpp
+ user_groups.cpp
+ config_base.cpp
+ contacts.cpp
+ conversation.cpp
+ util.cpp)
+
+add_library( # Sets the name of the library.
+ session_util
+ # Sets the library as a shared library.
+ SHARED
+ # Provides a relative path to your source file(s).
+ ${SOURCES})
+
+# Searches for a specified prebuilt library and stores the path as a
+# variable. Because CMake includes system libraries in the search path by
+# default, you only need to specify the name of the public NDK library
+# you want to add. CMake verifies that the library exists before
+# completing its build.
+
+find_library( # Sets the name of the path variable.
+ log-lib
+
+ # Specifies the name of the NDK library that
+ # you want CMake to locate.
+ log)
+
+# Specifies libraries CMake should link to your target library. You
+# can link multiple libraries, such as libraries you define in this
+# build script, prebuilt third-party libraries, or system libraries.
+
+target_link_libraries( # Specifies the target library.
+ session_util
+ PUBLIC
+ libsession::config
+ libsession::crypto
+ # Links the target library to the log library
+ # included in the NDK.
+ ${log-lib})
diff --git a/libsession-util/src/main/cpp/config_base.cpp b/libsession-util/src/main/cpp/config_base.cpp
new file mode 100644
index 0000000000..eed3ec56af
--- /dev/null
+++ b/libsession-util/src/main/cpp/config_base.cpp
@@ -0,0 +1,154 @@
+#include "config_base.h"
+#include "util.h"
+
+extern "C" {
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_dirty(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto* configBase = ptrToConfigBase(env, thiz);
+ return configBase->is_dirty();
+}
+
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_needsPush(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto config = ptrToConfigBase(env, thiz);
+ return config->needs_push();
+}
+
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_needsDump(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto config = ptrToConfigBase(env, thiz);
+ return config->needs_dump();
+}
+
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_push(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto config = ptrToConfigBase(env, thiz);
+ auto push_tuple = config->push();
+ auto to_push_str = std::get<1>(push_tuple);
+ auto to_delete = std::get<2>(push_tuple);
+
+ jbyteArray returnByteArray = util::bytes_from_ustring(env, to_push_str);
+ jlong seqNo = std::get<0>(push_tuple);
+ jclass returnObjectClass = env->FindClass("network/loki/messenger/libsession_util/util/ConfigPush");
+ jclass stackClass = env->FindClass("java/util/Stack");
+ jmethodID methodId = env->GetMethodID(returnObjectClass, "", "([BJLjava/util/List;)V");
+ jmethodID stack_init = env->GetMethodID(stackClass, "", "()V");
+ jobject our_stack = env->NewObject(stackClass, stack_init);
+ jmethodID push_stack = env->GetMethodID(stackClass, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ for (auto entry : to_delete) {
+ auto entry_jstring = env->NewStringUTF(entry.data());
+ env->CallObjectMethod(our_stack, push_stack, entry_jstring);
+ }
+ jobject returnObject = env->NewObject(returnObjectClass, methodId, returnByteArray, seqNo, our_stack);
+ return returnObject;
+}
+
+JNIEXPORT void JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_free(JNIEnv *env, jobject thiz) {
+ auto config = ptrToConfigBase(env, thiz);
+ delete config;
+}
+
+JNIEXPORT jbyteArray JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_dump(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto config = ptrToConfigBase(env, thiz);
+ auto dumped = config->dump();
+ jbyteArray bytes = util::bytes_from_ustring(env, dumped);
+ return bytes;
+}
+
+JNIEXPORT jstring JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_encryptionDomain(JNIEnv *env,
+ jobject thiz) {
+ auto conf = ptrToConfigBase(env, thiz);
+ return env->NewStringUTF(conf->encryption_domain());
+}
+
+JNIEXPORT void JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_confirmPushed(JNIEnv *env, jobject thiz,
+ jlong seq_no,
+ jstring new_hash_jstring) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToConfigBase(env, thiz);
+ auto new_hash = env->GetStringUTFChars(new_hash_jstring, nullptr);
+ conf->confirm_pushed(seq_no, new_hash);
+ env->ReleaseStringUTFChars(new_hash_jstring, new_hash);
+}
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "bugprone-reserved-identifier"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
+ jobjectArray to_merge) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToConfigBase(env, thiz);
+ size_t number = env->GetArrayLength(to_merge);
+ std::vector> configs = {};
+ for (int i = 0; i < number; i++) {
+ auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i);
+ auto pair = extractHashAndData(env, jElement);
+ configs.push_back(pair);
+ }
+ return conf->merge(configs);
+}
+
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_merge__Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
+ jobject to_merge) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToConfigBase(env, thiz);
+ std::vector> configs = {extractHashAndData(env, to_merge)};
+ return conf->merge(configs);
+}
+
+#pragma clang diagnostic pop
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_configNamespace(JNIEnv *env, jobject thiz) {
+ auto conf = ptrToConfigBase(env, thiz);
+ return (std::int16_t) conf->storage_namespace();
+}
+extern "C"
+JNIEXPORT jclass JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_00024Companion_kindFor(JNIEnv *env,
+ jobject thiz,
+ jint config_namespace) {
+ auto user_class = env->FindClass("network/loki/messenger/libsession_util/UserProfile");
+ auto contact_class = env->FindClass("network/loki/messenger/libsession_util/Contacts");
+ auto convo_volatile_class = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig");
+ auto group_list_class = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig");
+ switch (config_namespace) {
+ case (int)session::config::Namespace::UserProfile:
+ return user_class;
+ case (int)session::config::Namespace::Contacts:
+ return contact_class;
+ case (int)session::config::Namespace::ConvoInfoVolatile:
+ return convo_volatile_class;
+ case (int)session::config::Namespace::UserGroups:
+ return group_list_class;
+ default:
+ return nullptr;
+ }
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConfigBase_currentHashes(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToConfigBase(env, thiz);
+ jclass stack = env->FindClass("java/util/Stack");
+ jmethodID init = env->GetMethodID(stack, "", "()V");
+ jobject our_stack = env->NewObject(stack, init);
+ jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ auto vec = conf->current_hashes();
+ for (std::string element: vec) {
+ env->CallObjectMethod(our_stack, push, env->NewStringUTF(element.data()));
+ }
+ return our_stack;
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/config_base.h b/libsession-util/src/main/cpp/config_base.h
new file mode 100644
index 0000000000..836fb04ef5
--- /dev/null
+++ b/libsession-util/src/main/cpp/config_base.h
@@ -0,0 +1,28 @@
+#ifndef SESSION_ANDROID_CONFIG_BASE_H
+#define SESSION_ANDROID_CONFIG_BASE_H
+
+#include "session/config/base.hpp"
+#include "util.h"
+#include
+#include
+
+inline session::config::ConfigBase* ptrToConfigBase(JNIEnv *env, jobject obj) {
+ jclass baseClass = env->FindClass("network/loki/messenger/libsession_util/ConfigBase");
+ jfieldID pointerField = env->GetFieldID(baseClass, "pointer", "J");
+ return (session::config::ConfigBase*) env->GetLongField(obj, pointerField);
+}
+
+inline std::pair extractHashAndData(JNIEnv *env, jobject kotlin_pair) {
+ jclass pair = env->FindClass("kotlin/Pair");
+ jfieldID first = env->GetFieldID(pair, "first", "Ljava/lang/Object;");
+ jfieldID second = env->GetFieldID(pair, "second", "Ljava/lang/Object;");
+ jstring hash_as_jstring = static_cast(env->GetObjectField(kotlin_pair, first));
+ jbyteArray data_as_jbytes = static_cast(env->GetObjectField(kotlin_pair, second));
+ auto hash_as_string = env->GetStringUTFChars(hash_as_jstring, nullptr);
+ auto data_as_ustring = util::ustring_from_bytes(env, data_as_jbytes);
+ auto ret_pair = std::pair{hash_as_string, data_as_ustring};
+ env->ReleaseStringUTFChars(hash_as_jstring, hash_as_string);
+ return ret_pair;
+}
+
+#endif
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/contacts.cpp b/libsession-util/src/main/cpp/contacts.cpp
new file mode 100644
index 0000000000..7d04904802
--- /dev/null
+++ b/libsession-util/src/main/cpp/contacts.cpp
@@ -0,0 +1,100 @@
+#include "contacts.h"
+#include "util.h"
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject thiz,
+ jstring session_id) {
+ std::lock_guard lock{util::util_mutex_};
+ auto contacts = ptrToContacts(env, thiz);
+ auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
+ auto contact = contacts->get(session_id_chars);
+ env->ReleaseStringUTFChars(session_id, session_id_chars);
+ if (!contact) return nullptr;
+ jobject j_contact = serialize_contact(env, contact.value());
+ return j_contact;
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_Contacts_getOrConstruct(JNIEnv *env, jobject thiz,
+ jstring session_id) {
+ std::lock_guard lock{util::util_mutex_};
+ auto contacts = ptrToContacts(env, thiz);
+ auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
+ auto contact = contacts->get_or_construct(session_id_chars);
+ env->ReleaseStringUTFChars(session_id, session_id_chars);
+ return serialize_contact(env, contact);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_network_loki_messenger_libsession_1util_Contacts_set(JNIEnv *env, jobject thiz,
+ jobject contact) {
+ std::lock_guard lock{util::util_mutex_};
+ auto contacts = ptrToContacts(env, thiz);
+ auto contact_info = deserialize_contact(env, contact, contacts);
+ contacts->set(contact_info);
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject thiz,
+ jstring session_id) {
+ std::lock_guard lock{util::util_mutex_};
+ auto contacts = ptrToContacts(env, thiz);
+ auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
+
+ bool result = contacts->erase(session_id_chars);
+ env->ReleaseStringUTFChars(session_id, session_id_chars);
+ return result;
+}
+extern "C"
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "bugprone-reserved-identifier"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B(JNIEnv *env,
+ jobject thiz,
+ jbyteArray ed25519_secret_key) {
+ std::lock_guard lock{util::util_mutex_};
+ auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
+ auto* contacts = new session::config::Contacts(secret_key, std::nullopt);
+
+ jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
+ jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V");
+ jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast(contacts));
+
+ return newConfig;
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B_3B(
+ JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) {
+ std::lock_guard lock{util::util_mutex_};
+ auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
+ auto initial = util::ustring_from_bytes(env, initial_dump);
+
+ auto* contacts = new session::config::Contacts(secret_key, initial);
+
+ jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
+ jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V");
+ jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast(contacts));
+
+ return newConfig;
+}
+#pragma clang diagnostic pop
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto contacts = ptrToContacts(env, thiz);
+ jclass stack = env->FindClass("java/util/Stack");
+ jmethodID init = env->GetMethodID(stack, "", "()V");
+ jobject our_stack = env->NewObject(stack, init);
+ jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ for (const auto& contact : *contacts) {
+ auto contact_obj = serialize_contact(env, contact);
+ env->CallObjectMethod(our_stack, push, contact_obj);
+ }
+ return our_stack;
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/contacts.h b/libsession-util/src/main/cpp/contacts.h
new file mode 100644
index 0000000000..c5496a68c8
--- /dev/null
+++ b/libsession-util/src/main/cpp/contacts.h
@@ -0,0 +1,109 @@
+#ifndef SESSION_ANDROID_CONTACTS_H
+#define SESSION_ANDROID_CONTACTS_H
+
+#include
+#include "session/config/contacts.hpp"
+#include "util.h"
+
+inline session::config::Contacts *ptrToContacts(JNIEnv *env, jobject obj) {
+ jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
+ jfieldID pointerField = env->GetFieldID(contactsClass, "pointer", "J");
+ return (session::config::Contacts *) env->GetLongField(obj, pointerField);
+}
+
+inline jobject serialize_contact(JNIEnv *env, session::config::contact_info info) {
+ jclass contactClass = env->FindClass("network/loki/messenger/libsession_util/util/Contact");
+ jmethodID constructor = env->GetMethodID(contactClass, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZLnetwork/loki/messenger/libsession_util/util/UserPic;ILnetwork/loki/messenger/libsession_util/util/ExpiryMode;)V");
+ jstring id = env->NewStringUTF(info.session_id.data());
+ jstring name = env->NewStringUTF(info.name.data());
+ jstring nickname = env->NewStringUTF(info.nickname.data());
+ jboolean approved, approvedMe, blocked;
+ approved = info.approved;
+ approvedMe = info.approved_me;
+ blocked = info.blocked;
+ auto created = info.created;
+ jobject profilePic = util::serialize_user_pic(env, info.profile_picture);
+ jobject returnObj = env->NewObject(contactClass, constructor, id, name, nickname, approved,
+ approvedMe, blocked, profilePic, info.priority,
+ util::serialize_expiry(env, info.exp_mode, info.exp_timer));
+ return returnObj;
+}
+
+inline session::config::contact_info
+deserialize_contact(JNIEnv *env, jobject info, session::config::Contacts *conf) {
+ jclass contactClass = env->FindClass("network/loki/messenger/libsession_util/util/Contact");
+
+ jfieldID getId, getName, getNick, getApproved, getApprovedMe, getBlocked, getUserPic, getPriority, getExpiry, getHidden;
+ getId = env->GetFieldID(contactClass, "id", "Ljava/lang/String;");
+ getName = env->GetFieldID(contactClass, "name", "Ljava/lang/String;");
+ getNick = env->GetFieldID(contactClass, "nickname", "Ljava/lang/String;");
+ getApproved = env->GetFieldID(contactClass, "approved", "Z");
+ getApprovedMe = env->GetFieldID(contactClass, "approvedMe", "Z");
+ getBlocked = env->GetFieldID(contactClass, "blocked", "Z");
+ getUserPic = env->GetFieldID(contactClass, "profilePicture",
+ "Lnetwork/loki/messenger/libsession_util/util/UserPic;");
+ getPriority = env->GetFieldID(contactClass, "priority", "I");
+ getExpiry = env->GetFieldID(contactClass, "expiryMode", "Lnetwork/loki/messenger/libsession_util/util/ExpiryMode;");
+ jstring name, nickname, session_id;
+ session_id = static_cast(env->GetObjectField(info, getId));
+ name = static_cast(env->GetObjectField(info, getName));
+ nickname = static_cast(env->GetObjectField(info, getNick));
+ bool approved, approvedMe, blocked, hidden;
+ int priority = env->GetIntField(info, getPriority);
+ approved = env->GetBooleanField(info, getApproved);
+ approvedMe = env->GetBooleanField(info, getApprovedMe);
+ blocked = env->GetBooleanField(info, getBlocked);
+ jobject user_pic = env->GetObjectField(info, getUserPic);
+ jobject expiry_mode = env->GetObjectField(info, getExpiry);
+
+ auto expiry_pair = util::deserialize_expiry(env, expiry_mode);
+
+ std::string url;
+ session::ustring key;
+
+ if (user_pic != nullptr) {
+ auto deserialized_pic = util::deserialize_user_pic(env, user_pic);
+ auto url_jstring = deserialized_pic.first;
+ auto url_bytes = env->GetStringUTFChars(url_jstring, nullptr);
+ url = std::string(url_bytes);
+ env->ReleaseStringUTFChars(url_jstring, url_bytes);
+ key = util::ustring_from_bytes(env, deserialized_pic.second);
+ }
+
+ auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr);
+ auto name_bytes = name ? env->GetStringUTFChars(name, nullptr) : nullptr;
+ auto nickname_bytes = nickname ? env->GetStringUTFChars(nickname, nullptr) : nullptr;
+
+ auto contact_info = conf->get_or_construct(session_id_bytes);
+ if (name_bytes) {
+ contact_info.name = name_bytes;
+ }
+ if (nickname_bytes) {
+ contact_info.nickname = nickname_bytes;
+ }
+ contact_info.approved = approved;
+ contact_info.approved_me = approvedMe;
+ contact_info.blocked = blocked;
+ if (!url.empty() && !key.empty()) {
+ contact_info.profile_picture = session::config::profile_pic(url, key);
+ } else {
+ contact_info.profile_picture = session::config::profile_pic();
+ }
+
+ env->ReleaseStringUTFChars(session_id, session_id_bytes);
+ if (name_bytes) {
+ env->ReleaseStringUTFChars(name, name_bytes);
+ }
+ if (nickname_bytes) {
+ env->ReleaseStringUTFChars(nickname, nickname_bytes);
+ }
+
+ contact_info.priority = priority;
+ contact_info.exp_mode = expiry_pair.first;
+ contact_info.exp_timer = std::chrono::seconds(expiry_pair.second);
+
+ return contact_info;
+}
+
+
+#endif //SESSION_ANDROID_CONTACTS_H
diff --git a/libsession-util/src/main/cpp/conversation.cpp b/libsession-util/src/main/cpp/conversation.cpp
new file mode 100644
index 0000000000..4f0f531dea
--- /dev/null
+++ b/libsession-util/src/main/cpp/conversation.cpp
@@ -0,0 +1,352 @@
+#include
+#include "conversation.h"
+
+#pragma clang diagnostic push
+
+extern "C"
+#pragma ide diagnostic ignored "bugprone-reserved-identifier"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_00024Companion_newInstance___3B(
+ JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key) {
+ std::lock_guard lock{util::util_mutex_};
+ auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
+ auto* convo_info_volatile = new session::config::ConvoInfoVolatile(secret_key, std::nullopt);
+
+ jclass convoClass = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig");
+ jmethodID constructor = env->GetMethodID(convoClass, "", "(J)V");
+ jobject newConfig = env->NewObject(convoClass, constructor, reinterpret_cast(convo_info_volatile));
+
+ return newConfig;
+}
+extern "C"
+#pragma ide diagnostic ignored "bugprone-reserved-identifier"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_00024Companion_newInstance___3B_3B(
+ JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) {
+ std::lock_guard lock{util::util_mutex_};
+ auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
+ auto initial = util::ustring_from_bytes(env, initial_dump);
+ auto* convo_info_volatile = new session::config::ConvoInfoVolatile(secret_key, initial);
+
+ jclass convoClass = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig");
+ jmethodID constructor = env->GetMethodID(convoClass, "", "(J)V");
+ jobject newConfig = env->NewObject(convoClass, constructor, reinterpret_cast(convo_info_volatile));
+
+ return newConfig;
+}
+
+
+
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeOneToOnes(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conversations = ptrToConvoInfo(env, thiz);
+ return conversations->size_1to1();
+}
+
+#pragma clang diagnostic pop
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseAll(JNIEnv *env,
+ jobject thiz,
+ jobject predicate) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conversations = ptrToConvoInfo(env, thiz);
+
+ jclass predicate_class = env->FindClass("kotlin/jvm/functions/Function1");
+ jmethodID predicate_call = env->GetMethodID(predicate_class, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;");
+
+ jclass bool_class = env->FindClass("java/lang/Boolean");
+ jmethodID bool_get = env->GetMethodID(bool_class, "booleanValue", "()Z");
+
+ int removed = 0;
+ auto to_erase = std::vector();
+
+ for (auto it = conversations->begin(); it != conversations->end(); ++it) {
+ auto result = env->CallObjectMethod(predicate, predicate_call, serialize_any(env, *it));
+ bool bool_result = env->CallBooleanMethod(result, bool_get);
+ if (bool_result) {
+ to_erase.push_back(*it);
+ }
+ }
+
+ for (auto & entry : to_erase) {
+ if (conversations->erase(entry)) {
+ removed++;
+ }
+ }
+
+ return removed;
+}
+
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_size(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto config = ptrToConvoInfo(env, thiz);
+ return (jint)config->size();
+}
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_empty(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto config = ptrToConvoInfo(env, thiz);
+ return config->empty();
+}
+extern "C"
+JNIEXPORT void JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_set(JNIEnv *env,
+ jobject thiz,
+ jobject to_store) {
+ std::lock_guard lock{util::util_mutex_};
+
+ auto convos = ptrToConvoInfo(env, thiz);
+
+ jclass one_to_one = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne");
+ jclass open_group = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community");
+ jclass legacy_closed_group = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup");
+
+ jclass to_store_class = env->GetObjectClass(to_store);
+ if (env->IsSameObject(to_store_class, one_to_one)) {
+ // store as 1to1
+ convos->set(deserialize_one_to_one(env, to_store, convos));
+ } else if (env->IsSameObject(to_store_class,open_group)) {
+ // store as open_group
+ convos->set(deserialize_community(env, to_store, convos));
+ } else if (env->IsSameObject(to_store_class,legacy_closed_group)) {
+ // store as legacy_closed_group
+ convos->set(deserialize_legacy_closed_group(env, to_store, convos));
+ }
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOneToOne(JNIEnv *env,
+ jobject thiz,
+ jstring pub_key_hex) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto param = env->GetStringUTFChars(pub_key_hex, nullptr);
+ auto internal = convos->get_1to1(param);
+ env->ReleaseStringUTFChars(pub_key_hex, param);
+ if (internal) {
+ return serialize_one_to_one(env, *internal);
+ }
+ return nullptr;
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructOneToOne(
+ JNIEnv *env, jobject thiz, jstring pub_key_hex) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto param = env->GetStringUTFChars(pub_key_hex, nullptr);
+ auto internal = convos->get_or_construct_1to1(param);
+ env->ReleaseStringUTFChars(pub_key_hex, param);
+ return serialize_one_to_one(env, internal);
+}
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseOneToOne(JNIEnv *env,
+ jobject thiz,
+ jstring pub_key_hex) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto param = env->GetStringUTFChars(pub_key_hex, nullptr);
+ auto result = convos->erase_1to1(param);
+ env->ReleaseStringUTFChars(pub_key_hex, param);
+ return result;
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getCommunity__Ljava_lang_String_2Ljava_lang_String_2(
+ JNIEnv *env, jobject thiz, jstring base_url, jstring room) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto base_url_chars = env->GetStringUTFChars(base_url, nullptr);
+ auto room_chars = env->GetStringUTFChars(room, nullptr);
+ auto open = convos->get_community(base_url_chars, room_chars);
+ if (open) {
+ auto serialized = serialize_open_group(env, *open);
+ return serialized;
+ }
+ return nullptr;
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructCommunity__Ljava_lang_String_2Ljava_lang_String_2_3B(
+ JNIEnv *env, jobject thiz, jstring base_url, jstring room, jbyteArray pub_key) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto base_url_chars = env->GetStringUTFChars(base_url, nullptr);
+ auto room_chars = env->GetStringUTFChars(room, nullptr);
+ auto pub_key_ustring = util::ustring_from_bytes(env, pub_key);
+ auto open = convos->get_or_construct_community(base_url_chars, room_chars, pub_key_ustring);
+ auto serialized = serialize_open_group(env, open);
+ return serialized;
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructCommunity__Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2(
+ JNIEnv *env, jobject thiz, jstring base_url, jstring room, jstring pub_key_hex) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto base_url_chars = env->GetStringUTFChars(base_url, nullptr);
+ auto room_chars = env->GetStringUTFChars(room, nullptr);
+ auto hex_chars = env->GetStringUTFChars(pub_key_hex, nullptr);
+ auto open = convos->get_or_construct_community(base_url_chars, room_chars, hex_chars);
+ env->ReleaseStringUTFChars(base_url, base_url_chars);
+ env->ReleaseStringUTFChars(room, room_chars);
+ env->ReleaseStringUTFChars(pub_key_hex, hex_chars);
+ auto serialized = serialize_open_group(env, open);
+ return serialized;
+}
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseCommunity__Lnetwork_loki_messenger_libsession_1util_util_Conversation_Community_2(JNIEnv *env,
+ jobject thiz,
+ jobject open_group) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto deserialized = deserialize_community(env, open_group, convos);
+ return convos->erase(deserialized);
+}
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseCommunity__Ljava_lang_String_2Ljava_lang_String_2(
+ JNIEnv *env, jobject thiz, jstring base_url, jstring room) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto base_url_chars = env->GetStringUTFChars(base_url, nullptr);
+ auto room_chars = env->GetStringUTFChars(room, nullptr);
+ auto result = convos->erase_community(base_url_chars, room_chars);
+ env->ReleaseStringUTFChars(base_url, base_url_chars);
+ env->ReleaseStringUTFChars(room, room_chars);
+ return result;
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getLegacyClosedGroup(
+ JNIEnv *env, jobject thiz, jstring group_id) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto id_chars = env->GetStringUTFChars(group_id, nullptr);
+ auto lgc = convos->get_legacy_group(id_chars);
+ env->ReleaseStringUTFChars(group_id, id_chars);
+ if (lgc) {
+ auto serialized = serialize_legacy_group(env, *lgc);
+ return serialized;
+ }
+ return nullptr;
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructLegacyGroup(
+ JNIEnv *env, jobject thiz, jstring group_id) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto id_chars = env->GetStringUTFChars(group_id, nullptr);
+ auto lgc = convos->get_or_construct_legacy_group(id_chars);
+ env->ReleaseStringUTFChars(group_id, id_chars);
+ return serialize_legacy_group(env, lgc);
+}
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseLegacyClosedGroup(
+ JNIEnv *env, jobject thiz, jstring group_id) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto id_chars = env->GetStringUTFChars(group_id, nullptr);
+ auto result = convos->erase_legacy_group(id_chars);
+ env->ReleaseStringUTFChars(group_id, id_chars);
+ return result;
+}
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_erase(JNIEnv *env,
+ jobject thiz,
+ jobject conversation) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ auto deserialized = deserialize_any(env, conversation, convos);
+ if (!deserialized.has_value()) return false;
+ return convos->erase(*deserialized);
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeCommunities(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ return convos->size_communities();
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeLegacyClosedGroups(
+ JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ return convos->size_legacy_groups();
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_all(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ jclass stack = env->FindClass("java/util/Stack");
+ jmethodID init = env->GetMethodID(stack, "", "()V");
+ jobject our_stack = env->NewObject(stack, init);
+ jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ for (const auto& convo : *convos) {
+ auto contact_obj = serialize_any(env, convo);
+ env->CallObjectMethod(our_stack, push, contact_obj);
+ }
+ return our_stack;
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_allOneToOnes(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ jclass stack = env->FindClass("java/util/Stack");
+ jmethodID init = env->GetMethodID(stack, "", "()V");
+ jobject our_stack = env->NewObject(stack, init);
+ jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ for (auto contact = convos->begin_1to1(); contact != convos->end(); ++contact)
+ env->CallObjectMethod(our_stack, push, serialize_one_to_one(env, *contact));
+ return our_stack;
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_allCommunities(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ jclass stack = env->FindClass("java/util/Stack");
+ jmethodID init = env->GetMethodID(stack, "", "()V");
+ jobject our_stack = env->NewObject(stack, init);
+ jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ for (auto contact = convos->begin_communities(); contact != convos->end(); ++contact)
+ env->CallObjectMethod(our_stack, push, serialize_open_group(env, *contact));
+ return our_stack;
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_allLegacyClosedGroups(
+ JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto convos = ptrToConvoInfo(env, thiz);
+ jclass stack = env->FindClass("java/util/Stack");
+ jmethodID init = env->GetMethodID(stack, "", "()V");
+ jobject our_stack = env->NewObject(stack, init);
+ jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ for (auto contact = convos->begin_legacy_groups(); contact != convos->end(); ++contact)
+ env->CallObjectMethod(our_stack, push, serialize_legacy_group(env, *contact));
+ return our_stack;
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/conversation.h b/libsession-util/src/main/cpp/conversation.h
new file mode 100644
index 0000000000..45e453a595
--- /dev/null
+++ b/libsession-util/src/main/cpp/conversation.h
@@ -0,0 +1,122 @@
+#ifndef SESSION_ANDROID_CONVERSATION_H
+#define SESSION_ANDROID_CONVERSATION_H
+
+#include
+#include "util.h"
+#include "session/config/convo_info_volatile.hpp"
+
+inline session::config::ConvoInfoVolatile *ptrToConvoInfo(JNIEnv *env, jobject obj) {
+ jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig");
+ jfieldID pointerField = env->GetFieldID(contactsClass, "pointer", "J");
+ return (session::config::ConvoInfoVolatile *) env->GetLongField(obj, pointerField);
+}
+
+inline jobject serialize_one_to_one(JNIEnv *env, session::config::convo::one_to_one one_to_one) {
+ jclass clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne");
+ jmethodID constructor = env->GetMethodID(clazz, "", "(Ljava/lang/String;JZ)V");
+ auto session_id = env->NewStringUTF(one_to_one.session_id.data());
+ auto last_read = one_to_one.last_read;
+ auto unread = one_to_one.unread;
+ jobject serialized = env->NewObject(clazz, constructor, session_id, last_read, unread);
+ return serialized;
+}
+
+inline jobject serialize_open_group(JNIEnv *env, session::config::convo::community community) {
+ jclass clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community");
+ auto base_community = util::serialize_base_community(env, community);
+ jmethodID constructor = env->GetMethodID(clazz, "",
+ "(Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;JZ)V");
+ auto last_read = community.last_read;
+ auto unread = community.unread;
+ jobject serialized = env->NewObject(clazz, constructor, base_community, last_read, unread);
+ return serialized;
+}
+
+inline jobject serialize_legacy_group(JNIEnv *env, session::config::convo::legacy_group group) {
+ jclass clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup");
+ jmethodID constructor = env->GetMethodID(clazz, "", "(Ljava/lang/String;JZ)V");
+ auto group_id = env->NewStringUTF(group.id.data());
+ auto last_read = group.last_read;
+ auto unread = group.unread;
+ jobject serialized = env->NewObject(clazz, constructor, group_id, last_read, unread);
+ return serialized;
+}
+
+inline jobject serialize_any(JNIEnv *env, session::config::convo::any any) {
+ if (auto* dm = std::get_if(&any)) {
+ return serialize_one_to_one(env, *dm);
+ } else if (auto* og = std::get_if(&any)) {
+ return serialize_open_group(env, *og);
+ } else if (auto* lgc = std::get_if(&any)) {
+ return serialize_legacy_group(env, *lgc);
+ }
+ return nullptr;
+}
+
+inline session::config::convo::one_to_one deserialize_one_to_one(JNIEnv *env, jobject info, session::config::ConvoInfoVolatile *conf) {
+ auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne");
+ auto id_getter = env->GetFieldID(clazz, "sessionId", "Ljava/lang/String;");
+ auto last_read_getter = env->GetFieldID(clazz, "lastRead", "J");
+ auto unread_getter = env->GetFieldID(clazz, "unread", "Z");
+ jstring id = static_cast(env->GetObjectField(info, id_getter));
+ auto id_chars = env->GetStringUTFChars(id, nullptr);
+ std::string id_string = std::string{id_chars};
+ auto deserialized = conf->get_or_construct_1to1(id_string);
+ deserialized.last_read = env->GetLongField(info, last_read_getter);
+ deserialized.unread = env->GetBooleanField(info, unread_getter);
+ env->ReleaseStringUTFChars(id, id_chars);
+ return deserialized;
+}
+
+inline session::config::convo::community deserialize_community(JNIEnv *env, jobject info, session::config::ConvoInfoVolatile *conf) {
+ auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community");
+ auto base_community_getter = env->GetFieldID(clazz, "baseCommunityInfo", "Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;");
+ auto last_read_getter = env->GetFieldID(clazz, "lastRead", "J");
+ auto unread_getter = env->GetFieldID(clazz, "unread", "Z");
+
+ auto base_community_info = env->GetObjectField(info, base_community_getter);
+
+ auto base_community_deserialized = util::deserialize_base_community(env, base_community_info);
+ auto deserialized = conf->get_or_construct_community(
+ base_community_deserialized.base_url(),
+ base_community_deserialized.room(),
+ base_community_deserialized.pubkey()
+ );
+
+ deserialized.last_read = env->GetLongField(info, last_read_getter);
+ deserialized.unread = env->GetBooleanField(info, unread_getter);
+
+ return deserialized;
+}
+
+inline session::config::convo::legacy_group deserialize_legacy_closed_group(JNIEnv *env, jobject info, session::config::ConvoInfoVolatile *conf) {
+ auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup");
+ auto group_id_getter = env->GetFieldID(clazz, "groupId", "Ljava/lang/String;");
+ auto last_read_getter = env->GetFieldID(clazz, "lastRead", "J");
+ auto unread_getter = env->GetFieldID(clazz, "unread", "Z");
+ auto group_id = static_cast(env->GetObjectField(info, group_id_getter));
+ auto group_id_bytes = env->GetStringUTFChars(group_id, nullptr);
+ auto group_id_string = std::string{group_id_bytes};
+ auto deserialized = conf->get_or_construct_legacy_group(group_id_string);
+ deserialized.last_read = env->GetLongField(info, last_read_getter);
+ deserialized.unread = env->GetBooleanField(info, unread_getter);
+ env->ReleaseStringUTFChars(group_id, group_id_bytes);
+ return deserialized;
+}
+
+inline std::optional deserialize_any(JNIEnv *env, jobject convo, session::config::ConvoInfoVolatile *conf) {
+ auto oto_class = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne");
+ auto og_class = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community");
+ auto lgc_class = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup");
+ auto object_class = env->GetObjectClass(convo);
+ if (env->IsSameObject(object_class, oto_class)) {
+ return session::config::convo::any{deserialize_one_to_one(env, convo, conf)};
+ } else if (env->IsSameObject(object_class, og_class)) {
+ return session::config::convo::any{deserialize_community(env, convo, conf)};
+ } else if (env->IsSameObject(object_class, lgc_class)) {
+ return session::config::convo::any{deserialize_legacy_closed_group(env, convo, conf)};
+ }
+ return std::nullopt;
+}
+
+#endif //SESSION_ANDROID_CONVERSATION_H
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/user_groups.cpp b/libsession-util/src/main/cpp/user_groups.cpp
new file mode 100644
index 0000000000..4f2b0e6b85
--- /dev/null
+++ b/libsession-util/src/main/cpp/user_groups.cpp
@@ -0,0 +1,273 @@
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "bugprone-reserved-identifier"
+#include "user_groups.h"
+
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "bugprone-reserved-identifier"
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_00024Companion_newInstance___3B(
+ JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key) {
+ std::lock_guard lock{util::util_mutex_};
+ auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
+
+ auto* user_groups = new session::config::UserGroups(secret_key, std::nullopt);
+
+ jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig");
+ jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V");
+ jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast(user_groups));
+
+ return newConfig;
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_00024Companion_newInstance___3B_3B(
+ JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) {
+ std::lock_guard lock{util::util_mutex_};
+ auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
+ auto initial = util::ustring_from_bytes(env, initial_dump);
+
+ auto* user_groups = new session::config::UserGroups(secret_key, initial);
+
+ jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig");
+ jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V");
+ jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast(user_groups));
+
+ return newConfig;
+}
+#pragma clang diagnostic pop
+
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_util_GroupInfo_00024LegacyGroupInfo_00024Companion_NAME_1MAX_1LENGTH(
+ JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ return session::config::legacy_group_info::NAME_MAX_LENGTH;
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getCommunityInfo(JNIEnv *env,
+ jobject thiz,
+ jstring base_url,
+ jstring room) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ auto base_url_bytes = env->GetStringUTFChars(base_url, nullptr);
+ auto room_bytes = env->GetStringUTFChars(room, nullptr);
+
+ auto community = conf->get_community(base_url_bytes, room_bytes);
+
+ jobject community_info = nullptr;
+
+ if (community) {
+ community_info = serialize_community_info(env, *community);
+ }
+ env->ReleaseStringUTFChars(base_url, base_url_bytes);
+ env->ReleaseStringUTFChars(room, room_bytes);
+ return community_info;
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getLegacyGroupInfo(JNIEnv *env,
+ jobject thiz,
+ jstring session_id) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ auto id_bytes = env->GetStringUTFChars(session_id, nullptr);
+ auto legacy_group = conf->get_legacy_group(id_bytes);
+ jobject return_group = nullptr;
+ if (legacy_group) {
+ return_group = serialize_legacy_group_info(env, *legacy_group);
+ }
+ env->ReleaseStringUTFChars(session_id, id_bytes);
+ return return_group;
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getOrConstructCommunityInfo(
+ JNIEnv *env, jobject thiz, jstring base_url, jstring room, jstring pub_key_hex) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ auto base_url_bytes = env->GetStringUTFChars(base_url, nullptr);
+ auto room_bytes = env->GetStringUTFChars(room, nullptr);
+ auto pub_hex_bytes = env->GetStringUTFChars(pub_key_hex, nullptr);
+
+ auto group = conf->get_or_construct_community(base_url_bytes, room_bytes, pub_hex_bytes);
+
+ env->ReleaseStringUTFChars(base_url, base_url_bytes);
+ env->ReleaseStringUTFChars(room, room_bytes);
+ env->ReleaseStringUTFChars(pub_key_hex, pub_hex_bytes);
+ return serialize_community_info(env, group);
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getOrConstructLegacyGroupInfo(
+ JNIEnv *env, jobject thiz, jstring session_id) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ auto id_bytes = env->GetStringUTFChars(session_id, nullptr);
+ auto group = conf->get_or_construct_legacy_group(id_bytes);
+ env->ReleaseStringUTFChars(session_id, id_bytes);
+ return serialize_legacy_group_info(env, group);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_set__Lnetwork_loki_messenger_libsession_1util_util_GroupInfo_2(
+ JNIEnv *env, jobject thiz, jobject group_info) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ auto community_info = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo");
+ auto legacy_info = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo");
+ auto object_class = env->GetObjectClass(group_info);
+ if (env->IsSameObject(community_info, object_class)) {
+ auto deserialized = deserialize_community_info(env, group_info, conf);
+ conf->set(deserialized);
+ } else if (env->IsSameObject(legacy_info, object_class)) {
+ auto deserialized = deserialize_legacy_group_info(env, group_info, conf);
+ conf->set(deserialized);
+ }
+}
+
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_erase__Lnetwork_loki_messenger_libsession_1util_util_GroupInfo_2(
+ JNIEnv *env, jobject thiz, jobject group_info) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ auto communityInfo = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo");
+ auto legacyInfo = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo");
+ if (env->GetObjectClass(group_info) == communityInfo) {
+ auto deserialized = deserialize_community_info(env, group_info, conf);
+ conf->erase(deserialized);
+ } else if (env->GetObjectClass(group_info) == legacyInfo) {
+ auto deserialized = deserialize_legacy_group_info(env, group_info, conf);
+ conf->erase(deserialized);
+ }
+}
+
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_sizeCommunityInfo(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ return conf->size_communities();
+}
+
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_sizeLegacyGroupInfo(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ return conf->size_legacy_groups();
+}
+
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_size(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToConvoInfo(env, thiz);
+ return conf->size();
+}
+
+inline jobject iterator_as_java_stack(JNIEnv *env, const session::config::UserGroups::iterator& begin, const session::config::UserGroups::iterator& end) {
+ jclass stack = env->FindClass("java/util/Stack");
+ jmethodID init = env->GetMethodID(stack, "", "()V");
+ jobject our_stack = env->NewObject(stack, init);
+ jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ for (auto it = begin; it != end;) {
+ // do something with it
+ auto item = *it;
+ jobject serialized = nullptr;
+ if (auto* lgc = std::get_if(&item)) {
+ serialized = serialize_legacy_group_info(env, *lgc);
+ } else if (auto* community = std::get_if(&item)) {
+ serialized = serialize_community_info(env, *community);
+ }
+ if (serialized != nullptr) {
+ env->CallObjectMethod(our_stack, push, serialized);
+ }
+ it++;
+ }
+ return our_stack;
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_all(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ jobject all_stack = iterator_as_java_stack(env, conf->begin(), conf->end());
+ return all_stack;
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_allCommunityInfo(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ jobject community_stack = iterator_as_java_stack(env, conf->begin_communities(), conf->end());
+ return community_stack;
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_allLegacyGroupInfo(JNIEnv *env,
+ jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ jobject legacy_stack = iterator_as_java_stack(env, conf->begin_legacy_groups(), conf->end());
+ return legacy_stack;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseCommunity__Lnetwork_loki_messenger_libsession_1util_util_BaseCommunityInfo_2(JNIEnv *env,
+ jobject thiz,
+ jobject base_community_info) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ auto base_community = util::deserialize_base_community(env, base_community_info);
+ return conf->erase_community(base_community.base_url(),base_community.room());
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseCommunity__Ljava_lang_String_2Ljava_lang_String_2(
+ JNIEnv *env, jobject thiz, jstring server, jstring room) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ auto server_bytes = env->GetStringUTFChars(server, nullptr);
+ auto room_bytes = env->GetStringUTFChars(room, nullptr);
+ auto community = conf->get_community(server_bytes, room_bytes);
+ bool deleted = false;
+ if (community) {
+ deleted = conf->erase(*community);
+ }
+ env->ReleaseStringUTFChars(server, server_bytes);
+ env->ReleaseStringUTFChars(room, room_bytes);
+ return deleted;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseLegacyGroup(JNIEnv *env,
+ jobject thiz,
+ jstring session_id) {
+ std::lock_guard lock{util::util_mutex_};
+ auto conf = ptrToUserGroups(env, thiz);
+ auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr);
+ bool return_bool = conf->erase_legacy_group(session_id_bytes);
+ env->ReleaseStringUTFChars(session_id, session_id_bytes);
+ return return_bool;
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/user_groups.h b/libsession-util/src/main/cpp/user_groups.h
new file mode 100644
index 0000000000..c4754fe113
--- /dev/null
+++ b/libsession-util/src/main/cpp/user_groups.h
@@ -0,0 +1,139 @@
+
+#ifndef SESSION_ANDROID_USER_GROUPS_H
+#define SESSION_ANDROID_USER_GROUPS_H
+
+#include "jni.h"
+#include "util.h"
+#include "conversation.h"
+#include "session/config/user_groups.hpp"
+
+inline session::config::UserGroups* ptrToUserGroups(JNIEnv *env, jobject obj) {
+ jclass configClass = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig");
+ jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J");
+ return (session::config::UserGroups*) env->GetLongField(obj, pointerField);
+}
+
+inline void deserialize_members_into(JNIEnv *env, jobject members_map, session::config::legacy_group_info& to_append_group) {
+ jclass map_class = env->FindClass("java/util/Map");
+ jclass map_entry_class = env->FindClass("java/util/Map$Entry");
+ jclass set_class = env->FindClass("java/util/Set");
+ jclass iterator_class = env->FindClass("java/util/Iterator");
+ jclass boxed_bool = env->FindClass("java/lang/Boolean");
+
+ jmethodID get_entry_set = env->GetMethodID(map_class, "entrySet", "()Ljava/util/Set;");
+ jmethodID get_at = env->GetMethodID(set_class, "iterator", "()Ljava/util/Iterator;");
+ jmethodID has_next = env->GetMethodID(iterator_class, "hasNext", "()Z");
+ jmethodID next = env->GetMethodID(iterator_class, "next", "()Ljava/lang/Object;");
+ jmethodID get_key = env->GetMethodID(map_entry_class, "getKey", "()Ljava/lang/Object;");
+ jmethodID get_value = env->GetMethodID(map_entry_class, "getValue", "()Ljava/lang/Object;");
+ jmethodID get_bool_value = env->GetMethodID(boxed_bool, "booleanValue", "()Z");
+
+ jobject entry_set = env->CallObjectMethod(members_map, get_entry_set);
+ jobject iterator = env->CallObjectMethod(entry_set, get_at);
+
+ while (env->CallBooleanMethod(iterator, has_next)) {
+ jobject entry = env->CallObjectMethod(iterator, next);
+ jstring key = static_cast(env->CallObjectMethod(entry, get_key));
+ jobject boxed = env->CallObjectMethod(entry, get_value);
+ bool is_admin = env->CallBooleanMethod(boxed, get_bool_value);
+ auto member_string = env->GetStringUTFChars(key, nullptr);
+ to_append_group.insert(member_string, is_admin);
+ env->ReleaseStringUTFChars(key, member_string);
+ }
+}
+
+inline session::config::legacy_group_info deserialize_legacy_group_info(JNIEnv *env, jobject info, session::config::UserGroups* conf) {
+ auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo");
+ auto id_field = env->GetFieldID(clazz, "sessionId", "Ljava/lang/String;");
+ auto name_field = env->GetFieldID(clazz, "name", "Ljava/lang/String;");
+ auto members_field = env->GetFieldID(clazz, "members", "Ljava/util/Map;");
+ auto enc_pub_key_field = env->GetFieldID(clazz, "encPubKey", "[B");
+ auto enc_sec_key_field = env->GetFieldID(clazz, "encSecKey", "[B");
+ auto priority_field = env->GetFieldID(clazz, "priority", "I");
+ auto disappearing_timer_field = env->GetFieldID(clazz, "disappearingTimer", "J");
+ auto joined_at_field = env->GetFieldID(clazz, "joinedAt", "J");
+ jstring id = static_cast(env->GetObjectField(info, id_field));
+ jstring name = static_cast(env->GetObjectField(info, name_field));
+ jobject members_map = env->GetObjectField(info, members_field);
+ jbyteArray enc_pub_key = static_cast(env->GetObjectField(info, enc_pub_key_field));
+ jbyteArray enc_sec_key = static_cast(env->GetObjectField(info, enc_sec_key_field));
+ int priority = env->GetIntField(info, priority_field);
+ long joined_at = env->GetLongField(info, joined_at_field);
+
+ auto id_bytes = env->GetStringUTFChars(id, nullptr);
+ auto name_bytes = env->GetStringUTFChars(name, nullptr);
+ auto enc_pub_key_bytes = util::ustring_from_bytes(env, enc_pub_key);
+ auto enc_sec_key_bytes = util::ustring_from_bytes(env, enc_sec_key);
+
+ auto info_deserialized = conf->get_or_construct_legacy_group(id_bytes);
+
+ auto current_members = info_deserialized.members();
+ for (auto member = current_members.begin(); member != current_members.end(); ++member) {
+ info_deserialized.erase(member->first);
+ }
+ deserialize_members_into(env, members_map, info_deserialized);
+ info_deserialized.name = name_bytes;
+ info_deserialized.enc_pubkey = enc_pub_key_bytes;
+ info_deserialized.enc_seckey = enc_sec_key_bytes;
+ info_deserialized.priority = priority;
+ info_deserialized.disappearing_timer = std::chrono::seconds(env->GetLongField(info, disappearing_timer_field));
+ info_deserialized.joined_at = joined_at;
+ env->ReleaseStringUTFChars(id, id_bytes);
+ env->ReleaseStringUTFChars(name, name_bytes);
+ return info_deserialized;
+}
+
+inline session::config::community_info deserialize_community_info(JNIEnv *env, jobject info, session::config::UserGroups* conf) {
+ auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo");
+ auto base_info = env->GetFieldID(clazz, "community", "Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;");
+ auto priority = env->GetFieldID(clazz, "priority", "I");
+ jobject base_community_info = env->GetObjectField(info, base_info);
+ auto deserialized_base_info = util::deserialize_base_community(env, base_community_info);
+ int deserialized_priority = env->GetIntField(info, priority);
+ auto community_info = conf->get_or_construct_community(deserialized_base_info.base_url(), deserialized_base_info.room(), deserialized_base_info.pubkey_hex());
+ community_info.priority = deserialized_priority;
+ return community_info;
+}
+
+inline jobject serialize_members(JNIEnv *env, std::map members_map) {
+ jclass map_class = env->FindClass("java/util/HashMap");
+ jclass boxed_bool = env->FindClass("java/lang/Boolean");
+ jmethodID map_constructor = env->GetMethodID(map_class, "", "()V");
+ jmethodID insert = env->GetMethodID(map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+ jmethodID new_bool = env->GetMethodID(boxed_bool, "", "(Z)V");
+
+ jobject new_map = env->NewObject(map_class, map_constructor);
+ for (auto it = members_map.begin(); it != members_map.end(); it++) {
+ auto session_id = env->NewStringUTF(it->first.data());
+ bool is_admin = it->second;
+ auto jbool = env->NewObject(boxed_bool, new_bool, is_admin);
+ env->CallObjectMethod(new_map, insert, session_id, jbool);
+ }
+ return new_map;
+}
+
+inline jobject serialize_legacy_group_info(JNIEnv *env, session::config::legacy_group_info info) {
+ jstring session_id = env->NewStringUTF(info.session_id.data());
+ jstring name = env->NewStringUTF(info.name.data());
+ jobject members = serialize_members(env, info.members());
+ jbyteArray enc_pubkey = util::bytes_from_ustring(env, info.enc_pubkey);
+ jbyteArray enc_seckey = util::bytes_from_ustring(env, info.enc_seckey);
+ int priority = info.priority;
+ long joined_at = info.joined_at;
+
+ jclass legacy_group_class = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo");
+ jmethodID constructor = env->GetMethodID(legacy_group_class, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[B[BIJJ)V");
+ jobject serialized = env->NewObject(legacy_group_class, constructor, session_id, name, members, enc_pubkey, enc_seckey, priority, (jlong) info.disappearing_timer.count(), joined_at);
+ return serialized;
+}
+
+inline jobject serialize_community_info(JNIEnv *env, session::config::community_info info) {
+ auto priority = info.priority;
+ auto serialized_info = util::serialize_base_community(env, info);
+ auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo");
+ jmethodID constructor = env->GetMethodID(clazz, "", "(Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;I)V");
+ jobject serialized = env->NewObject(clazz, constructor, serialized_info, priority);
+ return serialized;
+}
+
+#endif //SESSION_ANDROID_USER_GROUPS_H
diff --git a/libsession-util/src/main/cpp/user_profile.cpp b/libsession-util/src/main/cpp/user_profile.cpp
new file mode 100644
index 0000000000..78b671ef0d
--- /dev/null
+++ b/libsession-util/src/main/cpp/user_profile.cpp
@@ -0,0 +1,98 @@
+#include "user_profile.h"
+#include "util.h"
+
+extern "C" {
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "bugprone-reserved-identifier"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserProfile_00024Companion_newInstance___3B_3B(
+ JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) {
+ std::lock_guard lock{util::util_mutex_};
+ auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
+ auto initial = util::ustring_from_bytes(env, initial_dump);
+ auto* profile = new session::config::UserProfile(secret_key, std::optional(initial));
+
+ jclass userClass = env->FindClass("network/loki/messenger/libsession_util/UserProfile");
+ jmethodID constructor = env->GetMethodID(userClass, "", "(J)V");
+ jobject newConfig = env->NewObject(userClass, constructor, reinterpret_cast(profile));
+
+ return newConfig;
+}
+
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserProfile_00024Companion_newInstance___3B(
+ JNIEnv* env,
+ jobject,
+ jbyteArray secretKey) {
+ std::lock_guard lock{util::util_mutex_};
+ auto* profile = new session::config::UserProfile(util::ustring_from_bytes(env, secretKey), std::nullopt);
+
+ jclass userClass = env->FindClass("network/loki/messenger/libsession_util/UserProfile");
+ jmethodID constructor = env->GetMethodID(userClass, "", "(J)V");
+ jobject newConfig = env->NewObject(userClass, constructor, reinterpret_cast(profile));
+
+ return newConfig;
+}
+#pragma clang diagnostic pop
+
+JNIEXPORT void JNICALL
+Java_network_loki_messenger_libsession_1util_UserProfile_setName(
+ JNIEnv* env,
+ jobject thiz,
+ jstring newName) {
+ std::lock_guard lock{util::util_mutex_};
+ auto profile = ptrToProfile(env, thiz);
+ auto name_chars = env->GetStringUTFChars(newName, nullptr);
+ profile->set_name(name_chars);
+ env->ReleaseStringUTFChars(newName, name_chars);
+}
+
+JNIEXPORT jstring JNICALL
+Java_network_loki_messenger_libsession_1util_UserProfile_getName(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto profile = ptrToProfile(env, thiz);
+ auto name = profile->get_name();
+ if (name == std::nullopt) return nullptr;
+ jstring returnString = env->NewStringUTF(name->data());
+ return returnString;
+}
+
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_UserProfile_getPic(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto profile = ptrToProfile(env, thiz);
+ auto pic = profile->get_profile_pic();
+
+ jobject returnObject = util::serialize_user_pic(env, pic);
+
+ return returnObject;
+}
+
+JNIEXPORT void JNICALL
+Java_network_loki_messenger_libsession_1util_UserProfile_setPic(JNIEnv *env, jobject thiz,
+ jobject user_pic) {
+ std::lock_guard lock{util::util_mutex_};
+ auto profile = ptrToProfile(env, thiz);
+ auto pic = util::deserialize_user_pic(env, user_pic);
+ auto url = env->GetStringUTFChars(pic.first, nullptr);
+ auto key = util::ustring_from_bytes(env, pic.second);
+ profile->set_profile_pic(url, key);
+ env->ReleaseStringUTFChars(pic.first, url);
+}
+
+}
+extern "C"
+JNIEXPORT void JNICALL
+Java_network_loki_messenger_libsession_1util_UserProfile_setNtsPriority(JNIEnv *env, jobject thiz,
+ jint priority) {
+ std::lock_guard lock{util::util_mutex_};
+ auto profile = ptrToProfile(env, thiz);
+ profile->set_nts_priority(priority);
+}
+extern "C"
+JNIEXPORT jint JNICALL
+Java_network_loki_messenger_libsession_1util_UserProfile_getNtsPriority(JNIEnv *env, jobject thiz) {
+ std::lock_guard lock{util::util_mutex_};
+ auto profile = ptrToProfile(env, thiz);
+ return profile->get_nts_priority();
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/user_profile.h b/libsession-util/src/main/cpp/user_profile.h
new file mode 100644
index 0000000000..cb1b8d973b
--- /dev/null
+++ b/libsession-util/src/main/cpp/user_profile.h
@@ -0,0 +1,14 @@
+#ifndef SESSION_ANDROID_USER_PROFILE_H
+#define SESSION_ANDROID_USER_PROFILE_H
+
+#include "session/config/user_profile.hpp"
+#include
+#include
+
+inline session::config::UserProfile* ptrToProfile(JNIEnv* env, jobject obj) {
+ jclass configClass = env->FindClass("network/loki/messenger/libsession_util/UserProfile");
+ jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J");
+ return (session::config::UserProfile*) env->GetLongField(obj, pointerField);
+}
+
+#endif
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/util.cpp b/libsession-util/src/main/cpp/util.cpp
new file mode 100644
index 0000000000..69469eac1e
--- /dev/null
+++ b/libsession-util/src/main/cpp/util.cpp
@@ -0,0 +1,167 @@
+#include "util.h"
+#include
+#include
+
+namespace util {
+
+ std::mutex util_mutex_ = std::mutex();
+
+ jbyteArray bytes_from_ustring(JNIEnv* env, session::ustring_view from_str) {
+ size_t length = from_str.length();
+ auto jlength = (jsize)length;
+ jbyteArray new_array = env->NewByteArray(jlength);
+ env->SetByteArrayRegion(new_array, 0, jlength, (jbyte*)from_str.data());
+ return new_array;
+ }
+
+ session::ustring ustring_from_bytes(JNIEnv* env, jbyteArray byteArray) {
+ size_t len = env->GetArrayLength(byteArray);
+ auto bytes = env->GetByteArrayElements(byteArray, nullptr);
+
+ session::ustring st{reinterpret_cast(bytes), len};
+ env->ReleaseByteArrayElements(byteArray, bytes, 0);
+ return st;
+ }
+
+ jobject serialize_user_pic(JNIEnv *env, session::config::profile_pic pic) {
+ jclass returnObjectClass = env->FindClass("network/loki/messenger/libsession_util/util/UserPic");
+ jmethodID constructor = env->GetMethodID(returnObjectClass, "", "(Ljava/lang/String;[B)V");
+ jstring url = env->NewStringUTF(pic.url.data());
+ jbyteArray byteArray = util::bytes_from_ustring(env, pic.key);
+ return env->NewObject(returnObjectClass, constructor, url, byteArray);
+ }
+
+ std::pair deserialize_user_pic(JNIEnv *env, jobject user_pic) {
+ jclass userPicClass = env->FindClass("network/loki/messenger/libsession_util/util/UserPic");
+ jfieldID picField = env->GetFieldID(userPicClass, "url", "Ljava/lang/String;");
+ jfieldID keyField = env->GetFieldID(userPicClass, "key", "[B");
+ auto pic = (jstring)env->GetObjectField(user_pic, picField);
+ auto key = (jbyteArray)env->GetObjectField(user_pic, keyField);
+ return {pic, key};
+ }
+
+ jobject serialize_base_community(JNIEnv *env, const session::config::community& community) {
+ jclass base_community_clazz = env->FindClass("network/loki/messenger/libsession_util/util/BaseCommunityInfo");
+ jmethodID base_community_constructor = env->GetMethodID(base_community_clazz, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
+ auto base_url = env->NewStringUTF(community.base_url().data());
+ auto room = env->NewStringUTF(community.room().data());
+ auto pubkey_jstring = env->NewStringUTF(community.pubkey_hex().data());
+ jobject ret = env->NewObject(base_community_clazz, base_community_constructor, base_url, room, pubkey_jstring);
+ return ret;
+ }
+
+ session::config::community deserialize_base_community(JNIEnv *env, jobject base_community) {
+ jclass base_community_clazz = env->FindClass("network/loki/messenger/libsession_util/util/BaseCommunityInfo");
+ jfieldID base_url_field = env->GetFieldID(base_community_clazz, "baseUrl", "Ljava/lang/String;");
+ jfieldID room_field = env->GetFieldID(base_community_clazz, "room", "Ljava/lang/String;");
+ jfieldID pubkey_hex_field = env->GetFieldID(base_community_clazz, "pubKeyHex", "Ljava/lang/String;");
+ auto base_url = (jstring)env->GetObjectField(base_community,base_url_field);
+ auto room = (jstring)env->GetObjectField(base_community, room_field);
+ auto pub_key_hex = (jstring)env->GetObjectField(base_community, pubkey_hex_field);
+ auto base_url_chars = env->GetStringUTFChars(base_url, nullptr);
+ auto room_chars = env->GetStringUTFChars(room, nullptr);
+ auto pub_key_hex_chars = env->GetStringUTFChars(pub_key_hex, nullptr);
+
+ auto community = session::config::community(base_url_chars, room_chars, pub_key_hex_chars);
+
+ env->ReleaseStringUTFChars(base_url, base_url_chars);
+ env->ReleaseStringUTFChars(room, room_chars);
+ env->ReleaseStringUTFChars(pub_key_hex, pub_key_hex_chars);
+ return community;
+ }
+
+ jobject serialize_expiry(JNIEnv *env, const session::config::expiration_mode& mode, const std::chrono::seconds& time_seconds) {
+ jclass none = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$NONE");
+ jfieldID none_instance = env->GetStaticFieldID(none, "INSTANCE", "Lnetwork/loki/messenger/libsession_util/util/ExpiryMode$NONE;");
+ jclass after_send = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterSend");
+ jmethodID send_init = env->GetMethodID(after_send, "", "(J)V");
+ jclass after_read = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterRead");
+ jmethodID read_init = env->GetMethodID(after_read, "", "(J)V");
+
+ if (mode == session::config::expiration_mode::none) {
+ return env->GetStaticObjectField(none, none_instance);
+ } else if (mode == session::config::expiration_mode::after_send) {
+ return env->NewObject(after_send, send_init, time_seconds.count());
+ } else if (mode == session::config::expiration_mode::after_read) {
+ return env->NewObject(after_read, read_init, time_seconds.count());
+ }
+ return nullptr;
+ }
+
+ std::pair deserialize_expiry(JNIEnv *env, jobject expiry_mode) {
+ jclass parent = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode");
+ jclass after_read = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterRead");
+ jclass after_send = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterSend");
+ jfieldID duration_seconds = env->GetFieldID(parent, "expirySeconds", "J");
+
+ jclass object_class = env->GetObjectClass(expiry_mode);
+
+ if (object_class == after_read) {
+ return std::pair(session::config::expiration_mode::after_read, env->GetLongField(expiry_mode, duration_seconds));
+ } else if (object_class == after_send) {
+ return std::pair(session::config::expiration_mode::after_send, env->GetLongField(expiry_mode, duration_seconds));
+ }
+ return std::pair(session::config::expiration_mode::none, 0);
+ }
+
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_util_Sodium_ed25519KeyPair(JNIEnv *env, jobject thiz, jbyteArray seed) {
+ std::array ed_pk; // NOLINT(cppcoreguidelines-pro-type-member-init)
+ std::array ed_sk; // NOLINT(cppcoreguidelines-pro-type-member-init)
+ auto seed_bytes = util::ustring_from_bytes(env, seed);
+ crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), seed_bytes.data());
+
+ jclass kp_class = env->FindClass("network/loki/messenger/libsession_util/util/KeyPair");
+ jmethodID kp_constructor = env->GetMethodID(kp_class, "", "([B[B)V");
+
+ jbyteArray pk_jarray = util::bytes_from_ustring(env, session::ustring_view {ed_pk.data(), ed_pk.size()});
+ jbyteArray sk_jarray = util::bytes_from_ustring(env, session::ustring_view {ed_sk.data(), ed_sk.size()});
+
+ jobject return_obj = env->NewObject(kp_class, kp_constructor, pk_jarray, sk_jarray);
+ return return_obj;
+}
+extern "C"
+JNIEXPORT jbyteArray JNICALL
+Java_network_loki_messenger_libsession_1util_util_Sodium_ed25519PkToCurve25519(JNIEnv *env,
+ jobject thiz,
+ jbyteArray pk) {
+ auto ed_pk = util::ustring_from_bytes(env, pk);
+ std::array curve_pk; // NOLINT(cppcoreguidelines-pro-type-member-init)
+ int success = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data());
+ if (success != 0) {
+ jclass exception = env->FindClass("java/lang/Exception");
+ env->ThrowNew(exception, "Invalid crypto_sign_ed25519_pk_to_curve25519 operation");
+ return nullptr;
+ }
+ jbyteArray curve_pk_jarray = util::bytes_from_ustring(env, session::ustring_view {curve_pk.data(), curve_pk.size()});
+ return curve_pk_jarray;
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_util_BaseCommunityInfo_00024Companion_parseFullUrl(
+ JNIEnv *env, jobject thiz, jstring full_url) {
+ auto bytes = env->GetStringUTFChars(full_url, nullptr);
+ auto [base, room, pk] = session::config::community::parse_full_url(bytes);
+ env->ReleaseStringUTFChars(full_url, bytes);
+
+ jclass clazz = env->FindClass("kotlin/Triple");
+ jmethodID constructor = env->GetMethodID(clazz, "", "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V");
+
+ auto base_j = env->NewStringUTF(base.data());
+ auto room_j = env->NewStringUTF(room.data());
+ auto pk_jbytes = util::bytes_from_ustring(env, pk);
+
+ jobject triple = env->NewObject(clazz, constructor, base_j, room_j, pk_jbytes);
+ return triple;
+}
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_network_loki_messenger_libsession_1util_util_BaseCommunityInfo_fullUrl(JNIEnv *env,
+ jobject thiz) {
+ auto deserialized = util::deserialize_base_community(env, thiz);
+ auto full_url = deserialized.full_url();
+ return env->NewStringUTF(full_url.data());
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/util.h b/libsession-util/src/main/cpp/util.h
new file mode 100644
index 0000000000..9348e8bd7e
--- /dev/null
+++ b/libsession-util/src/main/cpp/util.h
@@ -0,0 +1,24 @@
+#ifndef SESSION_ANDROID_UTIL_H
+#define SESSION_ANDROID_UTIL_H
+
+#include
+#include
+#include
+#include "session/types.hpp"
+#include "session/config/profile_pic.hpp"
+#include "session/config/user_groups.hpp"
+#include "session/config/expiring.hpp"
+
+namespace util {
+ extern std::mutex util_mutex_;
+ jbyteArray bytes_from_ustring(JNIEnv* env, session::ustring_view from_str);
+ session::ustring ustring_from_bytes(JNIEnv* env, jbyteArray byteArray);
+ jobject serialize_user_pic(JNIEnv *env, session::config::profile_pic pic);
+ std::pair deserialize_user_pic(JNIEnv *env, jobject user_pic);
+ jobject serialize_base_community(JNIEnv *env, const session::config::community& base_community);
+ session::config::community deserialize_base_community(JNIEnv *env, jobject base_community);
+ jobject serialize_expiry(JNIEnv *env, const session::config::expiration_mode& mode, const std::chrono::seconds& time_seconds);
+ std::pair deserialize_expiry(JNIEnv *env, jobject expiry_mode);
+}
+
+#endif
\ No newline at end of file
diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt
new file mode 100644
index 0000000000..52fb541d7d
--- /dev/null
+++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt
@@ -0,0 +1,200 @@
+package network.loki.messenger.libsession_util
+
+import network.loki.messenger.libsession_util.util.BaseCommunityInfo
+import network.loki.messenger.libsession_util.util.ConfigPush
+import network.loki.messenger.libsession_util.util.Contact
+import network.loki.messenger.libsession_util.util.Conversation
+import network.loki.messenger.libsession_util.util.GroupInfo
+import network.loki.messenger.libsession_util.util.UserPic
+import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind
+import org.session.libsignal.utilities.IdPrefix
+import org.session.libsignal.utilities.Log
+
+
+sealed class ConfigBase(protected val /* yucky */ pointer: Long) {
+ companion object {
+ init {
+ System.loadLibrary("session_util")
+ }
+ external fun kindFor(configNamespace: Int): Class
+
+ fun ConfigBase.protoKindFor(): Kind = when (this) {
+ is UserProfile -> Kind.USER_PROFILE
+ is Contacts -> Kind.CONTACTS
+ is ConversationVolatileConfig -> Kind.CONVO_INFO_VOLATILE
+ is UserGroupsConfig -> Kind.GROUPS
+ }
+
+ // TODO: time in future to activate (hardcoded to 1st jan 2024 for testing, change before release)
+ private const val ACTIVATE_TIME = 1690761600000
+
+ fun isNewConfigEnabled(forced: Boolean, currentTime: Long) =
+ forced || currentTime >= ACTIVATE_TIME
+
+ const val PRIORITY_HIDDEN = -1
+ const val PRIORITY_VISIBLE = 0
+ const val PRIORITY_PINNED = 1
+
+ }
+
+ external fun dirty(): Boolean
+ external fun needsPush(): Boolean
+ external fun needsDump(): Boolean
+ external fun push(): ConfigPush
+ external fun dump(): ByteArray
+ external fun encryptionDomain(): String
+ external fun confirmPushed(seqNo: Long, newHash: String)
+ external fun merge(toMerge: Array>): Int
+ external fun currentHashes(): List
+
+ external fun configNamespace(): Int
+
+ // Singular merge
+ external fun merge(toMerge: Pair