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..fc710a8e00 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -93,17 +93,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 +112,52 @@ 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'
}
-def canonicalVersionCode = 338
-def canonicalVersionName = "1.16.9"
+def canonicalVersionCode = 354
+def canonicalVersionName = "1.17.0"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
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 027dba87a5..e33b321f35 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.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..008a45cfd1 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;
/**
@@ -416,7 +417,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 +434,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
})
.execute();
+ return Unit.INSTANCE;
});
}
@@ -449,29 +451,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
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..ded6b4f140 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt
@@ -260,6 +260,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
CALL_CONNECTED -> {
wantsToAnswer = false
}
+ else -> { /* do nothing */ }
}
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 a827a7d260..0101677d89 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
@@ -32,10 +32,10 @@ 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
@@ -52,12 +52,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
@@ -108,30 +115,36 @@ class ProfilePictureView @JvmOverloads constructor(
val signalProfilePicture = recipient.contactPhoto
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
+ val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
+
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
glide.clear(imageView)
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) {
glide.clear(imageView)
- imageView.setImageDrawable(unknownOpenGroupDrawable)
+ glide.load(unknownOpenGroupDrawable)
+ .centerCrop()
+ .circleCrop()
+ .into(imageView)
} else {
- val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
-
glide.clear(imageView)
glide.load(placeholder)
.placeholder(unknownRecipientDrawable)
.centerCrop()
+ .circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
}
profilePicturesCache[publicKey] = recipient.profileAvatar
} 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..72c9749f9b 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,12 +48,12 @@ 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)
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 2c3e8203b3..054550735a 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
@@ -8,6 +8,10 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
+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
@@ -19,7 +23,10 @@ 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.ActionMode
@@ -28,14 +35,26 @@ import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
+import android.widget.LinearLayout
+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.RelativeLayout
import android.widget.Toast
import androidx.activity.viewModels
-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
@@ -43,6 +62,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
@@ -114,6 +138,10 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
+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.GroupDatabase
@@ -126,6 +154,18 @@ 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.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
@@ -157,9 +197,20 @@ import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.toPx
+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.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
@@ -226,11 +277,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
@@ -251,6 +302,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
@@ -270,11 +322,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)
},
@@ -316,6 +374,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
@@ -360,28 +419,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()
@@ -391,13 +453,17 @@ 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()
@@ -406,6 +472,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
setUpSearchResultObserver()
scrollToFirstUnreadMessageIfNeeded()
setUpOutdatedClientBanner()
+
+ if (author != null && messageTimestamp >= 0 && targetPosition >= 0) {
+ binding?.conversationRecyclerView?.scrollToPosition(targetPosition)
+ }
+ else {
+ scrollToFirstUnreadMessageIfNeeded(true)
+ }
}
}
@@ -413,16 +486,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,
@@ -450,23 +532,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) {
@@ -476,7 +580,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)
@@ -485,6 +589,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 { _, _, _, _, _, _, _, _, _ ->
@@ -610,7 +718,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 setUpOutdatedClientBanner() {
@@ -655,15 +763,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 {
@@ -746,11 +876,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)
}
@@ -948,17 +1075,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
}
@@ -990,21 +1160,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) {
@@ -1037,15 +1204,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
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
@@ -1389,11 +1556,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) {
@@ -1411,19 +1584,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 expiresInMillis = (viewModel.expirationConfiguration?.durationSeconds ?: 0) * 1000L
val expireStartedAt = if (viewModel.expirationConfiguration?.expirationType == ExpirationType.DELETE_AFTER_SEND) {
@@ -1444,14 +1619,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()
@@ -1489,28 +1666,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)
}
@@ -1657,35 +1834,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()
}
@@ -1696,54 +1861,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) {
@@ -1807,7 +1950,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
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)
@@ -1835,7 +1978,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
Toast.LENGTH_LONG).show()
}
.execute()
- })
+ }
}
override fun reply(messages: Set) {
@@ -1893,7 +2036,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() }
}
}
@@ -1930,15 +2073,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/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
index a87d3b2067..91a0a446be 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
@@ -22,15 +22,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() {
@@ -38,7 +39,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 {
@@ -65,6 +66,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)
@@ -85,27 +98,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)
- }
}
}
@@ -202,19 +205,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
}
}
}
@@ -223,7 +227,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/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/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
index 68228d592a..0a5eb829a7 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.os.AsyncTask
@@ -10,7 +9,7 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
+import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.widget.SearchView
@@ -27,7 +26,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
@@ -38,6 +36,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
@@ -66,7 +66,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)
}
}
@@ -164,29 +164,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)
}
@@ -273,9 +267,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)
@@ -285,29 +277,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) {
@@ -322,7 +310,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..e7214cfa7a 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
@@ -39,9 +37,10 @@ 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.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 {
@@ -69,12 +68,10 @@ class VisibleMessageContentView : ConstraintLayout {
onAttachmentNeedsDownload: (Long, Long) -> Unit
) {
// 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 +128,6 @@ class VisibleMessageContentView : ConstraintLayout {
delegate?.scrollToMessageIfPossible(quote.id)
}
}
- val hasMedia = message.slideDeck.asAttachments().isNotEmpty()
}
if (message is MmsMessageRecord) {
@@ -244,11 +240,6 @@ class VisibleMessageContentView : ConstraintLayout {
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 +257,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..00c5c08a4d 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
@@ -111,6 +111,8 @@ class VisibleMessageView : LinearLayout {
private fun initialize() {
isHapticFeedbackEnabled = true
setWillNotDraw(false)
+ binding.root.disableClipping()
+ binding.mainContainer.disableClipping()
binding.messageInnerContainer.disableClipping()
binding.messageContentView.root.disableClipping()
}
@@ -125,6 +127,7 @@ class VisibleMessageView : LinearLayout {
searchQuery: String?,
contact: Contact?,
senderSessionID: String,
+ lastSeen: Long,
delegate: VisibleMessageViewDelegate?,
onAttachmentNeedsDownload: (Long, Long) -> Unit
) {
@@ -164,6 +167,7 @@ class VisibleMessageView : LinearLayout {
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 +181,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 +195,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
@@ -409,6 +415,10 @@ class VisibleMessageView : LinearLayout {
binding.profilePictureView.root.recycle()
binding.messageContentView.root.recycle()
}
+
+ fun playHighlight() {
+ binding.messageContentView.root.playHighlight()
+ }
// endregion
// region Interaction
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 b13a5e1ce2..4805cbb690 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
@@ -21,13 +21,11 @@ import android.content.Context
import android.database.Cursor
import android.provider.ContactsContract.CommonDataKinds.BaseTypes
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
@@ -42,16 +40,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
@@ -163,7 +158,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)
}
}
@@ -206,25 +201,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(
@@ -266,7 +242,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)
}
}
@@ -323,10 +299,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)
}
@@ -350,6 +323,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)",
@@ -576,18 +556,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!")
deleteExpirationTimerMessages(threadId)
val contentValues = ContentValues()
contentValues.put(DATE_SENT, retrieved.sentTimeMillis)
@@ -642,12 +613,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)
@@ -661,27 +628,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!")
deleteExpirationTimerMessages(threadId)
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
if (messageId == -1L) {
@@ -697,7 +644,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
@@ -716,7 +662,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
@@ -806,10 +752,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
@@ -944,7 +893,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()
@@ -961,7 +910,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()
@@ -1159,7 +1108,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 fe076916c2..696fbd4a8d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
@@ -63,13 +63,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, DISAPPEARING_STATE
+ FORCE_SMS_SELECTION, NOTIFY_TYPE, DISAPPEARING_STATE, WRAPPER_HASH
};
static final List TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
@@ -142,6 +143,11 @@ public class RecipientDatabase extends Database {
"ADD COLUMN " + DISAPPEARING_STATE + " INTEGER DEFAULT 0;";
}
+ 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;
@@ -160,18 +166,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();
}
}
@@ -201,6 +203,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;
@@ -233,7 +236,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) {
@@ -260,6 +263,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);
@@ -276,14 +297,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 dc458d0f35..3a9eeb493e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -149,7 +149,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);
}
@@ -235,10 +235,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);
}
@@ -257,7 +253,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);
}
@@ -320,7 +316,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;
}
@@ -338,6 +334,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)});
}
@@ -399,14 +398,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) {
Recipient recipient = Recipient.from(context, message.getSender(), true);
Recipient groupRecipient;
@@ -475,12 +474,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) {
@@ -512,16 +507,16 @@ public class SmsDatabase extends MessagingDatabase {
return typeMask;
}
- 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) {
@@ -576,9 +571,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);
@@ -630,7 +628,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;
}
@@ -654,7 +652,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 c1a1d4c1ef..6460d0d8ee 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
@@ -2,6 +2,18 @@ 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
@@ -14,6 +26,16 @@ 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.messages.ExpirationConfiguration
+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
@@ -35,11 +57,14 @@ 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.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupRecord
@@ -48,25 +73,105 @@ 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.protos.SignalServiceProtos.Content.ExpirationType
+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)
}
@@ -87,6 +192,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) {
@@ -107,19 +231,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,
@@ -128,7 +289,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!!)
@@ -155,13 +315,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)
+ }
val expirationConfig = getExpirationConfiguration(message.threadID ?: -1)
val expiresInMillis = (expirationConfig?.durationSeconds ?: 0) * 1000L
val expireStartedAt = if (expirationConfig?.expirationType == ExpirationType.DELETE_AFTER_SEND) message.sentTimestamp!! else 0
@@ -186,7 +349,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
it.toSignalPointer()
}
val mediaMessage = IncomingMediaMessage.from(message, senderAddress, expiresInMillis, expireStartedAt, 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
@@ -203,7 +366,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, senderAddress, message.sentTimestamp, expiresInMillis, expireStartedAt)
else IncomingTextMessage.from(message, senderAddress, group, expiresInMillis, expireStartedAt)
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
@@ -252,6 +415,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)
@@ -261,11 +430,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)
@@ -501,6 +860,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
}
@@ -536,7 +948,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)
if (expirationConfig?.expirationType == ExpirationType.DELETE_AFTER_SEND) {
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(sentTimestamp, senderPublicKey, expireStartedAt)
}
@@ -593,8 +1005,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) {
@@ -611,6 +1023,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
.updateTimestampUpdated(groupID, updatedTimestamp)
}
+ 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 setExpirationTimer(groupID: String, duration: Int) {
val recipient = Recipient.from(context, fromSerialized(groupID), false)
val threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
@@ -635,16 +1062,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 {
@@ -662,17 +1102,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 }
}
}
@@ -681,6 +1123,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)
@@ -710,6 +1156,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? {
@@ -721,6 +1172,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()
@@ -744,19 +1240,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()) {
@@ -764,6 +1259,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)
@@ -784,12 +1284,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)
@@ -805,7 +1371,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val recipient = Recipient.from(context, address, false)
if (recipient.isBlocked) return
- val threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
+ val threadId = getThreadId(recipient) ?: return
val expirationConfig = getExpirationConfiguration(threadId)
val expiresInMillis = (expirationConfig?.durationSeconds ?: 0) * 100L
val expireStartedAt = if (expirationConfig?.expirationType == ExpirationType.DELETE_AFTER_SEND) sentTimestamp else 0
@@ -828,7 +1394,7 @@ 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)
if (expirationConfig?.expirationType == ExpirationType.DELETE_AFTER_SEND) {
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(sentTimestamp, senderPublicKey, expireStartedAt)
}
@@ -838,7 +1404,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
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) {
@@ -850,7 +1423,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
@@ -865,9 +1438,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)
@@ -929,16 +1501,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) {
@@ -1076,9 +1660,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 70e551b5ea..3a280c44c5 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.ExpirationConfigurationDatabase;
@@ -40,6 +40,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;
@@ -86,9 +87,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";
@@ -148,7 +151,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); }
@@ -342,6 +345,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);
db.execSQL(ExpirationConfigurationDatabase.CREATE_EXPIRATION_CONFIGURATION_TABLE_COMMAND);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
@@ -354,6 +358,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
executeStatements(db, ReactionDatabase.CREATE_INDEXS);
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
+ db.execSQL(RecipientDatabase.getAddWrapperHash());
}
@Override
@@ -587,6 +592,16 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
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());
+ }
+
+ if (oldVersion < lokiV43) {
db.execSQL(RecipientDatabase.getCreateDisappearingStateCommand());
db.execSQL(ExpirationConfigurationDatabase.CREATE_EXPIRATION_CONFIGURATION_TABLE_COMMAND);
db.execSQL(ExpirationConfigurationDatabase.MIGRATE_GROUP_CONVERSATION_EXPIRY_TYPE_COMMAND);
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 a4b98b6852..30eb596183 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt
@@ -46,4 +46,5 @@ interface DatabaseComponent {
fun blindedIdMappingDatabase(): BlindedIdMappingDatabase
fun groupMemberDatabase(): GroupMemberDatabase
fun expirationConfigurationDatabase(): ExpirationConfigurationDatabase
+ 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 3a6357fa52..30fb40d89a 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.session.libsession.utilities.SSKEnvironment
import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider
@@ -142,10 +141,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..8d5acb244d 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
@@ -71,7 +78,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 +87,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)
@@ -128,7 +136,7 @@ class ConversationView : LinearLayout {
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..e78b4388f5 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 ->
@@ -158,15 +175,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
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 +195,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 +216,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 +245,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
}
}
+
// monitor the global search VM query
launch {
binding.globalSearchInputLayout.query
@@ -264,6 +298,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,6 +354,11 @@ 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)
@@ -322,6 +369,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
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)
@@ -488,39 +540,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 +582,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 +604,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 +628,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 +676,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..b37bd886c5 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
@@ -61,6 +59,7 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
profilePictureView.root.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 +86,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 +135,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/GlobalSearchAdapterUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt
index 2c64ded866..54f9290ffb 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)
}
}
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/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/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 55155b9819..92e0d3c0e0 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
@@ -16,11 +19,10 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi
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.dependencies.DatabaseComponent
+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 {
@@ -37,7 +39,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))
@@ -64,8 +70,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..f3cdc269c3 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() }
@@ -76,7 +89,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
glide = GlideApp.with(this)
with(binding) {
setupProfilePictureView(profilePictureView.root)
- profilePictureView.root.setOnClickListener { showEditProfilePictureUI() }
+ profilePictureView.root.setOnClickListener {
+ showEditProfilePictureUI()
+ }
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
btnGroupNameDisplay.text = displayName
publicKeyTextView.text = hexEncodedPublicKey
@@ -204,27 +219,36 @@ 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) {
@@ -264,19 +288,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/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
index cc8caee765..ae88ceb3fb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
@@ -25,9 +25,11 @@ import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.SessionJobDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
+import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -64,7 +66,7 @@ interface ConversationRepository {
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf
- suspend fun clearAllMessageRequests(): ResultOf
+ suspend fun clearAllMessageRequests(block: Boolean): ResultOf
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf
@@ -84,9 +86,11 @@ 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 configDb: ExpirationConfigurationDatabase
+ private val configFactory: ConfigFactory
) : ConversationRepository {
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
@@ -133,8 +137,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) {
@@ -147,7 +152,7 @@ class DefaultConversationRepository @Inject constructor(
}
override fun setApproved(recipient: Recipient, isApproved: Boolean) {
- recipientDb.setApproved(recipient, isApproved)
+ storage.setRecipientApproved(recipient, isApproved)
}
override suspend fun deleteForEveryone(
@@ -258,29 +263,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))
@@ -291,7 +300,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 5a7610d1d0..c9f8c1e034 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java
@@ -4,6 +4,8 @@ import android.content.Context;
import org.jetbrains.annotations.NotNull;
import org.session.libsession.messaging.messages.ExpirationConfiguration;
+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;
@@ -17,6 +19,7 @@ import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType;
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;
@@ -37,12 +40,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());
@@ -87,7 +92,6 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
}
private void insertIncomingExpirationTimerMessage(ExpirationTimerUpdate message, long expireStartedAt) {
- MmsDatabase database = DatabaseComponent.get(context).mmsDatabase();
String senderPublicKey = message.getSender();
Long sentTimestamp = message.getSentTimestamp();
@@ -109,6 +113,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,
expiresInMillis, expireStartedAt, true,
@@ -123,33 +131,52 @@ 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);
+
} catch (IOException | MmsException ioe) {
Log.e("Loki", "Failed to insert expiration update message.");
}
}
private void insertOutgoingExpirationTimerMessage(ExpirationTimerUpdate message, long expireStartedAt) {
- 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;
+ // DISAPPEARING MESSAGES
try {
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, expireStartedAt, groupId);
database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp, true);
} catch (MmsException e) {
Log.e("Loki", "Failed to insert expiration update message.");
}
+ // SHARED CONFIG
+ try {
+ if (groupId != null) {
+ address = Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId));
+ } else {
+ address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : message.getRecipient());
+ }
+
+ 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.", ioe);
+ }
}
@Override
public void startAnyExpiration(long timestamp, @NotNull String author, long expireStartedAt) {
- MessageRecord messageRecord = DatabaseComponent.get(context).mmsSmsDatabase().getMessageFor(timestamp, author);
+ MessageRecord messageRecord = mmsSmsDatabase.getMessageFor(timestamp, author);
if (messageRecord != null) {
boolean mms = messageRecord.isMms();
ExpirationConfiguration config = DatabaseComponent.get(context).expirationConfigurationDatabase().getExpirationConfiguration(messageRecord.getThreadId());
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/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/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml
index 4e1c5853e2..000b860841 100644
--- a/app/src/main/res/layout/activity_conversation_v2.xml
+++ b/app/src/main/res/layout/activity_conversation_v2.xml
@@ -241,6 +241,19 @@
+
+
-
+ 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" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_download.xml b/app/src/main/res/layout/dialog_download.xml
deleted file mode 100644
index a54a0357c2..0000000000
--- a/app/src/main/res/layout/dialog_download.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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_user_details_bottom_sheet.xml b/app/src/main/res/layout/fragment_user_details_bottom_sheet.xml
index 98c87331a6..eb04dbb274 100644
--- a/app/src/main/res/layout/fragment_user_details_bottom_sheet.xml
+++ b/app/src/main/res/layout/fragment_user_details_bottom_sheet.xml
@@ -52,6 +52,7 @@
android:text="Spiderman" />
diff --git a/app/src/main/res/layout/view_visible_message.xml b/app/src/main/res/layout/view_visible_message.xml
index d328d19ab3..11155ccc20 100644
--- a/app/src/main/res/layout/view_visible_message.xml
+++ b/app/src/main/res/layout/view_visible_message.xml
@@ -8,6 +8,46 @@
android:layout_height="wrap_content"
android:orientation="vertical">
+
+
+
+
+
+
-
-
+
@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 4fbe80dbe7..cdb63ac0c6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1054,4 +1054,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 f876a75a19..9fcc7441d4 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -75,6 +75,7 @@
+
+
+
+
diff --git a/app/src/main/res/xml/preferences_app_protection.xml b/app/src/main/res/xml/preferences_app_protection.xml
index 6119719930..12607ee769 100644
--- a/app/src/main/res/xml/preferences_app_protection.xml
+++ b/app/src/main/res/xml/preferences_app_protection.xml
@@ -12,7 +12,6 @@
android:title="@string/preferences_app_protection__screen_lock"
android:summary="@string/preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint" />
-
+ 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): Int
+
+ external fun free()
+
+}
+
+class Contacts(pointer: Long) : ConfigBase(pointer) {
+ companion object {
+ init {
+ System.loadLibrary("session_util")
+ }
+ external fun newInstance(ed25519SecretKey: ByteArray): Contacts
+ external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): Contacts
+ }
+
+ external fun get(sessionId: String): Contact?
+ external fun getOrConstruct(sessionId: String): Contact
+ external fun all(): List
+ external fun set(contact: Contact)
+ external fun erase(sessionId: String): Boolean
+
+ /**
+ * Similar to [updateIfExists], but will create the underlying contact if it doesn't exist before passing to [updateFunction]
+ */
+ fun upsertContact(sessionId: String, updateFunction: Contact.()->Unit = {}) {
+ if (sessionId.startsWith(IdPrefix.BLINDED.value)) {
+ Log.w("Loki", "Trying to create a contact with a blinded ID prefix")
+ return
+ } else if (sessionId.startsWith(IdPrefix.UN_BLINDED.value)) {
+ Log.w("Loki", "Trying to create a contact with an un-blinded ID prefix")
+ return
+ } else if (sessionId.startsWith(IdPrefix.BLINDEDV2.value)) {
+ Log.w("Loki", "Trying to create a contact with a blindedv2 ID prefix")
+ return
+ }
+ val contact = getOrConstruct(sessionId)
+ updateFunction(contact)
+ set(contact)
+ }
+
+ /**
+ * Updates the contact by sessionId with a given [updateFunction], and applies to the underlying config.
+ * the [updateFunction] doesn't run if there is no contact
+ */
+ fun updateIfExists(sessionId: String, updateFunction: Contact.()->Unit) {
+ if (sessionId.startsWith(IdPrefix.BLINDED.value)) {
+ Log.w("Loki", "Trying to create a contact with a blinded ID prefix")
+ return
+ } else if (sessionId.startsWith(IdPrefix.UN_BLINDED.value)) {
+ Log.w("Loki", "Trying to create a contact with an un-blinded ID prefix")
+ return
+ } else if (sessionId.startsWith(IdPrefix.BLINDEDV2.value)) {
+ Log.w("Loki", "Trying to create a contact with a blindedv2 ID prefix")
+ return
+ }
+ val contact = get(sessionId) ?: return
+ updateFunction(contact)
+ set(contact)
+ }
+}
+
+class UserProfile(pointer: Long) : ConfigBase(pointer) {
+ companion object {
+ init {
+ System.loadLibrary("session_util")
+ }
+ external fun newInstance(ed25519SecretKey: ByteArray): UserProfile
+ external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): UserProfile
+ }
+
+ external fun setName(newName: String)
+ external fun getName(): String?
+ external fun getPic(): UserPic
+ external fun setPic(userPic: UserPic)
+ external fun setNtsPriority(priority: Int)
+ external fun getNtsPriority(): Int
+}
+
+class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) {
+ companion object {
+ init {
+ System.loadLibrary("session_util")
+ }
+ external fun newInstance(ed25519SecretKey: ByteArray): ConversationVolatileConfig
+ external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): ConversationVolatileConfig
+ }
+
+ external fun getOneToOne(pubKeyHex: String): Conversation.OneToOne?
+ external fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne
+ external fun eraseOneToOne(pubKeyHex: String): Boolean
+
+ external fun getCommunity(baseUrl: String, room: String): Conversation.Community?
+ external fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community
+ external fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community
+ external fun eraseCommunity(community: Conversation.Community): Boolean
+ external fun eraseCommunity(baseUrl: String, room: String): Boolean
+
+ external fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup?
+ external fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup
+ external fun eraseLegacyClosedGroup(groupId: String): Boolean
+ external fun erase(conversation: Conversation): Boolean
+
+ external fun set(toStore: Conversation)
+
+ /**
+ * Erase all conversations that do not satisfy the `predicate`, similar to [MutableList.removeAll]
+ */
+ external fun eraseAll(predicate: (Conversation) -> Boolean): Int
+
+ external fun sizeOneToOnes(): Int
+ external fun sizeCommunities(): Int
+ external fun sizeLegacyClosedGroups(): Int
+ external fun size(): Int
+
+ external fun empty(): Boolean
+
+ external fun allOneToOnes(): List
+ external fun allCommunities(): List
+ external fun allLegacyClosedGroups(): List
+ external fun all(): List
+
+}
+
+class UserGroupsConfig(pointer: Long): ConfigBase(pointer) {
+ companion object {
+ init {
+ System.loadLibrary("session_util")
+ }
+ external fun newInstance(ed25519SecretKey: ByteArray): UserGroupsConfig
+ external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): UserGroupsConfig
+ }
+
+ external fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo?
+ external fun getLegacyGroupInfo(sessionId: String): GroupInfo.LegacyGroupInfo?
+ external fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo
+ external fun getOrConstructLegacyGroupInfo(sessionId: String): GroupInfo.LegacyGroupInfo
+ external fun set(groupInfo: GroupInfo)
+ external fun erase(communityInfo: GroupInfo)
+ external fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean
+ external fun eraseCommunity(server: String, room: String): Boolean
+ external fun eraseLegacyGroup(sessionId: String): Boolean
+ external fun sizeCommunityInfo(): Int
+ external fun sizeLegacyGroupInfo(): Int
+ external fun size(): Int
+ external fun all(): List
+ external fun allCommunityInfo(): List
+ external fun allLegacyGroupInfo(): List
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BaseCommunity.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BaseCommunity.kt
new file mode 100644
index 0000000000..a48d082a62
--- /dev/null
+++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BaseCommunity.kt
@@ -0,0 +1,11 @@
+package network.loki.messenger.libsession_util.util
+
+data class BaseCommunityInfo(val baseUrl: String, val room: String, val pubKeyHex: String) {
+ companion object {
+ init {
+ System.loadLibrary("session_util")
+ }
+ external fun parseFullUrl(fullUrl: String): Triple?
+ }
+ external fun fullUrl(): String
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Contact.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Contact.kt
new file mode 100644
index 0000000000..8cc22a6afe
--- /dev/null
+++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Contact.kt
@@ -0,0 +1,13 @@
+package network.loki.messenger.libsession_util.util
+
+data class Contact(
+ val id: String,
+ var name: String = "",
+ var nickname: String = "",
+ var approved: Boolean = false,
+ var approvedMe: Boolean = false,
+ var blocked: Boolean = false,
+ var profilePicture: UserPic = UserPic.DEFAULT,
+ var priority: Int = 0,
+ var expiryMode: ExpiryMode,
+)
\ No newline at end of file
diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt
new file mode 100644
index 0000000000..97930e8b40
--- /dev/null
+++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt
@@ -0,0 +1,25 @@
+package network.loki.messenger.libsession_util.util
+
+sealed class Conversation {
+
+ abstract var lastRead: Long
+ abstract var unread: Boolean
+
+ data class OneToOne(
+ val sessionId: String,
+ override var lastRead: Long,
+ override var unread: Boolean
+ ): Conversation()
+
+ data class Community(
+ val baseCommunityInfo: BaseCommunityInfo,
+ override var lastRead: Long,
+ override var unread: Boolean
+ ) : Conversation()
+
+ data class LegacyGroup(
+ val groupId: String,
+ override var lastRead: Long,
+ override var unread: Boolean
+ ): Conversation()
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/ExpiryMode.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/ExpiryMode.kt
new file mode 100644
index 0000000000..58e98a4392
--- /dev/null
+++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/ExpiryMode.kt
@@ -0,0 +1,7 @@
+package network.loki.messenger.libsession_util.util
+
+sealed class ExpiryMode(val expirySeconds: Long) {
+ object NONE: ExpiryMode(0)
+ class AfterSend(seconds: Long): ExpiryMode(seconds)
+ class AfterRead(seconds: Long): ExpiryMode(seconds)
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupInfo.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupInfo.kt
new file mode 100644
index 0000000000..c8ace0a9a7
--- /dev/null
+++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupInfo.kt
@@ -0,0 +1,53 @@
+package network.loki.messenger.libsession_util.util
+
+sealed class GroupInfo {
+
+ data class CommunityGroupInfo(val community: BaseCommunityInfo, val priority: Int) : GroupInfo()
+
+ data class LegacyGroupInfo(
+ val sessionId: String,
+ val name: String,
+ val members: Map,
+ val encPubKey: ByteArray,
+ val encSecKey: ByteArray,
+ val priority: Int,
+ val disappearingTimer: Long,
+ val joinedAt: Long
+ ): GroupInfo() {
+ companion object {
+ @Suppress("FunctionName")
+ external fun NAME_MAX_LENGTH(): Int
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as LegacyGroupInfo
+
+ if (sessionId != other.sessionId) return false
+ if (name != other.name) return false
+ if (members != other.members) return false
+ if (!encPubKey.contentEquals(other.encPubKey)) return false
+ if (!encSecKey.contentEquals(other.encSecKey)) return false
+ if (priority != other.priority) return false
+ if (disappearingTimer != other.disappearingTimer) return false
+ if (joinedAt != other.joinedAt) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = sessionId.hashCode()
+ result = 31 * result + name.hashCode()
+ result = 31 * result + members.hashCode()
+ result = 31 * result + encPubKey.contentHashCode()
+ result = 31 * result + encSecKey.contentHashCode()
+ result = 31 * result + priority
+ result = 31 * result + disappearingTimer.hashCode()
+ result = 31 * result + joinedAt.hashCode()
+ return result
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Sodium.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Sodium.kt
new file mode 100644
index 0000000000..6168bd2165
--- /dev/null
+++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Sodium.kt
@@ -0,0 +1,9 @@
+package network.loki.messenger.libsession_util.util
+
+object Sodium {
+ init {
+ System.loadLibrary("session_util")
+ }
+ external fun ed25519KeyPair(seed: ByteArray): KeyPair
+ external fun ed25519PkToCurve25519(pk: ByteArray): ByteArray
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Utils.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Utils.kt
new file mode 100644
index 0000000000..4222395b5d
--- /dev/null
+++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Utils.kt
@@ -0,0 +1,67 @@
+package network.loki.messenger.libsession_util.util
+
+data class ConfigPush(val config: ByteArray, val seqNo: Long, val obsoleteHashes: List) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ConfigPush
+
+ if (!config.contentEquals(other.config)) return false
+ if (seqNo != other.seqNo) return false
+ if (obsoleteHashes != other.obsoleteHashes) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = config.contentHashCode()
+ result = 31 * result + seqNo.hashCode()
+ result = 31 * result + obsoleteHashes.hashCode()
+ return result
+ }
+
+}
+
+data class UserPic(val url: String, val key: ByteArray) {
+ companion object {
+ val DEFAULT = UserPic("", byteArrayOf())
+ }
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as UserPic
+
+ if (url != other.url) return false
+ if (!key.contentEquals(other.key)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = url.hashCode()
+ result = 31 * result + key.contentHashCode()
+ return result
+ }
+}
+
+data class KeyPair(val pubKey: ByteArray, val secretKey: ByteArray) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as KeyPair
+
+ if (!pubKey.contentEquals(other.pubKey)) return false
+ if (!secretKey.contentEquals(other.secretKey)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = pubKey.contentHashCode()
+ result = 31 * result + secretKey.contentHashCode()
+ return result
+ }
+}
\ No newline at end of file
diff --git a/libsession-util/src/test/java/network/loki/messenger/libsession_util/ExampleUnitTest.kt b/libsession-util/src/test/java/network/loki/messenger/libsession_util/ExampleUnitTest.kt
new file mode 100644
index 0000000000..3d156bfd4d
--- /dev/null
+++ b/libsession-util/src/test/java/network/loki/messenger/libsession_util/ExampleUnitTest.kt
@@ -0,0 +1,14 @@
+package network.loki.messenger.libsession_util
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+
+}
\ No newline at end of file
diff --git a/libsession/build.gradle b/libsession/build.gradle
index dd8959958e..045648e090 100644
--- a/libsession/build.gradle
+++ b/libsession/build.gradle
@@ -18,6 +18,7 @@ android {
dependencies {
implementation project(":libsignal")
+ implementation project(":libsession-util")
implementation project(":liblazysodium")
implementation "net.java.dev.jna:jna:5.8.0@aar"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
@@ -34,7 +35,6 @@ dependencies {
implementation 'com.annimon:stream:1.1.8'
implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.esotericsoftware:kryo:5.1.1'
- implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
@@ -46,10 +46,6 @@ dependencies {
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation "org.mockito:mockito-inline:4.0.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'
testImplementation "androidx.test:core:$testCoreVersion"
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
diff --git a/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java b/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java
index a448b3f7a7..f78089e25e 100644
--- a/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java
+++ b/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java
@@ -33,7 +33,7 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId));
- foreground.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+ foreground.setScaleType(ImageView.ScaleType.CENTER_CROP);
if (inverted) {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
index a06438e630..0bf3eaf3e5 100644
--- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
+++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
@@ -2,12 +2,14 @@ package org.session.libsession.database
import android.content.Context
import android.net.Uri
+import network.loki.messenger.libsession_util.ConfigBase
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.AttachmentUploadJob
import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.MessageSendJob
+import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ConfigurationMessage
@@ -31,6 +33,7 @@ import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
+import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
interface StorageProtocol {
@@ -39,6 +42,9 @@ interface StorageProtocol {
fun getUserX25519KeyPair(): ECKeyPair
fun getUserProfile(): Profile
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
+ fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?)
+ fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?)
+ fun clearUserPic()
// Signal
fun getOrGenerateRegistrationID(): Int
@@ -51,8 +57,10 @@ interface StorageProtocol {
fun getMessageSendJob(messageSendJobID: String): MessageSendJob?
fun getMessageReceiveJob(messageReceiveJobID: String): Job?
fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): Job?
+ fun getConfigSyncJob(destination: Destination): Job?
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
fun isJobCanceled(job: Job): Boolean
+ fun cancelPendingMessageSendJobs(threadID: Long)
// Authorization
fun getAuthToken(room: String, server: String): String?
@@ -68,7 +76,7 @@ interface StorageProtocol {
fun updateOpenGroup(openGroup: OpenGroup)
fun getOpenGroup(threadId: Long): OpenGroup?
fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo?
- fun onOpenGroupAdded(server: String)
+ fun onOpenGroupAdded(server: String, room: String)
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
fun getOpenGroup(room: String, server: String): OpenGroup?
@@ -120,6 +128,8 @@ interface StorageProtocol {
// Closed Groups
fun getGroup(groupID: String): GroupRecord?
fun createGroup(groupID: String, title: String?, members: List, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List, formationTimestamp: Long)
+ fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map, formationTimestamp: Long, encryptionKeyPair: ECKeyPair)
+ fun updateGroupConfig(groupPublicKey: String)
fun isGroupActive(groupPublicKey: String): Boolean
fun setActive(groupID: String, value: Boolean)
fun getZombieMembers(groupID: String): Set
@@ -130,7 +140,7 @@ interface StorageProtocol {
fun getAllActiveClosedGroupPublicKeys(): Set
fun addClosedGroupPublicKey(groupPublicKey: String)
fun removeClosedGroupPublicKey(groupPublicKey: String)
- fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
+ fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long)
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type,
name: String, members: Collection, admins: Collection, sentTimestamp: Long)
@@ -141,18 +151,20 @@ interface StorageProtocol {
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
fun updateFormationTimestamp(groupID: String, formationTimestamp: Long)
fun updateTimestampUpdated(groupID: String, updatedTimestamp: Long)
- fun setExpirationTimer(groupID: String, duration: Int)
+ fun setExpirationTimer(address: String, duration: Int)
// Groups
- fun getAllGroups(): List
+ fun getAllGroups(includeInactive: Boolean): List
// Settings
fun setProfileSharing(address: Address, value: Boolean)
+
// Thread
fun getOrCreateThreadIdFor(address: Address): Long
- fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String? = null, openGroupID: String? = null): Long
+ fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long?
fun getThreadId(publicKeyOrOpenGroupID: String): Long?
+ fun getThreadId(openGroup: OpenGroup): Long?
fun getThreadId(address: Address): Long?
fun getThreadId(recipient: Recipient): Long?
fun getThreadIdForMms(mmsId: Long): Long
@@ -160,7 +172,10 @@ interface StorageProtocol {
fun trimThread(threadID: Long, threadLimit: Int)
fun trimThreadBefore(threadID: Long, timestamp: Long)
fun getMessageCount(threadID: Long): Long
- fun deleteConversation(threadId: Long)
+ fun setPinned(threadID: Long, isPinned: Boolean)
+ fun isPinned(threadID: Long): Boolean
+ fun deleteConversation(threadID: Long)
+ fun setThreadDate(threadId: Long, newDate: Long)
// Contacts
fun getContactWithSessionID(sessionID: String): Contact?
@@ -168,6 +183,7 @@ interface StorageProtocol {
fun setContact(contact: Contact)
fun getRecipientForThread(threadId: Long): Recipient?
fun getRecipientSettings(address: Address): RecipientSettings?
+ fun addLibSessionContacts(contacts: List)
fun addContacts(contacts: List)
// Attachments
@@ -178,13 +194,14 @@ interface StorageProtocol {
/**
* Returns the ID of the `TSIncomingMessage` that was constructed.
*/
- fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?, attachments: List, runIncrement: Boolean, runThreadUpdate: Boolean): Long?
- fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean)
- fun incrementUnread(threadId: Long, amount: Int, unreadMentionAmount: Int)
+ fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?, attachments: List, runThreadUpdate: Boolean): Long?
+ fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean = false)
+ fun getLastSeen(threadId: Long): Long
fun updateThread(threadId: Long, unarchive: Boolean)
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
fun insertMessageRequestResponse(response: MessageRequestResponse)
fun setRecipientApproved(recipient: Recipient, approved: Boolean)
+ fun getRecipientApproved(address: Address): Boolean
fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean)
fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long)
fun conversationHasOutgoing(userPublicKey: String): Boolean
@@ -204,10 +221,16 @@ interface StorageProtocol {
fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean)
fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long)
fun deleteReactions(messageId: Long, mms: Boolean)
- fun unblock(toUnblock: Iterable)
+ fun setBlocked(recipients: Iterable