mirror of
https://github.com/oxen-io/session-android.git
synced 2025-04-21 22:11:38 +00:00
Integrate shared libsession-util library (#1096)
* feat: add some config db basics and DI for it, make the user profile optional, start looking at integrate building from initial dump * update: get latest util library submodule update * refactor: fix compile for refactored API * refactor: naming consistent with library * feat: add in config storage and injection to common places, managing lifecycle of native instances * refactor: config database changes, new protos, adding in support for config base namespace queries * refactor: config query and store use the same format as other platforms * feat: add batch snode calls and try to poll from all the config namespaces * fix: add optional namespace in signature and params * feat: add raw requests for modifying expiry and getting expiries * feat: add some base config migration logic, start implementing wrappers for conversation and expiry types * chore: update libsession base * feat: start integrating conversation wrapper functions * feat: add basic conversation info volatile types and implementations, start working on tests * feat: more common library wrapper implementation and test * fix: tests and compile issues * fix: fix tests, don't use iterables * feat: add all iterators and tests * feat: add in more config factory for volatile * feat: update request responses and their appropriate processing * feat: add storage with hashes and some basic profile update logic in config factory probably move that somewhere else * feat: adding config sync functionality, refactoring jobs to execute in suspend context to do some nice coroutine execution * refactor: moving some properties around so we have access in libsession * feat: expand on the config sync job, finish basic implementation to test against * feat: add forced config sync * feat: syncs the user profile stuff for now, and errors back to placeholder instead of unknown recipient * feat: add basic message read logic for synchronizing last reads, need to modify the query to use the last seen instead of the unread count in a subquery possibly for thread display record * feat: add broken unreads everywhere * fix: unreads work now for incoming messages, need to sync conv volatile properly still * feat: batching poll responses properly and handling groups properly * fix: replace the mark read receiver (from notifications) to use the new set last seen mark read logic * feat: update to the group list branch * fix: compile errors from updating library to use latest branch, now requires cmake 3.22.1 * fix: fix the contact tests * fix: getters weren't getters properly in the config factory, fixed new onboarding from configs * feat: add the last seen * feat: start adding user groups wrapper objects * refactor: add more else branches for unimplemented types * feat: buffer the last read when in conversation * feat: add basic contact logic for setting local contact state. Need to implement handling properly * refactor: trying to just include blocked status for now in updating contacts * fix: add some more contact syncing: nicknames, approved statuses, blocked statuses * feat: start implementing hashes in shared lib and refactoring * feat: start to implement group list info classes and wrappers and refactor to use library based hashes * feat: incorporate hashes from library, more wrapper for user groups and serialization from c++ * feat: adding more serialization changes for community base info and user groups LGC * feat: adding more serialize deserialize to legacy closed groups * feat: finish serial/deserial helper * feat: just implement deserialize community info * refactor: refactor tests and wrappers to use less pointers, finish implementing user groups API * feat: finish latest wrappers fix tests and continue building default generation functions. refactor defaults to be used if no stored data blob in DB * feat: more usergroup functionality, storage functionality for checking pinned status, adding pinned status for NTS/contacts, move community info parse full url to base community, add StorageProtocol logic for group info * feat: adding user groups to the list of user configs, refactorign some of the config factory to fetch the user configs easier. Add handling for polling user group namespace * feat: implement the default user config list * feat: add user group config handling * chore: extra missed existing group * refactor: use existing lookup for objects in wrappers so they don't overwrite missing values * feat: add contacts expiry serialization/deserialization, more LGC, timestamps to add closed group encryption info (for latest tracking) * refactor: change how expiration timer works for contacts, set the expiration timer for those conversations in handling contact configs * feat: add expiration updates via config for contacts as well * feat: add almost all group editing cases, need to hook into the thread deletion for groups in the user groups * feat: open group joining should work now * feat: add groups to configs for push * fix: handling user group updates bug fix for closed groups instead of all groups * fix: open group sync persistence * feat: add in activity finish if recipient no longer exists (deleted thread) from sync * feat: support avatar removal from shared library * feat: support thread deletion and refactoring a lot of getOrCreateThread references to go via storage or assume they are correctly set to hook into the contact and volatile creation during thread creation * fix: database update not deleting in certain circumstances, storage persisting and removing the volatile convo info for thread deletion / creation, NTS hidden getter values in shared library * refactor: make update listener visibility package * refactor: update kotlin * feat: update dependencies and support outdated config messages, refactor config factory to return null configs if new configs not supported * feat: update shared library to use priority only, fix compile errors, fix group member sync problem * fix: compile error * fix: profile avatar fixes for local user now that we aren't setting local user profile key * Revert "fix: profile avatar fixes for local user now that we aren't setting local user profile key" This reverts commit 3f569e34034713ee230581bc118e9127a8d0f434. * refactor: let the local number update recipient details in profile manager * fix: don't recreate thread after leaving * fix: fix up the duplicate thread creation in the message receive handler * fix: fix the placeholder rendering on new messages, add in extra context logging for adding contacts and preventing new thread creation on new messages of various types * feat: add test theme for xml layout previews * feat: add shortened hex for session IDs throughout, replace nullable getName with null in underlying contacts for individual contacts, build shared lib with release mode, remove todo, fix broken unit test * feat: setup android unit tests for verifying storage behaviours and state of shared configs * feat: adding dependencies to try and get android tests working, fixing bug with initial config not syncing properly * fix: remove hilt testing, add spy on app context storage field instead, update libsession-util to fixed sodium cmake branch * refactor: use PR version of libsession-util to test cmake build * fix: new build on normal repo * feat: new libsession util commit * refactor: remove the old custom build libsodium stuff from cmake * feat: update libsession module * fix: add legacy config subscription to the home activity to enable showing banner at any time * fix: pinned status for communities and groups, group last read time being set to snodeapi.now on finish joining * fix: some open group volatile convo fix for last read timer being set. Need to investigate further * fix: prevent blocking local number * fix: adding in more checks for open group inbox recipients before being saved to the shared configs. Prevent sending typing indicator for blocked users * fix: add blocked check for read receipt and updating expiring messages * fix: another contact recipient config library call removed for non-standard IDs * fix: another ID check * fix: don't process thread creation for user is sender && recipient (sync message) for message request responses * refactor: mark as read on open and use less buffer time * fix: finally fix the darn unread count issue by * fix: removing debug logs, adding failure error handling logs for expiry message updater, properly using the message thread ID created for the expiring messages. Process the non-thread messages properly with await in BatchMessageReceiveJob * fix: checking the last read open to message and make sure that scroll behaviour matches expected, fix the config sync job not deleting ALL old hashes only latest * refactor: try to add a retry logic to config sync job in case of snode failure * build: update submodule * fix: remove user notifications for leaving group to prevent synced device issues, don't create thread in messages for new closed groups, includei nactive groups in the deletion queries for merging group configs * feat: use blinded message count for banner also * refactor: remove some logging, don't use blinded conversations in the list * fix: don't set the read flag in update notifications, some roundabout logic for first loads and scrolling to last known positions * refactor: merge changes, re-add the group check in unapproved messages * fix: re-poll on fail in case that was breaking anything * fix: pinning groups and notifying list listeners in threadDb.setPinned * feat: add in TTL extension subrequest and builder, enable extending TTLs for all latest config messages in poll as subrequest * feat: add block to the delete all message requests, only if they're not open group inbox contacts * refactor: disable edit text for non contacts * refactor: let the user display name return "You" for local user * fix: prevent NTS self create thread on user view bind * refactor: remove populate public key cache if needed call which seems unnecessary at that point, maybe UserView refs have changed since 2020 * refactor: use just first visible instead of completely visible, merge message sender changes * fix: prevent block of users in delete all * fix: self sync sync message failures for default values * feat: update libsession-util, adjust docs, update mms and sms to use message sent timestamp instead of -1 for last read in the thread * fix: some compile issues in tests and some TODOs for things to do before merge * fix: handle recyclerview scrolled on scroll to first unread if it's the first load * fix: added more migration code for deleting unnecessary threads and groups, fixed a post-migration last seen issue on last item (current read is now), comment out actual network sync while testing migrations * feat: adding a force new configs flag and logic for timestamp handling / forced configs, fix issue with handling legacy messages * refactor: re-add the sending of configs * fix: don't add contacts if they don't exist in the profile manager * [wip] fix: trying to consolidate prof pic and key properly * feat: add logs and fix compile issue with a themes.xml entry, add removing profile picture into logic for profile manager * fix: force has sent for local user, only prevent setting last seen for open group recipients, allow empty user pics to trigger config sync in settings * fix: nts threads * fix: open group avatar loop for open groups we have left * feat: add a wrapper hash to track home diff util changes for wrapper contact recipient info, add test for dirty state in double set * feat: add a dump in there as well * refactor: more test code refactor * fix: update last seen if later than current * fix: open group threads and avatar downloads * fix: add max size and maybe fix the non-200 sub requests for batches (for 421s in particular) * fix: open group comparison issues potentially, have to update some more outgoing message open group flags for visibility of details etc * Updated to the latest libSession-util * Updated logic to delete legacy groups when kicked/left * Added the legacy group 'joined_at' value * Replaced incorrect character in JNI * Fixed an issue where the group keyPair was getting encoded incorrectly * Updated the code to ignore outdated legacy group control message changes * Updated the code to ignore messages invalidated by the config * [Review] Updated the poller to process config messages before standard * Cleaned up the outdated message logic * Fixed inverted config dropping flags * Fixed an issue where the joining a community would read all messages Stopped using a reversed RecyclerView in all cases (caused the unread issue) Updated the logic to jump to the newly sent message when sending a message (to be consistent with other platforms) Updated the logic to refresh the DB unread count when the cursor receives an update * Updated the conversation to highlight the first unread message on open * Fixed a couple of bugs with the highlighting * Fixed a bug where the user profile picture wasn't downloading correctly * feat: add all namespaces to delete all messages request and signature verification data * fix: merge namespace hashes for signature returned and * fix: import correct scroll to bottom * build: update version code and name * fix: initial contact generation fix for existing blinded contacts * fix: initial convo generation fix for existing blinded convos (?) * fix: conversation unread not doing a check for standard ID prefix * fix: thread ID not being created for legacy config messages * fix: don't treat 404 as bad snode * fix: don't add retrieve profile job if we have one for that address * build: update build code * fix: reduce attempts for downloading image, invert unreachable type check * fix: attempting to fix preventing message processing if group thread is not active for closed groups and initial contact dump only allows conversations with thread, may need further optimisations though * feat: Added an unread marker and search result focus highlighting * fix: empty set in appropriate places for current closed groups * build: update build version code * fix: fix the notifications and request at appropriate time * refactor: remove debug logging for thread create and delete * build: update build number * fix: new community doesn't break persisting config if the .add request fails * build: trying to track down broken retrieve avatar job * feat: update to latest libsession dev * fix: maybe fix avatar download for new messages * fix: 404s causing snode errors and trying to retrieve avatars that have already 404'd a lot * fix: closed group creation sets thread date to formation timestamp * build: update version code * build: update version code * build: remove debuggable release build * fix: use new permissions for external attachments * build: update version code * chore: remove debug logs * fix: tests and main thread blocking db fetch for path status view * wip: trying to track down failure to mark conversation as read in delayed group add * wip: add more logs for initial last Read sync of communities * wip: maybe the volatile is being updated with 0 on batch message receive? * fix: maybe syncing read statuses are working now * chore: remove debug logs * build: update build number * fix: trying to improve performance * fix: add close to banner * refactor: hide seed reminder in preview * build: update build number * fix: maybe requires update thread no matter what * fix: message request banner shows again * fix: android tests work again and permissions * fix: blocked contacts click handler being overridden by something * Revert "fix: blocked contacts click handler being overridden by something" This reverts commit 608572fc426def0850085727ed0c399623110c37. * build: update build number * refactor: remove unused dependencies and update minor for sqlcipher * fix: actually do insert contact, because otherwise name doesn't get set properly * fix: maybe fix scroll to bottom issue * build: update build number * fix: the message time and jump to message queries are more optimized * fix: maybe fix the last seen issues * build: update build number * fix: pfp broken closed groups why * fix: add admins and members as member list instead of just members * fix: exclude lgc without membership > 1 and inactive explicitly * fix: submodule update * fix: compiles with removal of iterator erase * fix: unread indicator updates properly in ConversationActivityV2 * fix: unread notifications clear and altered if any notifications exist (prevents clearing read notifications in conversation or on home screen) * refactor: profile pictures kinda broken * build: update build number * refactor: remove full hash from log * fix: isPinned threadDB call * refactor: use mutex in all libsession native calls, change timestamp * refactor: add basic support for blinded v2 prefixes --------- Co-authored-by: Morgan Pretty <morgan.t.pretty@gmail.com>
This commit is contained in:
parent
96ec733517
commit
ac18f1cbfe
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "libsession-util/libsession-util"]
|
||||
path = libsession-util/libsession-util
|
||||
url = https://github.com/oxen-io/libsession-util.git
|
@ -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 = 353
|
||||
def canonicalVersionName = "1.17.0"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,97 @@
|
||||
package network.loki.messenger
|
||||
|
||||
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<ByteArray, String>? {
|
||||
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<Contact>): 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() {
|
||||
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))
|
||||
}
|
||||
|
||||
}
|
@ -29,12 +29,16 @@
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -260,6 +260,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
CALL_CONNECTED -> {
|
||||
wantsToAnswer = false
|
||||
}
|
||||
else -> { /* do nothing */ }
|
||||
}
|
||||
updateControls(state)
|
||||
}
|
||||
|
@ -32,10 +32,10 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||
var isLarge = false
|
||||
|
||||
private val profilePicturesCache = mutableMapOf<String, String?>()
|
||||
private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
||||
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()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 }
|
||||
|
@ -3,28 +3,50 @@ package org.thoughtcrime.securesms.conversation.v2
|
||||
import android.Manifest
|
||||
import android.animation.FloatEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.*
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.SpannedString
|
||||
import android.text.TextUtils
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Pair
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.DimenRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.text.set
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.drawToBitmap
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@ -32,6 +54,11 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.annimon.stream.Stream
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
@ -58,8 +85,12 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.*
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.MediaTypes
|
||||
import org.session.libsession.utilities.Stub
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.concurrent.SimpleTask
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.recipients.RecipientModifiedListener
|
||||
@ -91,10 +122,25 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
|
||||
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.*
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.database.*
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||
import org.thoughtcrime.securesms.database.ReactionDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
@ -107,16 +153,31 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
|
||||
import org.thoughtcrime.securesms.mms.*
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide
|
||||
import org.thoughtcrime.securesms.mms.GifSlide
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
|
||||
import org.thoughtcrime.securesms.showExpirationDialog
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import org.thoughtcrime.securesms.util.*
|
||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.inject.Inject
|
||||
@ -185,11 +246,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
|
||||
@ -210,6 +271,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val searchViewModel: SearchViewModel by viewModels()
|
||||
var searchViewItem: MenuItem? = null
|
||||
|
||||
private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
private var emojiPickerVisible = false
|
||||
|
||||
private val isScrolledToBottom: Boolean
|
||||
@ -229,11 +291,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)
|
||||
},
|
||||
@ -275,6 +343,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<Address?>(null)
|
||||
private val firstLoad = AtomicBoolean(true)
|
||||
|
||||
private lateinit var reactionDelegate: ConversationReactionDelegate
|
||||
private val reactWithAnyEmojiStartPage = -1
|
||||
@ -319,28 +388,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()
|
||||
@ -350,20 +422,30 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val weakActivity = WeakReference(this)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
|
||||
|
||||
// Note: We are accessing the `adapter` property because we want it to be loaded on
|
||||
// the background thread to avoid blocking the UI thread and potentially hanging when
|
||||
// transitioning to the activity
|
||||
weakActivity.get()?.adapter ?: return@launch
|
||||
|
||||
// 'Get' instead of 'GetAndSet' here because we want to trigger the highlight in 'onFirstLoad'
|
||||
// by triggering 'jumpToMessage' using these values
|
||||
val messageTimestamp = messageToScrollTimestamp.get()
|
||||
val author = messageToScrollAuthor.get()
|
||||
val targetPosition = if (author != null && messageTimestamp >= 0) mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, messageTimestamp, author, reverseMessageList) else -1
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
setUpRecyclerView()
|
||||
setUpTypingObserver()
|
||||
setUpRecipientObserver()
|
||||
getLatestOpenGroupInfoIfNeeded()
|
||||
setUpSearchResultObserver()
|
||||
scrollToFirstUnreadMessageIfNeeded()
|
||||
|
||||
if (author != null && messageTimestamp >= 0 && targetPosition >= 0) {
|
||||
binding?.conversationRecyclerView?.scrollToPosition(targetPosition)
|
||||
}
|
||||
else {
|
||||
scrollToFirstUnreadMessageIfNeeded(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -371,16 +453,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,
|
||||
@ -412,18 +503,40 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
|
||||
return ConversationLoader(viewModel.threadId, !isIncomingMessageRequestThread(), this@ConversationActivityV2)
|
||||
return ConversationLoader(viewModel.threadId, reverseMessageList, this@ConversationActivityV2)
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<Cursor>, 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<Cursor>) {
|
||||
@ -433,7 +546,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)
|
||||
@ -442,6 +555,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 { _, _, _, _, _, _, _, _, _ ->
|
||||
@ -577,7 +694,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
binding?.blockedBannerTextView?.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
|
||||
binding?.blockedBanner?.isVisible = recipient.isBlocked
|
||||
binding?.blockedBanner?.setOnClickListener { viewModel.unblock(this@ConversationActivityV2) }
|
||||
binding?.blockedBanner?.setOnClickListener { viewModel.unblock() }
|
||||
}
|
||||
|
||||
private fun setUpLinkPreviewObserver() {
|
||||
@ -610,15 +727,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 {
|
||||
@ -702,11 +841,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)
|
||||
}
|
||||
@ -904,17 +1040,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
|
||||
}
|
||||
@ -970,7 +1149,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
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(this@ConversationActivityV2)
|
||||
viewModel.block()
|
||||
if (deleteThread) {
|
||||
viewModel.deleteThread()
|
||||
finish()
|
||||
@ -1005,7 +1184,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
if (group?.isActive == false) { return }
|
||||
}
|
||||
showExpirationDialog(thread.expireMessages) { expirationTime ->
|
||||
recipientDb.setExpireMessages(thread, expirationTime)
|
||||
storage.setExpirationTimer(thread.address.serialize(), expirationTime)
|
||||
val message = ExpirationTimerUpdate(expirationTime)
|
||||
message.recipient = thread.address.serialize()
|
||||
message.sentTimestamp = SnodeAPI.nowWithOffset
|
||||
@ -1022,7 +1201,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
destructiveButton(
|
||||
R.string.ConversationActivity_unblock,
|
||||
R.string.AccessibilityId_block_confirm
|
||||
) { viewModel.unblock(this@ConversationActivityV2) }
|
||||
) { viewModel.unblock() }
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
@ -1368,11 +1547,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) {
|
||||
@ -1390,19 +1575,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false) {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false): Pair<Address, Long>? {
|
||||
val recipient = viewModel.recipient ?: return null
|
||||
val sentTimestamp = SnodeAPI.nowWithOffset
|
||||
processMessageRequestApproval()
|
||||
val text = getMessageBody()
|
||||
val userPublicKey = textSecurePreferences.getLocalNumber()
|
||||
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
|
||||
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
|
||||
val dialog = SendSeedDialog { sendTextOnlyMessage(true) }
|
||||
return dialog.show(supportFragmentManager, "Send Seed Dialog")
|
||||
dialog.show(supportFragmentManager, "Send Seed Dialog")
|
||||
return null
|
||||
}
|
||||
// Create the message
|
||||
val message = VisibleMessage()
|
||||
message.sentTimestamp = SnodeAPI.nowWithOffset
|
||||
message.sentTimestamp = sentTimestamp
|
||||
message.text = text
|
||||
val outgoingTextMessage = OutgoingTextMessage.from(message, recipient)
|
||||
// Clear the input bar
|
||||
@ -1419,14 +1606,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<Attachment>, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
private fun sendAttachments(attachments: List<Attachment>, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null): Pair<Address, Long>? {
|
||||
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()
|
||||
@ -1460,6 +1649,7 @@ 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() {
|
||||
@ -1829,7 +2019,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() }
|
||||
}
|
||||
}
|
||||
@ -1866,15 +2056,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()
|
||||
}
|
||||
|
@ -31,10 +31,14 @@ 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<String>(1024, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
private val contactCache = SparseArray<Contact>(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
|
||||
)
|
||||
@ -183,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
|
||||
}
|
||||
|
||||
@ -217,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) {
|
||||
if (isReversed) {
|
||||
cursor.moveToPosition(i)
|
||||
val message = messageDB.readerFor(cursor).current
|
||||
if (message.isOutgoing || message.dateReceived <= lastSeenTimestamp) { return 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
|
||||
}
|
||||
@ -231,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
|
||||
}
|
||||
@ -241,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
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContentResolver
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.cash.copper.flow.observeQuery
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
@ -21,15 +21,16 @@ import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import java.util.UUID
|
||||
|
||||
class ConversationViewModel(
|
||||
val threadId: Long,
|
||||
val edKeyPair: KeyPair?,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val repository: ConversationRepository,
|
||||
private val storage: Storage
|
||||
) : ViewModel() {
|
||||
@ -37,7 +38,7 @@ class ConversationViewModel(
|
||||
val showSendAfterApprovalText: Boolean
|
||||
get() = recipient?.run { isContactRecipient && !isLocalNumber && !hasApprovedMe() } ?: false
|
||||
|
||||
private val _uiState = MutableStateFlow(ConversationUiState())
|
||||
private val _uiState = MutableStateFlow(ConversationUiState(conversationExists = true))
|
||||
val uiState: StateFlow<ConversationUiState> = _uiState
|
||||
|
||||
private var _recipient: RetrieveOnce<Recipient> = RetrieveOnce {
|
||||
@ -61,6 +62,18 @@ class ConversationViewModel(
|
||||
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId))
|
||||
.collect {
|
||||
val recipientExists = storage.getRecipientForThread(threadId) != null
|
||||
if (!recipientExists && _uiState.value.conversationExists) {
|
||||
_uiState.update { it.copy(conversationExists = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDraft(text: String) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
repository.saveDraft(threadId, text)
|
||||
@ -81,27 +94,17 @@ class ConversationViewModel(
|
||||
repository.inviteContacts(threadId, contacts)
|
||||
}
|
||||
|
||||
fun block(context: Context) {
|
||||
fun block() {
|
||||
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for block action")
|
||||
if (recipient.isContactRecipient) {
|
||||
repository.setBlocked(recipient, true)
|
||||
|
||||
// TODO: Remove in UserConfig branch
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unblock(context: Context) {
|
||||
fun unblock() {
|
||||
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for unblock action")
|
||||
if (recipient.isContactRecipient) {
|
||||
repository.setBlocked(recipient, false)
|
||||
|
||||
// TODO: Remove in UserConfig branch
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,19 +201,20 @@ class ConversationViewModel(
|
||||
|
||||
@dagger.assisted.AssistedFactory
|
||||
interface AssistedFactory {
|
||||
fun create(threadId: Long, edKeyPair: KeyPair?): Factory
|
||||
fun create(threadId: Long, edKeyPair: KeyPair?, contentResolver: ContentResolver): Factory
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class Factory @AssistedInject constructor(
|
||||
@Assisted private val threadId: Long,
|
||||
@Assisted private val edKeyPair: KeyPair?,
|
||||
@Assisted private val contentResolver: ContentResolver,
|
||||
private val repository: ConversationRepository,
|
||||
private val storage: Storage
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ConversationViewModel(threadId, edKeyPair, repository, storage) as T
|
||||
return ConversationViewModel(threadId, edKeyPair, contentResolver, repository, storage) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -219,7 +223,8 @@ data class UiMessage(val id: Long, val message: String)
|
||||
|
||||
data class ConversationUiState(
|
||||
val uiMessages: List<UiMessage> = emptyList(),
|
||||
val isMessageRequestAccepted: Boolean? = null
|
||||
val isMessageRequestAccepted: Boolean? = null,
|
||||
val conversationExists: Boolean
|
||||
)
|
||||
|
||||
data class RetrieveOnce<T>(val retrieval: () -> T?) {
|
||||
|
@ -39,12 +39,7 @@ class BlockedDialog(private val recipient: Recipient, private val context: Conte
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : D
|
||||
ThreadUtils.queue {
|
||||
try {
|
||||
openGroup.apply { OpenGroupManager.add(server, room, serverPublicKey, activity) }
|
||||
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server)
|
||||
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()
|
||||
|
@ -63,17 +63,18 @@ object ConversationMenuHelper {
|
||||
// Base menu (options that should always be present)
|
||||
inflater.inflate(R.menu.menu_conversation, menu)
|
||||
// Expiring messages
|
||||
if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient)) {
|
||||
if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient) && !thread.isBlocked) {
|
||||
if (thread.expireMessages > 0) {
|
||||
inflater.inflate(R.menu.menu_conversation_expiration_on, menu)
|
||||
val item = menu.findItem(R.id.menu_expiring_messages)
|
||||
val actionView = item.actionView
|
||||
item.actionView?.let { actionView ->
|
||||
val iconView = actionView.findViewById<ImageView>(R.id.menu_badge_icon)
|
||||
val badgeView = actionView.findViewById<TextView>(R.id.expiration_badge)
|
||||
@ColorInt val color = context.getColorFromAttr(android.R.attr.textColorPrimary)
|
||||
iconView.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)
|
||||
badgeView.text = ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, thread.expireMessages)
|
||||
actionView.setOnClickListener { onOptionsItemSelected(item) }
|
||||
}
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_conversation_expiration_off, menu)
|
||||
}
|
||||
@ -86,7 +87,7 @@ object ConversationMenuHelper {
|
||||
if (thread.isContactRecipient) {
|
||||
if (thread.isBlocked) {
|
||||
inflater.inflate(R.menu.menu_conversation_unblock, menu)
|
||||
} else {
|
||||
} else if (!thread.isLocalNumber) {
|
||||
inflater.inflate(R.menu.menu_conversation_block, menu)
|
||||
}
|
||||
}
|
||||
@ -309,7 +310,7 @@ object ConversationMenuHelper {
|
||||
val groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
|
||||
val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
|
||||
|
||||
if (isClosedGroup) MessageSender.leave(groupPublicKey, true)
|
||||
if (isClosedGroup) MessageSender.leave(groupPublicKey, notifyUser = false)
|
||||
else onLeaveFailed()
|
||||
} catch (e: Exception) {
|
||||
onLeaveFailed()
|
||||
|
@ -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<View>(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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,9 +245,14 @@ 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))
|
||||
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();
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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<GroupRecord> getAllGroups() {
|
||||
public List<GroupRecord> getAllGroups(boolean includeInactive) {
|
||||
Reader reader = getGroups();
|
||||
GroupRecord record;
|
||||
List<GroupRecord> 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;
|
||||
|
@ -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()
|
||||
|
@ -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<Long, OpenGroup> {
|
||||
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
|
||||
|
@ -20,13 +20,11 @@ import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import com.annimon.stream.Stream
|
||||
import com.google.android.mms.pdu_alt.NotificationInd
|
||||
import com.google.android.mms.pdu_alt.PduHeaders
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage
|
||||
@ -41,16 +39,13 @@ import org.session.libsession.utilities.Address.Companion.UNKNOWN
|
||||
import org.session.libsession.utilities.Address.Companion.fromExternal
|
||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||
import org.session.libsession.utilities.Contact
|
||||
import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch
|
||||
import org.session.libsession.utilities.IdentityKeyMismatchList
|
||||
import org.session.libsession.utilities.NetworkFailure
|
||||
import org.session.libsession.utilities.NetworkFailureList
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled
|
||||
import org.session.libsession.utilities.Util.toIsoBytes
|
||||
import org.session.libsession.utilities.Util.toIsoString
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.recipients.RecipientFormattingException
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.ThreadUtils.queue
|
||||
@ -162,7 +157,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
)
|
||||
get(context).groupReceiptDatabase()
|
||||
.update(ourAddress, id, status, timestamp)
|
||||
get(context).threadDatabase().update(threadId, false)
|
||||
get(context).threadDatabase().update(threadId, false, true)
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
}
|
||||
@ -205,25 +200,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(RecipientFormattingException::class, MmsException::class)
|
||||
private fun getThreadIdFor(retrieved: IncomingMediaMessage): Long {
|
||||
return if (retrieved.groupId != null) {
|
||||
val groupRecipients = Recipient.from(
|
||||
context,
|
||||
retrieved.groupId,
|
||||
true
|
||||
)
|
||||
get(context).threadDatabase().getOrCreateThreadIdFor(groupRecipients)
|
||||
} else {
|
||||
val sender = Recipient.from(
|
||||
context,
|
||||
retrieved.from,
|
||||
true
|
||||
)
|
||||
get(context).threadDatabase().getOrCreateThreadIdFor(sender)
|
||||
}
|
||||
}
|
||||
|
||||
private fun rawQuery(where: String, arguments: Array<String>?): Cursor {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.rawQuery(
|
||||
@ -259,7 +235,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
" WHERE " + ID + " = ?", arrayOf(id.toString() + "")
|
||||
)
|
||||
if (threadId.isPresent) {
|
||||
get(context).threadDatabase().update(threadId.get(), false)
|
||||
get(context).threadDatabase().update(threadId.get(), false, true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,10 +292,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
val attachmentDatabase = get(context).attachmentDatabase()
|
||||
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(messageId) })
|
||||
val threadId = getThreadIdForMessage(messageId)
|
||||
if (!read) {
|
||||
val mentionChange = if (hasMention) { 1 } else { 0 }
|
||||
get(context).threadDatabase().decrementUnread(threadId, 1, mentionChange)
|
||||
}
|
||||
|
||||
markAs(messageId, MmsSmsColumns.Types.BASE_DELETED_TYPE, threadId)
|
||||
}
|
||||
|
||||
@ -343,6 +316,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(id.toString()))
|
||||
}
|
||||
|
||||
fun setMessagesRead(threadId: Long, beforeTime: Long): List<MarkedMessageInfo> {
|
||||
return setMessagesRead(
|
||||
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?",
|
||||
arrayOf(threadId.toString(), beforeTime.toString())
|
||||
)
|
||||
}
|
||||
|
||||
fun setMessagesRead(threadId: Long): List<MarkedMessageInfo> {
|
||||
return setMessagesRead(
|
||||
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)",
|
||||
@ -567,18 +547,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
contentLocation: String,
|
||||
threadId: Long, mailbox: Long,
|
||||
serverTimestamp: Long,
|
||||
runIncrement: Boolean,
|
||||
runThreadUpdate: Boolean
|
||||
): Optional<InsertResult> {
|
||||
var threadId = threadId
|
||||
if (threadId == -1L || retrieved.isGroupMessage) {
|
||||
try {
|
||||
threadId = getThreadIdFor(retrieved)
|
||||
} catch (e: RecipientFormattingException) {
|
||||
Log.w("MmsDatabase", e)
|
||||
if (threadId == -1L) throw MmsException(e)
|
||||
}
|
||||
}
|
||||
if (threadId < 0 ) throw MmsException("No thread ID supplied!")
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(DATE_SENT, retrieved.sentTimeMillis)
|
||||
contentValues.put(ADDRESS, retrieved.from.serialize())
|
||||
@ -632,12 +603,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
null,
|
||||
)
|
||||
if (!MmsSmsColumns.Types.isExpirationTimerUpdate(mailbox)) {
|
||||
if (runIncrement) {
|
||||
val mentionAmount = if (retrieved.hasMention()) { 1 } else { 0 }
|
||||
get(context).threadDatabase().incrementUnread(threadId, 1, mentionAmount)
|
||||
}
|
||||
if (runThreadUpdate) {
|
||||
get(context).threadDatabase().update(threadId, true)
|
||||
get(context).threadDatabase().update(threadId, true, true)
|
||||
}
|
||||
}
|
||||
notifyConversationListeners(threadId)
|
||||
@ -651,27 +618,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
serverTimestamp: Long,
|
||||
runThreadUpdate: Boolean
|
||||
): Optional<InsertResult> {
|
||||
var threadId = threadId
|
||||
if (threadId == -1L) {
|
||||
if (retrieved.isGroup) {
|
||||
val decodedGroupId: String = if (retrieved is OutgoingExpirationUpdateMessage) {
|
||||
retrieved.groupId
|
||||
} else {
|
||||
(retrieved as OutgoingGroupMediaMessage).groupId
|
||||
}
|
||||
val groupId: String
|
||||
groupId = try {
|
||||
doubleEncodeGroupID(decodedGroupId)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Couldn't encrypt group ID")
|
||||
throw MmsException(e)
|
||||
}
|
||||
val group = Recipient.from(context, fromSerialized(groupId), false)
|
||||
threadId = get(context).threadDatabase().getOrCreateThreadIdFor(group)
|
||||
} else {
|
||||
threadId = get(context).threadDatabase().getOrCreateThreadIdFor(retrieved.recipient)
|
||||
}
|
||||
}
|
||||
if (threadId < 0 ) throw MmsException("No thread ID supplied!")
|
||||
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
|
||||
if (messageId == -1L) {
|
||||
return Optional.absent()
|
||||
@ -686,7 +633,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
retrieved: IncomingMediaMessage,
|
||||
threadId: Long,
|
||||
serverTimestamp: Long = 0,
|
||||
runIncrement: Boolean,
|
||||
runThreadUpdate: Boolean
|
||||
): Optional<InsertResult> {
|
||||
var type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT
|
||||
@ -705,7 +651,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
if (retrieved.isMessageRequestResponse) {
|
||||
type = type or MmsSmsColumns.Types.MESSAGE_REQUEST_RESPONSE_BIT
|
||||
}
|
||||
return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runIncrement, runThreadUpdate)
|
||||
return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runThreadUpdate)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
@ -794,10 +740,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
)
|
||||
}
|
||||
with (get(context).threadDatabase()) {
|
||||
setLastSeen(threadId)
|
||||
val lastSeen = getLastSeenAndHasSent(threadId).first()
|
||||
if (lastSeen < message.sentTimeMillis) {
|
||||
setLastSeen(threadId, message.sentTimeMillis)
|
||||
}
|
||||
setHasSent(threadId, true)
|
||||
if (runThreadUpdate) {
|
||||
update(threadId, true)
|
||||
update(threadId, true, true)
|
||||
}
|
||||
}
|
||||
return messageId
|
||||
@ -932,7 +881,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
groupReceiptDatabase.deleteRowsForMessage(messageId)
|
||||
val database = databaseHelper.writableDatabase
|
||||
database!!.delete(TABLE_NAME, ID_WHERE, arrayOf(messageId.toString()))
|
||||
val threadDeleted = get(context).threadDatabase().update(threadId, false)
|
||||
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
|
||||
notifyConversationListeners(threadId)
|
||||
notifyStickerListeners()
|
||||
notifyStickerPackListeners()
|
||||
@ -949,7 +898,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
val database = databaseHelper.writableDatabase
|
||||
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
|
||||
|
||||
val threadDeleted = get(context).threadDatabase().update(threadId, false)
|
||||
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
|
||||
notifyConversationListeners(threadId)
|
||||
notifyStickerListeners()
|
||||
notifyStickerPackListeners()
|
||||
@ -1147,7 +1096,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
}
|
||||
val threadDb = get(context).threadDatabase()
|
||||
for (threadId in threadIds) {
|
||||
val threadDeleted = threadDb.update(threadId, false)
|
||||
val threadDeleted = threadDb.update(threadId, false, true)
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
notifyStickerListeners()
|
||||
|
@ -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<Boolean, Long> 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<Boolean, Long>(MmsSmsColumns.Types.isOutgoingMessageType(type), sentTime);
|
||||
}
|
||||
|
||||
public class Reader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
|
@ -62,13 +62,14 @@ public class RecipientDatabase extends Database {
|
||||
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
|
||||
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
|
||||
private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none
|
||||
private static final String WRAPPER_HASH = "wrapper_hash";
|
||||
|
||||
private static final String[] RECIPIENT_PROJECTION = new String[] {
|
||||
BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED,
|
||||
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
|
||||
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
|
||||
UNIDENTIFIED_ACCESS_MODE,
|
||||
FORCE_SMS_SELECTION, NOTIFY_TYPE,
|
||||
FORCE_SMS_SELECTION, NOTIFY_TYPE, WRAPPER_HASH
|
||||
};
|
||||
|
||||
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
@ -136,6 +137,11 @@ public class RecipientDatabase extends Database {
|
||||
"OR "+ADDRESS+" IN (SELECT "+GroupDatabase.TABLE_NAME+"."+GroupDatabase.ADMINS+" FROM "+GroupDatabase.TABLE_NAME+")))";
|
||||
}
|
||||
|
||||
public static String getAddWrapperHash() {
|
||||
return "ALTER TABLE "+TABLE_NAME+" "+
|
||||
"ADD COLUMN "+WRAPPER_HASH+" TEXT DEFAULT NULL;";
|
||||
}
|
||||
|
||||
public static final int NOTIFY_TYPE_ALL = 0;
|
||||
public static final int NOTIFY_TYPE_MENTIONS = 1;
|
||||
public static final int NOTIFY_TYPE_NONE = 2;
|
||||
@ -154,18 +160,14 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
public Optional<RecipientSettings> getRecipientSettings(@NonNull Address address) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[] {address.serialize()}, null, null, null);
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[]{address.serialize()}, null, null, null)) {
|
||||
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return getRecipientSettings(cursor);
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,6 +196,7 @@ public class RecipientDatabase extends Database {
|
||||
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
|
||||
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
|
||||
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
|
||||
String wrapperHash = cursor.getString(cursor.getColumnIndexOrThrow(WRAPPER_HASH));
|
||||
|
||||
MaterialColor color;
|
||||
byte[] profileKey = null;
|
||||
@ -225,7 +228,7 @@ public class RecipientDatabase extends Database {
|
||||
systemPhoneLabel, systemContactUri,
|
||||
signalProfileName, signalProfileAvatar, profileSharing,
|
||||
notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
||||
forceSmsSelection));
|
||||
forceSmsSelection, wrapperHash));
|
||||
}
|
||||
|
||||
public void setColor(@NonNull Recipient recipient, @NonNull MaterialColor color) {
|
||||
@ -252,6 +255,24 @@ public class RecipientDatabase extends Database {
|
||||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public boolean getApproved(@NonNull Address address) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[]{APPROVED}, ADDRESS + " = ?", new String[]{address.serialize()}, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return cursor.getInt(cursor.getColumnIndexOrThrow(APPROVED)) == 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setRecipientHash(@NonNull Recipient recipient, String recipientHash) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(WRAPPER_HASH, recipientHash);
|
||||
updateOrInsert(recipient.getAddress(), values);
|
||||
recipient.resolve().setWrapperHash(recipientHash);
|
||||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public void setApproved(@NonNull Recipient recipient, boolean approved) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(APPROVED, approved ? 1 : 0);
|
||||
@ -268,14 +289,6 @@ public class RecipientDatabase extends Database {
|
||||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public void setBlocked(@NonNull Recipient recipient, boolean blocked) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(BLOCK, blocked ? 1 : 0);
|
||||
updateOrInsert(recipient.getAddress(), values);
|
||||
recipient.resolve().setBlocked(blocked);
|
||||
notifyRecipientListeners();
|
||||
}
|
||||
|
||||
public void setBlocked(@NonNull Iterable<Recipient> recipients, boolean blocked) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -93,6 +93,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
fun cancelPendingMessageSendJobs(threadID: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val attachmentUploadJobKeys = mutableListOf<String>()
|
||||
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,16 +104,20 @@ 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 {
|
||||
val database = databaseHelper.readableDatabase
|
||||
|
@ -148,7 +148,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
long threadId = getThreadIdForMessage(id);
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
@ -234,10 +234,6 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
contentValues.put(BODY, "");
|
||||
contentValues.put(HAS_MENTION, 0);
|
||||
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
if (!read) {
|
||||
DatabaseComponent.get(context).threadDatabase().decrementUnread(threadId, 1, (hasMention ? 1 : 0));
|
||||
}
|
||||
updateTypeBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_DELETED_TYPE);
|
||||
}
|
||||
|
||||
@ -256,7 +252,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
long threadId = getThreadIdForMessage(id);
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
@ -319,7 +315,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
ID + " = ?",
|
||||
new String[] {String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))});
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||
notifyConversationListeners(threadId);
|
||||
foundMessage = true;
|
||||
}
|
||||
@ -337,6 +333,9 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
public List<MarkedMessageInfo> 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<MarkedMessageInfo> setMessagesRead(long threadId) {
|
||||
return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)", new String[] {String.valueOf(threadId)});
|
||||
}
|
||||
@ -400,14 +399,14 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
|
||||
notifyConversationListeners(threadId);
|
||||
notifyConversationListListeners();
|
||||
|
||||
return new Pair<>(messageId, threadId);
|
||||
}
|
||||
|
||||
protected Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
|
||||
protected Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runThreadUpdate) {
|
||||
if (message.isSecureMessage()) {
|
||||
type |= Types.SECURE_MESSAGE_BIT;
|
||||
} else if (message.isGroup()) {
|
||||
@ -486,12 +485,8 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long messageId = db.insert(TABLE_NAME, null, values);
|
||||
|
||||
if (unread && runIncrement) {
|
||||
DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1, (message.hasMention() ? 1 : 0));
|
||||
}
|
||||
|
||||
if (runThreadUpdate) {
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
|
||||
}
|
||||
|
||||
if (message.getSubscriptionId() != -1) {
|
||||
@ -504,16 +499,16 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, boolean runIncrement, boolean runThreadUpdate) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runIncrement, runThreadUpdate);
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, boolean runThreadUpdate) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runThreadUpdate);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertCallMessage(IncomingTextMessage message) {
|
||||
return insertMessageInbox(message, 0, 0, true, true);
|
||||
return insertMessageInbox(message, 0, 0, true);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runIncrement, runThreadUpdate);
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runThreadUpdate) {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runThreadUpdate);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp, boolean runThreadUpdate) {
|
||||
@ -567,9 +562,12 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
|
||||
if (runThreadUpdate) {
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
|
||||
DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
|
||||
}
|
||||
long lastSeen = DatabaseComponent.get(context).threadDatabase().getLastSeenAndHasSent(threadId).first();
|
||||
if (lastSeen < message.getSentTimestampMillis()) {
|
||||
DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId, message.getSentTimestampMillis());
|
||||
}
|
||||
DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId);
|
||||
|
||||
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true);
|
||||
|
||||
@ -616,7 +614,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
|
||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||
notifyConversationListeners(threadId);
|
||||
return threadDeleted;
|
||||
}
|
||||
@ -640,7 +638,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
|
||||
argValues
|
||||
);
|
||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
||||
notifyConversationListeners(threadId);
|
||||
return threadDeleted;
|
||||
}
|
||||
|
@ -2,16 +2,43 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
|
||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED
|
||||
import network.loki.messenger.libsession_util.Contacts
|
||||
import network.loki.messenger.libsession_util.ConversationVolatileConfig
|
||||
import network.loki.messenger.libsession_util.UserGroupsConfig
|
||||
import network.loki.messenger.libsession_util.UserProfile
|
||||
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
|
||||
import network.loki.messenger.libsession_util.util.Conversation
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import network.loki.messenger.libsession_util.util.GroupInfo
|
||||
import network.loki.messenger.libsession_util.util.UserPic
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
import org.session.libsession.messaging.BlindedIdMapping
|
||||
import org.session.libsession.messaging.calls.CallMessageType
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.jobs.*
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||
import org.session.libsession.messaging.jobs.ConfigurationSyncJob
|
||||
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
||||
import org.session.libsession.messaging.jobs.Job
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
import org.session.libsession.messaging.messages.signal.*
|
||||
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
||||
import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.Profile
|
||||
import org.session.libsession.messaging.messages.visible.Reaction
|
||||
@ -23,12 +50,15 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.messaging.utilities.UpdateMessageData
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.utilities.*
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
@ -36,23 +66,103 @@ import org.session.libsession.utilities.ProfileKeyUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.groups.ClosedGroupManager
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol
|
||||
import java.security.MessageDigest
|
||||
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
|
||||
|
||||
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
|
||||
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)
|
||||
}
|
||||
|
||||
override fun getUserPublicKey(): String? {
|
||||
return TextSecurePreferences.getLocalNumber(context)
|
||||
@ -74,6 +184,25 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
database.setProfileAvatar(recipient, profileAvatar)
|
||||
}
|
||||
|
||||
override fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?) {
|
||||
val db = DatabaseComponent.get(context).recipientDatabase()
|
||||
db.setProfileAvatar(recipient, newProfilePicture)
|
||||
db.setProfileKey(recipient, newProfileKey)
|
||||
}
|
||||
|
||||
override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) {
|
||||
val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
|
||||
Recipient.from(context, it, false)
|
||||
}
|
||||
ourRecipient.resolve().profileKey = newProfileKey
|
||||
TextSecurePreferences.setProfileKey(context, newProfileKey?.let { Base64.encodeBytes(it) })
|
||||
TextSecurePreferences.setProfilePictureURL(context, newProfilePicture)
|
||||
|
||||
if (newProfileKey != null) {
|
||||
JobQueue.shared.add(RetrieveProfileAvatarJob(newProfilePicture, ourRecipient.address))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOrGenerateRegistrationID(): Int {
|
||||
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
|
||||
if (registrationID == 0) {
|
||||
@ -94,19 +223,56 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return database.getAttachmentsForMessage(messageID)
|
||||
}
|
||||
|
||||
override fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean) {
|
||||
override fun getLastSeen(threadId: Long): Long {
|
||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||
threadDb.setRead(threadId, updateLastSeen)
|
||||
return threadDb.getLastSeenAndHasSent(threadId)?.first() ?: 0L
|
||||
}
|
||||
|
||||
override fun incrementUnread(threadId: Long, amount: Int, unreadMentionAmount: Int) {
|
||||
override fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean) {
|
||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||
threadDb.incrementUnread(threadId, amount, unreadMentionAmount)
|
||||
getRecipientForThread(threadId)?.let { recipient ->
|
||||
val currentLastRead = threadDb.getLastSeenAndHasSent(threadId).first()
|
||||
// don't set the last read in the volatile if we didn't set it in the DB
|
||||
if (!threadDb.markAllAsRead(threadId, recipient.isGroupRecipient, lastSeenTime, force) && !force) return
|
||||
|
||||
// don't process configs for inbox recipients
|
||||
if (recipient.isOpenGroupInboxRecipient) return
|
||||
|
||||
configFactory.convoVolatile?.let { config ->
|
||||
val convo = when {
|
||||
// recipient closed group
|
||||
recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize()))
|
||||
// recipient is open group
|
||||
recipient.isOpenGroupRecipient -> {
|
||||
val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return
|
||||
BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
|
||||
config.getOrConstructCommunity(base, room, pubKey)
|
||||
} ?: return
|
||||
}
|
||||
// otherwise recipient is one to one
|
||||
recipient.isContactRecipient -> {
|
||||
// don't process non-standard session IDs though
|
||||
val sessionId = SessionId(recipient.address.serialize())
|
||||
if (sessionId.prefix != IdPrefix.STANDARD) return
|
||||
|
||||
config.getOrConstructOneToOne(recipient.address.serialize())
|
||||
}
|
||||
else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.serialize()}")
|
||||
}
|
||||
convo.lastRead = lastSeenTime
|
||||
if (convo.unread) {
|
||||
convo.unread = lastSeenTime <= currentLastRead
|
||||
notifyConversationListListeners()
|
||||
}
|
||||
config.set(convo)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateThread(threadId: Long, unarchive: Boolean) {
|
||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||
threadDb.update(threadId, unarchive)
|
||||
threadDb.update(threadId, unarchive, false)
|
||||
}
|
||||
|
||||
override fun persist(message: VisibleMessage,
|
||||
@ -115,7 +281,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
groupPublicKey: String?,
|
||||
openGroupID: String?,
|
||||
attachments: List<Attachment>,
|
||||
runIncrement: Boolean,
|
||||
runThreadUpdate: Boolean): Long? {
|
||||
var messageID: Long? = null
|
||||
val senderAddress = fromSerialized(message.sender!!)
|
||||
@ -142,13 +307,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
}
|
||||
val targetRecipient = Recipient.from(context, targetAddress, false)
|
||||
if (!targetRecipient.isGroupRecipient) {
|
||||
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
|
||||
if (isUserSender || isUserBlindedSender) {
|
||||
recipientDb.setApproved(targetRecipient, true)
|
||||
setRecipientApproved(targetRecipient, true)
|
||||
} else {
|
||||
recipientDb.setApprovedMe(targetRecipient, true)
|
||||
setRecipientApprovedMe(targetRecipient, true)
|
||||
}
|
||||
}
|
||||
if (message.threadID == null && !targetRecipient.isOpenGroupRecipient) {
|
||||
// open group recipients should explicitly create threads
|
||||
message.threadID = getOrCreateThreadIdFor(targetAddress)
|
||||
}
|
||||
if (message.isMediaMessage() || attachments.isNotEmpty()) {
|
||||
val quote: Optional<QuoteModel> = if (quotes != null) Optional.of(quotes) else Optional.absent()
|
||||
val linkPreviews: Optional<List<LinkPreview>> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! })
|
||||
@ -162,7 +330,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
it.toSignalPointer()
|
||||
}
|
||||
val mediaMessage = IncomingMediaMessage.from(message, senderAddress, targetRecipient.expireMessages * 1000L, group, signalServiceAttachments, quote, linkPreviews)
|
||||
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate)
|
||||
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID!!, message.receivedTimestamp ?: 0, runThreadUpdate)
|
||||
}
|
||||
if (insertResult.isPresent) {
|
||||
messageID = insertResult.get().messageId
|
||||
@ -179,7 +347,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, senderAddress, message.sentTimestamp)
|
||||
else IncomingTextMessage.from(message, senderAddress, group, targetRecipient.expireMessages * 1000L)
|
||||
val encrypted = IncomingEncryptedMessage(textMessage, textMessage.messageBody)
|
||||
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate)
|
||||
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runThreadUpdate)
|
||||
}
|
||||
insertResult.orNull()?.let { result ->
|
||||
messageID = result.messageId
|
||||
@ -225,6 +393,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room, imageId)
|
||||
}
|
||||
|
||||
override fun getConfigSyncJob(destination: Destination): Job? {
|
||||
return DatabaseComponent.get(context).sessionJobDatabase().getAllJobs(ConfigurationSyncJob.KEY).values.firstOrNull {
|
||||
(it as? ConfigurationSyncJob)?.destination == destination
|
||||
}
|
||||
}
|
||||
|
||||
override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) {
|
||||
val job = DatabaseComponent.get(context).sessionJobDatabase().getMessageSendJob(messageSendJobID) ?: return
|
||||
JobQueue.shared.resumePendingSendMessage(job)
|
||||
@ -234,11 +408,201 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return DatabaseComponent.get(context).sessionJobDatabase().isJobCanceled(job)
|
||||
}
|
||||
|
||||
override fun cancelPendingMessageSendJobs(threadID: Long) {
|
||||
val jobDb = DatabaseComponent.get(context).sessionJobDatabase()
|
||||
jobDb.cancelPendingMessageSendJobs(threadID)
|
||||
}
|
||||
|
||||
override fun getAuthToken(room: String, server: String): String? {
|
||||
val id = "$server.$room"
|
||||
return DatabaseComponent.get(context).lokiAPIDatabase().getAuthToken(id)
|
||||
}
|
||||
|
||||
override fun notifyConfigUpdates(forConfigObject: ConfigBase) {
|
||||
notifyUpdates(forConfigObject)
|
||||
}
|
||||
|
||||
override fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean {
|
||||
return configFactory.conversationInConfig(publicKey, groupPublicKey, openGroupId, visibleOnly)
|
||||
}
|
||||
|
||||
override fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean {
|
||||
return configFactory.canPerformChange(variant, publicKey, changeTimestampMs)
|
||||
}
|
||||
|
||||
fun notifyUpdates(forConfigObject: ConfigBase) {
|
||||
when (forConfigObject) {
|
||||
is UserProfile -> updateUser(forConfigObject)
|
||||
is Contacts -> updateContacts(forConfigObject)
|
||||
is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject)
|
||||
is UserGroupsConfig -> updateUserGroups(forConfigObject)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUser(userProfile: UserProfile) {
|
||||
val userPublicKey = getUserPublicKey() ?: return
|
||||
// would love to get rid of recipient and context from this
|
||||
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
|
||||
// update name
|
||||
val name = userProfile.getName() ?: return
|
||||
val userPic = userProfile.getPic()
|
||||
val profileManager = SSKEnvironment.shared.profileManager
|
||||
if (name.isNotEmpty()) {
|
||||
TextSecurePreferences.setProfileName(context, name)
|
||||
profileManager.setName(context, recipient, name)
|
||||
}
|
||||
|
||||
// update pfp
|
||||
if (userPic == UserPic.DEFAULT) {
|
||||
clearUserPic()
|
||||
} else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty()
|
||||
&& TextSecurePreferences.getProfilePictureURL(context) != userPic.url) {
|
||||
setUserProfilePicture(userPic.url, userPic.key)
|
||||
}
|
||||
if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) {
|
||||
// delete nts thread if needed
|
||||
val ourThread = getThreadId(recipient) ?: return
|
||||
deleteConversation(ourThread)
|
||||
} else {
|
||||
// create note to self thread if needed (?)
|
||||
val ourThread = getOrCreateThreadIdFor(recipient.address)
|
||||
DatabaseComponent.get(context).threadDatabase().setHasSent(ourThread, true)
|
||||
setPinned(ourThread, userProfile.getNtsPriority() > 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun updateContacts(contacts: Contacts) {
|
||||
val extracted = contacts.all().toList()
|
||||
addLibSessionContacts(extracted)
|
||||
}
|
||||
|
||||
override fun clearUserPic() {
|
||||
val userPublicKey = getUserPublicKey() ?: return
|
||||
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
|
||||
// would love to get rid of recipient and context from this
|
||||
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
|
||||
// clear picture if userPic is null
|
||||
TextSecurePreferences.setProfileKey(context, null)
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, null)
|
||||
recipientDatabase.setProfileAvatar(recipient, null)
|
||||
TextSecurePreferences.setProfileAvatarId(context, 0)
|
||||
TextSecurePreferences.setProfilePictureURL(context, null)
|
||||
|
||||
Recipient.removeCached(fromSerialized(userPublicKey))
|
||||
configFactory.user?.setPic(UserPic.DEFAULT)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
}
|
||||
|
||||
private fun updateConvoVolatile(convos: ConversationVolatileConfig) {
|
||||
val extracted = convos.all()
|
||||
for (conversation in extracted) {
|
||||
val threadId = when (conversation) {
|
||||
is Conversation.OneToOne -> getThreadIdFor(conversation.sessionId, null, null, createThread = false)
|
||||
is Conversation.LegacyGroup -> getThreadIdFor("", conversation.groupId,null, createThread = false)
|
||||
is Conversation.Community -> getThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl.removeSuffix("/")}.${conversation.baseCommunityInfo.room}", createThread = false)
|
||||
}
|
||||
if (threadId != null) {
|
||||
if (conversation.lastRead > getLastSeen(threadId)) {
|
||||
markConversationAsRead(threadId, conversation.lastRead, force = true)
|
||||
}
|
||||
updateThread(threadId, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUserGroups(userGroups: UserGroupsConfig) {
|
||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||
val localUserPublicKey = getUserPublicKey() ?: return Log.w(
|
||||
"Loki",
|
||||
"No user public key when trying to update user groups from config"
|
||||
)
|
||||
val communities = userGroups.allCommunityInfo()
|
||||
val lgc = userGroups.allLegacyGroupInfo()
|
||||
val allOpenGroups = getAllOpenGroups()
|
||||
val toDeleteCommunities = allOpenGroups.filter {
|
||||
Conversation.Community(BaseCommunityInfo(it.value.server, it.value.room, it.value.publicKey), 0, false).baseCommunityInfo.fullUrl() !in communities.map { it.community.fullUrl() }
|
||||
}
|
||||
|
||||
val existingCommunities: Map<Long, OpenGroup> = allOpenGroups.filterKeys { it !in toDeleteCommunities.keys }
|
||||
val toAddCommunities = communities.filter { it.community.fullUrl() !in existingCommunities.map { it.value.joinURL } }
|
||||
val existingJoinUrls = existingCommunities.values.map { it.joinURL }
|
||||
|
||||
val existingClosedGroups = getAllGroups(includeInactive = true).filter { it.isClosedGroup }
|
||||
val lgcIds = lgc.map { it.sessionId }
|
||||
val toDeleteClosedGroups = existingClosedGroups.filter { group ->
|
||||
GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds
|
||||
}
|
||||
|
||||
// delete the ones which are not listed in the config
|
||||
toDeleteCommunities.values.forEach { openGroup ->
|
||||
OpenGroupManager.delete(openGroup.server, openGroup.room, context)
|
||||
}
|
||||
|
||||
toDeleteClosedGroups.forEach { deleteGroup ->
|
||||
val threadId = getThreadId(deleteGroup.encodedId)
|
||||
if (threadId != null) {
|
||||
ClosedGroupManager.silentlyRemoveGroup(context,threadId,GroupUtil.doubleDecodeGroupId(deleteGroup.encodedId), deleteGroup.encodedId, localUserPublicKey, delete = true)
|
||||
}
|
||||
}
|
||||
|
||||
toAddCommunities.forEach { toAddCommunity ->
|
||||
val joinUrl = toAddCommunity.community.fullUrl()
|
||||
if (!hasBackgroundGroupAddJob(joinUrl)) {
|
||||
JobQueue.shared.add(BackgroundGroupAddJob(joinUrl))
|
||||
}
|
||||
}
|
||||
|
||||
for (groupInfo in communities) {
|
||||
val groupBaseCommunity = groupInfo.community
|
||||
if (groupBaseCommunity.fullUrl() in existingJoinUrls) {
|
||||
// add it
|
||||
val (threadId, _) = existingCommunities.entries.first { (_, v) -> v.joinURL == groupInfo.community.fullUrl() }
|
||||
threadDb.setPinned(threadId, groupInfo.priority == PRIORITY_PINNED)
|
||||
}
|
||||
}
|
||||
|
||||
for (group in lgc) {
|
||||
val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.sessionId }
|
||||
val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) }
|
||||
if (existingGroup != null) {
|
||||
if (group.priority == PRIORITY_HIDDEN && existingThread != null) {
|
||||
ClosedGroupManager.silentlyRemoveGroup(context,existingThread,GroupUtil.doubleDecodeGroupId(existingGroup.encodedId), existingGroup.encodedId, localUserPublicKey, delete = true)
|
||||
} else if (existingThread == null) {
|
||||
Log.w("Loki-DBG", "Existing group had no thread to hide")
|
||||
} else {
|
||||
Log.d("Loki-DBG", "Setting existing group pinned status to ${group.priority}")
|
||||
threadDb.setPinned(existingThread, group.priority == PRIORITY_PINNED)
|
||||
}
|
||||
} else {
|
||||
val members = group.members.keys.map { Address.fromSerialized(it) }
|
||||
val admins = group.members.filter { it.value /*admin = true*/ }.keys.map { Address.fromSerialized(it) }
|
||||
val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId)
|
||||
val title = group.name
|
||||
val formationTimestamp = (group.joinedAt * 1000L)
|
||||
createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp)
|
||||
setProfileSharing(Address.fromSerialized(groupId), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
addClosedGroupPublicKey(group.sessionId)
|
||||
// Store the encryption key pair
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey))
|
||||
addClosedGroupEncryptionKeyPair(keyPair, group.sessionId, SnodeAPI.nowWithOffset)
|
||||
// Set expiration timer
|
||||
val expireTimer = group.disappearingTimer
|
||||
setExpirationTimer(groupId, expireTimer.toInt())
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, group.sessionId, localUserPublicKey)
|
||||
// Notify the user
|
||||
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
||||
threadDb.setDate(threadID, formationTimestamp)
|
||||
insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
|
||||
// Don't create config group here, it's from a config update
|
||||
// Start polling
|
||||
ClosedGroupPollerV2.shared.startPolling(group.sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAuthToken(room: String, server: String, newValue: String) {
|
||||
val id = "$server.$room"
|
||||
DatabaseComponent.get(context).lokiAPIDatabase().setAuthToken(id, newValue)
|
||||
@ -474,6 +838,59 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseComponent.get(context).groupDatabase().create(groupId, title, members, avatar, relay, admins, formationTimestamp)
|
||||
}
|
||||
|
||||
override fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map<String, Boolean>, formationTimestamp: Long, encryptionKeyPair: ECKeyPair) {
|
||||
val volatiles = configFactory.convoVolatile ?: return
|
||||
val userGroups = configFactory.userGroups ?: return
|
||||
val groupVolatileConfig = volatiles.getOrConstructLegacyGroup(groupPublicKey)
|
||||
groupVolatileConfig.lastRead = formationTimestamp
|
||||
volatiles.set(groupVolatileConfig)
|
||||
val groupInfo = GroupInfo.LegacyGroupInfo(
|
||||
sessionId = groupPublicKey,
|
||||
name = name,
|
||||
members = members,
|
||||
priority = ConfigBase.PRIORITY_VISIBLE,
|
||||
encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
|
||||
encSecKey = encryptionKeyPair.privateKey.serialize(),
|
||||
disappearingTimer = 0L,
|
||||
joinedAt = (formationTimestamp / 1000L)
|
||||
)
|
||||
// shouldn't exist, don't use getOrConstruct + copy
|
||||
userGroups.set(groupInfo)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
}
|
||||
|
||||
override fun updateGroupConfig(groupPublicKey: String) {
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val groupAddress = fromSerialized(groupID)
|
||||
// TODO: probably add a check in here for isActive?
|
||||
// TODO: also check if local user is a member / maybe run delete otherwise?
|
||||
val existingGroup = getGroup(groupID)
|
||||
?: return Log.w("Loki-DBG", "No existing group for ${groupPublicKey.take(4)}} when updating group config")
|
||||
val userGroups = configFactory.userGroups ?: return
|
||||
if (!existingGroup.isActive) {
|
||||
userGroups.eraseLegacyGroup(groupPublicKey)
|
||||
return
|
||||
}
|
||||
val name = existingGroup.title
|
||||
val admins = existingGroup.admins.map { it.serialize() }
|
||||
val members = existingGroup.members.map { it.serialize() }
|
||||
val membersMap = GroupUtil.createConfigMemberMap(admins = admins, members = members)
|
||||
val latestKeyPair = getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
?: return Log.w("Loki-DBG", "No latest closed group encryption key pair for ${groupPublicKey.take(4)}} when updating group config")
|
||||
val recipientSettings = getRecipientSettings(groupAddress) ?: return
|
||||
val threadID = getThreadId(groupAddress) ?: return
|
||||
val groupInfo = userGroups.getOrConstructLegacyGroupInfo(groupPublicKey).copy(
|
||||
name = name,
|
||||
members = membersMap,
|
||||
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
|
||||
encSecKey = latestKeyPair.privateKey.serialize(),
|
||||
priority = if (isPinned(threadID)) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
|
||||
disappearingTimer = recipientSettings.expireMessages.toLong(),
|
||||
joinedAt = (existingGroup.formationTimestamp / 1000L)
|
||||
)
|
||||
userGroups.set(groupInfo)
|
||||
}
|
||||
|
||||
override fun isGroupActive(groupPublicKey: String): Boolean {
|
||||
return DatabaseComponent.get(context).groupDatabase().getGroup(GroupUtil.doubleEncodeGroupID(groupPublicKey)).orNull()?.isActive == true
|
||||
}
|
||||
@ -504,7 +921,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
|
||||
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
|
||||
val smsDB = DatabaseComponent.get(context).smsDatabase()
|
||||
smsDB.insertMessageInbox(infoMessage, true, true)
|
||||
smsDB.insertMessageInbox(infoMessage, true)
|
||||
}
|
||||
|
||||
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) {
|
||||
@ -552,8 +969,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseComponent.get(context).lokiAPIDatabase().removeClosedGroupPublicKey(groupPublicKey)
|
||||
}
|
||||
|
||||
override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
|
||||
DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) {
|
||||
DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, timestamp)
|
||||
}
|
||||
|
||||
override fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) {
|
||||
@ -570,9 +987,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
.updateTimestampUpdated(groupID, updatedTimestamp)
|
||||
}
|
||||
|
||||
override fun setExpirationTimer(groupID: String, duration: Int) {
|
||||
val recipient = Recipient.from(context, fromSerialized(groupID), false)
|
||||
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
|
||||
override fun setExpirationTimer(address: String, duration: Int) {
|
||||
val recipient = Recipient.from(context, fromSerialized(address), false)
|
||||
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration)
|
||||
if (recipient.isContactRecipient && !recipient.isLocalNumber) {
|
||||
configFactory.contacts?.upsertContact(address) {
|
||||
this.expiryMode = if (duration != 0) {
|
||||
ExpiryMode.AfterRead(duration.toLong())
|
||||
} else { // = 0 / delete
|
||||
ExpiryMode.NONE
|
||||
}
|
||||
}
|
||||
if (configFactory.contacts?.needsPush() == true) {
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setServerCapabilities(server: String, capabilities: List<String>) {
|
||||
@ -591,16 +1020,29 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
OpenGroupManager.updateOpenGroup(openGroup, context)
|
||||
}
|
||||
|
||||
override fun getAllGroups(): List<GroupRecord> {
|
||||
return DatabaseComponent.get(context).groupDatabase().allGroups
|
||||
override fun getAllGroups(includeInactive: Boolean): List<GroupRecord> {
|
||||
return DatabaseComponent.get(context).groupDatabase().getAllGroups(includeInactive)
|
||||
}
|
||||
|
||||
override fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? {
|
||||
return OpenGroupManager.addOpenGroup(urlAsString, context)
|
||||
}
|
||||
|
||||
override fun onOpenGroupAdded(server: String) {
|
||||
override fun onOpenGroupAdded(server: String, room: String) {
|
||||
OpenGroupManager.restartPollerForServer(server.removeSuffix("/"))
|
||||
val groups = configFactory.userGroups ?: return
|
||||
val volatileConfig = configFactory.convoVolatile ?: return
|
||||
val openGroup = getOpenGroup(room, server) ?: return
|
||||
val (infoServer, infoRoom, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
|
||||
val pubKeyHex = Hex.toStringCondensed(pubKey)
|
||||
val communityInfo = groups.getOrConstructCommunityInfo(infoServer, infoRoom, pubKeyHex)
|
||||
groups.set(communityInfo)
|
||||
val volatile = volatileConfig.getOrConstructCommunity(infoServer, infoRoom, pubKey)
|
||||
if (volatile.lastRead != 0L) {
|
||||
val threadId = getThreadId(openGroup) ?: return
|
||||
markConversationAsRead(threadId, volatile.lastRead, force = true)
|
||||
}
|
||||
volatileConfig.set(volatile)
|
||||
}
|
||||
|
||||
override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean {
|
||||
@ -618,17 +1060,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
|
||||
}
|
||||
|
||||
override fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long {
|
||||
override fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? {
|
||||
val database = DatabaseComponent.get(context).threadDatabase()
|
||||
return if (!openGroupID.isNullOrEmpty()) {
|
||||
val recipient = Recipient.from(context, fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false)
|
||||
database.getThreadIdIfExistsFor(recipient)
|
||||
database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it }
|
||||
} else if (!groupPublicKey.isNullOrEmpty()) {
|
||||
val recipient = Recipient.from(context, fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false)
|
||||
database.getOrCreateThreadIdFor(recipient)
|
||||
if (createThread) database.getOrCreateThreadIdFor(recipient)
|
||||
else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it }
|
||||
} else {
|
||||
val recipient = Recipient.from(context, fromSerialized(publicKey), false)
|
||||
database.getOrCreateThreadIdFor(recipient)
|
||||
if (createThread) database.getOrCreateThreadIdFor(recipient)
|
||||
else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it }
|
||||
}
|
||||
}
|
||||
|
||||
@ -637,6 +1081,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return getThreadId(address)
|
||||
}
|
||||
|
||||
override fun getThreadId(openGroup: OpenGroup): Long? {
|
||||
return GroupManager.getOpenGroupThreadID("${openGroup.server.removeSuffix("/")}.${openGroup.room}", context)
|
||||
}
|
||||
|
||||
override fun getThreadId(address: Address): Long? {
|
||||
val recipient = Recipient.from(context, address, false)
|
||||
return getThreadId(recipient)
|
||||
@ -666,6 +1114,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
|
||||
override fun setContact(contact: Contact) {
|
||||
DatabaseComponent.get(context).sessionContactDatabase().setContact(contact)
|
||||
val address = fromSerialized(contact.sessionID)
|
||||
if (!getRecipientApproved(address)) return
|
||||
val recipientHash = SSKEnvironment.shared.profileManager.contactUpdatedInternal(contact)
|
||||
val recipient = Recipient.from(context, address, false)
|
||||
setRecipientHash(recipient, recipientHash)
|
||||
}
|
||||
|
||||
override fun getRecipientForThread(threadId: Long): Recipient? {
|
||||
@ -677,6 +1130,51 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return if (recipientSettings.isPresent) { recipientSettings.get() } else null
|
||||
}
|
||||
|
||||
override fun addLibSessionContacts(contacts: List<LibSessionContact>) {
|
||||
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<ConfigurationMessage.Contact>) {
|
||||
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
|
||||
val threadDatabase = DatabaseComponent.get(context).threadDatabase()
|
||||
@ -700,19 +1198,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
recipientDatabase.setProfileSharing(recipient, true)
|
||||
recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED)
|
||||
// create Thread if needed
|
||||
val threadId = threadDatabase.getOrCreateThreadIdFor(recipient)
|
||||
val threadId = threadDatabase.getThreadIdIfExistsFor(recipient)
|
||||
if (contact.didApproveMe == true) {
|
||||
recipientDatabase.setApprovedMe(recipient, true)
|
||||
}
|
||||
if (contact.isApproved == true) {
|
||||
recipientDatabase.setApproved(recipient, true)
|
||||
if (contact.isApproved == true && threadId != -1L) {
|
||||
setRecipientApproved(recipient, true)
|
||||
threadDatabase.setHasSent(threadId, true)
|
||||
}
|
||||
|
||||
val contactIsBlocked: Boolean? = contact.isBlocked
|
||||
if (contactIsBlocked != null && recipient.isBlocked != contactIsBlocked) {
|
||||
recipientDatabase.setBlocked(recipient, contactIsBlocked)
|
||||
threadDatabase.deleteConversation(threadId)
|
||||
setBlocked(listOf(recipient), contactIsBlocked, fromConfigUpdate = true)
|
||||
}
|
||||
}
|
||||
if (contacts.isNotEmpty()) {
|
||||
@ -720,6 +1217,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
}
|
||||
}
|
||||
|
||||
override fun setRecipientHash(recipient: Recipient, recipientHash: String?) {
|
||||
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
|
||||
recipientDb.setRecipientHash(recipient, recipientHash)
|
||||
}
|
||||
|
||||
override fun getLastUpdated(threadID: Long): Long {
|
||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||
return threadDB.getLastUpdated(threadID)
|
||||
@ -740,12 +1242,78 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return mmsSmsDb.getConversationCount(threadID)
|
||||
}
|
||||
|
||||
override fun deleteConversation(threadId: Long) {
|
||||
override fun setPinned(threadID: Long, isPinned: Boolean) {
|
||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||
threadDB.deleteConversation(threadId)
|
||||
threadDB.setPinned(threadID, isPinned)
|
||||
val threadRecipient = getRecipientForThread(threadID) ?: return
|
||||
if (threadRecipient.isLocalNumber) {
|
||||
val user = configFactory.user ?: return
|
||||
user.setNtsPriority(if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE)
|
||||
} else if (threadRecipient.isContactRecipient) {
|
||||
val contacts = configFactory.contacts ?: return
|
||||
contacts.upsertContact(threadRecipient.address.serialize()) {
|
||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
||||
}
|
||||
} else if (threadRecipient.isGroupRecipient) {
|
||||
val groups = configFactory.userGroups ?: return
|
||||
if (threadRecipient.isClosedGroupRecipient) {
|
||||
val sessionId = GroupUtil.doubleDecodeGroupId(threadRecipient.address.serialize())
|
||||
val newGroupInfo = groups.getOrConstructLegacyGroupInfo(sessionId).copy (
|
||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
||||
)
|
||||
groups.set(newGroupInfo)
|
||||
} else if (threadRecipient.isOpenGroupRecipient) {
|
||||
val openGroup = getOpenGroup(threadID) ?: return
|
||||
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
|
||||
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
|
||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
||||
)
|
||||
groups.set(newGroupInfo)
|
||||
}
|
||||
}
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
}
|
||||
|
||||
override fun isPinned(threadID: Long): Boolean {
|
||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||
return threadDB.isPinned(threadID)
|
||||
}
|
||||
|
||||
override fun setThreadDate(threadId: Long, newDate: Long) {
|
||||
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||
threadDb.setDate(threadId, newDate)
|
||||
}
|
||||
|
||||
override fun deleteConversation(threadID: Long) {
|
||||
val recipient = getRecipientForThread(threadID)
|
||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||
val groupDB = DatabaseComponent.get(context).groupDatabase()
|
||||
threadDB.deleteConversation(threadID)
|
||||
if (recipient != null) {
|
||||
if (recipient.isContactRecipient) {
|
||||
if (recipient.isLocalNumber) return
|
||||
val contacts = configFactory.contacts ?: return
|
||||
contacts.upsertContact(recipient.address.serialize()) {
|
||||
this.priority = PRIORITY_HIDDEN
|
||||
}
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
} else if (recipient.isClosedGroupRecipient) {
|
||||
// TODO: handle closed group
|
||||
val volatile = configFactory.convoVolatile ?: return
|
||||
val groups = configFactory.userGroups ?: return
|
||||
val groupID = recipient.address.toGroupString()
|
||||
val closedGroup = getGroup(groupID)
|
||||
val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
|
||||
if (closedGroup != null) {
|
||||
groupDB.delete(groupID) // TODO: Should we delete the group? (seems odd to leave it)
|
||||
volatile.eraseLegacyClosedGroup(groupPublicKey)
|
||||
groups.eraseLegacyGroup(groupPublicKey)
|
||||
} else {
|
||||
Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
|
||||
return PartAuthority.getAttachmentDataUri(attachmentId)
|
||||
@ -762,6 +1330,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
|
||||
if (recipient.isBlocked) return
|
||||
|
||||
val threadId = getThreadId(recipient) ?: return
|
||||
|
||||
val mediaMessage = IncomingMediaMessage(
|
||||
address,
|
||||
sentTimestamp,
|
||||
@ -780,14 +1350,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
Optional.of(message)
|
||||
)
|
||||
|
||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1, runIncrement = true, runThreadUpdate = true)
|
||||
database.insertSecureDecryptedMessageInbox(mediaMessage, threadId, runThreadUpdate = true)
|
||||
}
|
||||
|
||||
override fun insertMessageRequestResponse(response: MessageRequestResponse) {
|
||||
val userPublicKey = getUserPublicKey()
|
||||
val senderPublicKey = response.sender!!
|
||||
val recipientPublicKey = response.recipient!!
|
||||
if (userPublicKey == null || (userPublicKey != recipientPublicKey && userPublicKey != senderPublicKey)) return
|
||||
|
||||
if (
|
||||
userPublicKey == null
|
||||
|| (userPublicKey != recipientPublicKey && userPublicKey != senderPublicKey)
|
||||
// this is true if it is a sync message
|
||||
|| (userPublicKey == recipientPublicKey && userPublicKey == senderPublicKey)
|
||||
) return
|
||||
|
||||
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
|
||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
@ -799,7 +1376,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
|
||||
val smsDb = DatabaseComponent.get(context).smsDatabase()
|
||||
val sender = Recipient.from(context, fromSerialized(senderPublicKey), false)
|
||||
val threadId = threadDB.getOrCreateThreadIdFor(sender)
|
||||
val threadId = getOrCreateThreadIdFor(sender.address)
|
||||
val profile = response.profile
|
||||
if (profile != null) {
|
||||
val profileManager = SSKEnvironment.shared.profileManager
|
||||
@ -814,9 +1391,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val profileKeyChanged = (sender.profileKey == null || !MessageDigest.isEqual(sender.profileKey, newProfileKey))
|
||||
|
||||
if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) {
|
||||
profileManager.setProfileKey(context, sender, newProfileKey!!)
|
||||
profileManager.setProfilePicture(context, sender, profile.profilePictureURL!!, newProfileKey!!)
|
||||
profileManager.setUnidentifiedAccessMode(context, sender, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
||||
profileManager.setProfilePictureURL(context, sender, profile.profilePictureURL!!)
|
||||
}
|
||||
}
|
||||
threadDB.setHasSent(threadId, true)
|
||||
@ -873,16 +1449,28 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
Optional.absent(),
|
||||
Optional.absent()
|
||||
)
|
||||
mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runIncrement = true, runThreadUpdate = true)
|
||||
mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runThreadUpdate = true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRecipientApproved(address: Address): Boolean {
|
||||
return DatabaseComponent.get(context).recipientDatabase().getApproved(address)
|
||||
}
|
||||
|
||||
override fun setRecipientApproved(recipient: Recipient, approved: Boolean) {
|
||||
DatabaseComponent.get(context).recipientDatabase().setApproved(recipient, approved)
|
||||
if (recipient.isLocalNumber || !recipient.isContactRecipient) return
|
||||
configFactory.contacts?.upsertContact(recipient.address.serialize()) {
|
||||
this.approved = approved
|
||||
}
|
||||
}
|
||||
|
||||
override fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) {
|
||||
DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe)
|
||||
if (recipient.isLocalNumber || !recipient.isContactRecipient) return
|
||||
configFactory.contacts?.upsertContact(recipient.address.serialize()) {
|
||||
this.approvedMe = approvedMe
|
||||
}
|
||||
}
|
||||
|
||||
override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) {
|
||||
@ -1012,9 +1600,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms))
|
||||
}
|
||||
|
||||
override fun unblock(toUnblock: Iterable<Recipient>) {
|
||||
override fun setBlocked(recipients: Iterable<Recipient>, 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<Recipient> {
|
||||
|
@ -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<Long, Address> 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<Long> 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<MarkedMessageInfo> setRead(long threadId, long lastReadTime) {
|
||||
|
||||
final List<MarkedMessageInfo> smsRecords = DatabaseComponent.get(context).smsDatabase().setMessagesRead(threadId, lastReadTime);
|
||||
final List<MarkedMessageInfo> 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<MarkedMessageInfo>() {{
|
||||
addAll(smsRecords);
|
||||
addAll(mmsRecords);
|
||||
}};
|
||||
}
|
||||
|
||||
public List<MarkedMessageInfo> 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) {
|
||||
/**
|
||||
* @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);
|
||||
if (timestamp == -1) {
|
||||
contentValues.put(LAST_SEEN, SnodeAPI.getNowWithOffset());
|
||||
} else {
|
||||
contentValues.put(LAST_SEEN, timestamp);
|
||||
}
|
||||
|
||||
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<Long, Boolean> 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<MarkedMessageInfo> 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<MarkedMessageInfo> 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) {
|
||||
|
@ -11,7 +11,6 @@ import androidx.core.app.NotificationCompat;
|
||||
import net.zetetic.database.sqlcipher.SQLiteConnection;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
|
||||
import net.zetetic.database.sqlcipher.SQLiteException;
|
||||
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
@ -19,6 +18,7 @@ import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.BlindedIdMappingDatabase;
|
||||
import org.thoughtcrime.securesms.database.ConfigDatabase;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.database.SessionJobDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@ -85,9 +86,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV38 = 59;
|
||||
private static final int lokiV39 = 60;
|
||||
private static final int lokiV40 = 61;
|
||||
private static final int lokiV41 = 62;
|
||||
private static final int lokiV42 = 63;
|
||||
|
||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final int DATABASE_VERSION = lokiV40;
|
||||
private static final int DATABASE_VERSION = lokiV42;
|
||||
private static final int MIN_DATABASE_VERSION = lokiV7;
|
||||
private static final String CIPHER3_DATABASE_NAME = "signal.db";
|
||||
public static final String DATABASE_NAME = "signal_v4.db";
|
||||
@ -147,7 +150,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
|
||||
}
|
||||
|
||||
private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) throws SQLiteException {
|
||||
private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) {
|
||||
return SQLiteDatabase.openDatabase(path, databaseSecret.asString(), null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() {
|
||||
@Override
|
||||
public void preKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); }
|
||||
@ -340,6 +343,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(ThreadDatabase.getUnreadMentionCountCommand());
|
||||
db.execSQL(SmsDatabase.CREATE_HAS_MENTION_COMMAND);
|
||||
db.execSQL(MmsDatabase.CREATE_HAS_MENTION_COMMAND);
|
||||
db.execSQL(ConfigDatabase.CREATE_CONFIG_TABLE_COMMAND);
|
||||
|
||||
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
||||
@ -351,6 +355,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
executeStatements(db, ReactionDatabase.CREATE_INDEXS);
|
||||
|
||||
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
|
||||
db.execSQL(RecipientDatabase.getAddWrapperHash());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -583,6 +588,16 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(MmsDatabase.CREATE_HAS_MENTION_COMMAND);
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV41) {
|
||||
db.execSQL(ConfigDatabase.CREATE_CONFIG_TABLE_COMMAND);
|
||||
db.execSQL(ConfigurationMessageUtilities.DELETE_INACTIVE_GROUPS);
|
||||
db.execSQL(ConfigurationMessageUtilities.DELETE_INACTIVE_ONE_TO_ONES);
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV42) {
|
||||
db.execSQL(RecipientDatabase.getAddWrapperHash());
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
@ -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
|
||||
@ -20,3 +21,9 @@ abstract class AppModule {
|
||||
abstract fun bindConversationRepository(repository: DefaultConversationRepository): ConversationRepository
|
||||
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface AppComponent {
|
||||
fun getPrefs(): TextSecurePreferences
|
||||
}
|
@ -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<ByteArray, String>?
|
||||
) :
|
||||
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 = TextSecurePreferences.hasForcedNewConfig(context)
|
||||
|
||||
private val listeners: MutableList<ConfigFactoryUpdateListener> = mutableListOf()
|
||||
fun registerListener(listener: ConfigFactoryUpdateListener) {
|
||||
listeners += listener
|
||||
}
|
||||
|
||||
fun unregisterListener(listener: ConfigFactoryUpdateListener) {
|
||||
listeners -= listener
|
||||
}
|
||||
|
||||
private inline fun <T> 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<ConfigBase> =
|
||||
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))
|
||||
}
|
||||
}
|
@ -45,4 +45,5 @@ interface DatabaseComponent {
|
||||
fun attachmentProvider(): MessageDataProvider
|
||||
fun blindedIdMappingDatabase(): BlindedIdMappingDatabase
|
||||
fun groupMemberDatabase(): GroupMemberDatabase
|
||||
fun configDatabase(): ConfigDatabase
|
||||
}
|
@ -6,7 +6,6 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
@ -132,10 +131,18 @@ object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = Storage(context,openHelper)
|
||||
fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper, configFactory: ConfigFactory, threadDatabase: ThreadDatabase): Storage {
|
||||
val storage = Storage(context,openHelper, configFactory)
|
||||
threadDatabase.setUpdateListener(storage)
|
||||
return storage
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAttachmentProvider(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper): MessageDataProvider = DatabaseAttachmentProvider(context, openHelper)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConfigDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper): ConfigDatabase = ConfigDatabase(context, openHelper)
|
||||
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
package org.thoughtcrime.securesms.dependencies;
|
||||
|
||||
public interface InjectableType {
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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<String>()
|
||||
private val zombies = HashSet<String>()
|
||||
private val members = HashSet<String>()
|
||||
@ -289,7 +302,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
isLoading = true
|
||||
loaderContainer.fadeIn()
|
||||
val promise: Promise<Any, Exception> = 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<String>, val zombieMembers: List<String>) { }
|
||||
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<String>, val zombieMembers: List<String>)
|
||||
}
|
@ -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) {
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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<Long,OpenGroupApi.RoomInfo?> {
|
||||
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
|
||||
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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,18 +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.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
|
||||
@ -26,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
|
||||
@ -48,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
|
||||
@ -60,6 +69,7 @@ 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
|
||||
@ -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<GlobalSearchViewModel>()
|
||||
private val homeViewModel by viewModels<HomeViewModel>()
|
||||
@ -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,6 +175,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
binding.sessionToolbar.disableClipping()
|
||||
// Set up seed reminder view
|
||||
lifecycleScope.launchWhenStarted {
|
||||
val hasViewedSeed = textSecurePreferences.getHasViewedSeed()
|
||||
if (!hasViewedSeed) {
|
||||
binding.seedReminderView.isVisible = true
|
||||
@ -168,6 +186,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
} else {
|
||||
binding.seedReminderView.isVisible = false
|
||||
}
|
||||
}
|
||||
setupMessageRequestsBanner()
|
||||
// Set up recycler view
|
||||
binding.globalSearchInputLayout.listener = this
|
||||
@ -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)
|
||||
@ -493,9 +545,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
|
||||
button(R.string.RecipientPreferenceActivity_block) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setBlocked(thread.recipient, true)
|
||||
// TODO: Remove in UserConfig branch
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@HomeActivity)
|
||||
storage.setBlocked(listOf(thread.recipient), true)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
@ -511,9 +562,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
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) {
|
||||
recipientDatabase.setBlocked(thread.recipient, false)
|
||||
// TODO: Remove in UserConfig branch
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@HomeActivity)
|
||||
storage.setBlocked(listOf(thread.recipient), false)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<RecyclerView.ViewHolder>(), 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)
|
||||
|
@ -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<ThreadRecord>,
|
||||
private val new: List<ThreadRecord>,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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,7 +94,10 @@ class PathStatusView : View {
|
||||
private fun handlePathsBuiltEvent() { update() }
|
||||
|
||||
private fun update() {
|
||||
if (OnionRequestAPI.paths.isNotEmpty()) {
|
||||
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
|
||||
@ -99,6 +109,8 @@ class PathStatusView : View {
|
||||
sessionShadowColor = pathsBuildingColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(c: Canvas) {
|
||||
val w = width.toFloat()
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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() {
|
||||
@ -113,9 +113,9 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat
|
||||
binding.clearAllMessageRequestsButton.isVisible = threadCount != 0
|
||||
}
|
||||
|
||||
private fun deleteAllAndBlock() {
|
||||
private fun deleteAll() {
|
||||
fun doDeleteAllAndBlock() {
|
||||
viewModel.clearAllMessageRequests()
|
||||
viewModel.clearAllMessageRequests(false)
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity)
|
||||
|
@ -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!!)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<MarkedMessageInfo> 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;
|
||||
}
|
||||
|
@ -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<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
List<MarkedMessageInfo> messageIdsCollection = new LinkedList<>();
|
||||
|
||||
long currentTime = SnodeAPI.getNowWithOffset();
|
||||
for (long threadId : threadIds) {
|
||||
Log.i(TAG, "Marking as read: " + threadId);
|
||||
List<MarkedMessageInfo> 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);
|
||||
|
@ -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 {
|
||||
|
@ -164,6 +164,7 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -22,7 +22,7 @@ class BlockedContactsActivity: PassphraseRequiredActionBarActivity() {
|
||||
showSessionDialog {
|
||||
title(viewModel.getTitle(this@BlockedContactsActivity))
|
||||
text(viewModel.getMessage(this@BlockedContactsActivity))
|
||||
button(R.string.continue_2) { viewModel.unblock(this@BlockedContactsActivity) }
|
||||
button(R.string.continue_2) { viewModel.unblock() }
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
@ -17,14 +20,18 @@ import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
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
|
||||
@ -32,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
|
||||
@ -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,28 +219,37 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
val promises = mutableListOf<Promise<*, Exception>>()
|
||||
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 (userConfig != null && userConfig.needsDump()) {
|
||||
configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
|
||||
}
|
||||
if (profilePicture != null || displayName != null) {
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
||||
}
|
||||
}
|
||||
compoundPromise.alwaysUi {
|
||||
if (displayName != null) {
|
||||
binding.btnGroupNameDisplay.text = displayName
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -23,9 +23,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
|
||||
@ -62,7 +64,7 @@ interface ConversationRepository {
|
||||
|
||||
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit>
|
||||
|
||||
suspend fun clearAllMessageRequests(): ResultOf<Unit>
|
||||
suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit>
|
||||
|
||||
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||
|
||||
@ -82,8 +84,10 @@ class DefaultConversationRepository @Inject constructor(
|
||||
private val mmsDb: MmsDatabase,
|
||||
private val mmsSmsDb: MmsSmsDatabase,
|
||||
private val recipientDb: RecipientDatabase,
|
||||
private val storage: Storage,
|
||||
private val lokiMessageDb: LokiMessageDatabase,
|
||||
private val sessionJobDb: SessionJobDatabase
|
||||
private val sessionJobDb: SessionJobDatabase,
|
||||
private val configFactory: ConfigFactory
|
||||
) : ConversationRepository {
|
||||
|
||||
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
|
||||
@ -125,8 +129,9 @@ class DefaultConversationRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// This assumes that recipient.isContactRecipient is true
|
||||
override fun setBlocked(recipient: Recipient, blocked: Boolean) {
|
||||
recipientDb.setBlocked(recipient, blocked)
|
||||
storage.setBlocked(listOf(recipient), blocked)
|
||||
}
|
||||
|
||||
override fun deleteLocally(recipient: Recipient, message: MessageRecord) {
|
||||
@ -139,7 +144,7 @@ class DefaultConversationRepository @Inject constructor(
|
||||
}
|
||||
|
||||
override fun setApproved(recipient: Recipient, isApproved: Boolean) {
|
||||
recipientDb.setApproved(recipient, isApproved)
|
||||
storage.setRecipientApproved(recipient, isApproved)
|
||||
}
|
||||
|
||||
override suspend fun deleteForEveryone(
|
||||
@ -250,29 +255,33 @@ class DefaultConversationRepository @Inject constructor(
|
||||
|
||||
override suspend fun deleteThread(threadId: Long): ResultOf<Unit> {
|
||||
sessionJobDb.cancelPendingMessageSendJobs(threadId)
|
||||
threadDb.deleteConversation(threadId)
|
||||
storage.deleteConversation(threadId)
|
||||
return ResultOf.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit> {
|
||||
sessionJobDb.cancelPendingMessageSendJobs(thread.threadId)
|
||||
threadDb.deleteConversation(thread.threadId)
|
||||
storage.deleteConversation(thread.threadId)
|
||||
return ResultOf.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun clearAllMessageRequests(): ResultOf<Unit> {
|
||||
override suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit> {
|
||||
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<Unit> = suspendCoroutine { continuation ->
|
||||
recipientDb.setApproved(recipient, true)
|
||||
storage.setRecipientApproved(recipient, true)
|
||||
val message = MessageRequestResponse(true)
|
||||
MessageSender.send(message, Destination.from(recipient.address))
|
||||
MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = recipient.isLocalNumber)
|
||||
.success {
|
||||
threadDb.setHasSent(threadId, true)
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
@ -283,7 +292,7 @@ class DefaultConversationRepository @Inject constructor(
|
||||
|
||||
override fun declineMessageRequest(threadId: Long) {
|
||||
sessionJobDb.cancelPendingMessageSendJobs(threadId)
|
||||
threadDb.deleteConversation(threadId)
|
||||
storage.deleteConversation(threadId)
|
||||
}
|
||||
|
||||
override fun hasReceived(threadId: Long): Boolean {
|
||||
|
@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.service;
|
||||
import android.content.Context;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.session.libsession.database.StorageProtocol;
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
|
||||
@ -15,6 +17,7 @@ import org.session.libsignal.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
@ -35,12 +38,14 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
|
||||
private final SmsDatabase smsDatabase;
|
||||
private final MmsDatabase mmsDatabase;
|
||||
private final MmsSmsDatabase mmsSmsDatabase;
|
||||
private final Context context;
|
||||
|
||||
public ExpiringMessageManager(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.smsDatabase = DatabaseComponent.get(context).smsDatabase();
|
||||
this.mmsDatabase = DatabaseComponent.get(context).mmsDatabase();
|
||||
this.mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
||||
|
||||
executor.execute(new LoadTask());
|
||||
executor.execute(new ProcessTask());
|
||||
@ -79,12 +84,11 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
}
|
||||
|
||||
if (message.getId() != null) {
|
||||
DatabaseComponent.get(context).smsDatabase().deleteMessage(message.getId());
|
||||
smsDatabase.deleteMessage(message.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void insertIncomingExpirationTimerMessage(ExpirationTimerUpdate message) {
|
||||
MmsDatabase database = DatabaseComponent.get(context).mmsDatabase();
|
||||
|
||||
String senderPublicKey = message.getSender();
|
||||
Long sentTimestamp = message.getSentTimestamp();
|
||||
@ -106,6 +110,10 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
Address groupAddress = Address.fromSerialized(groupID);
|
||||
recipient = Recipient.from(context, groupAddress, false);
|
||||
}
|
||||
Long threadId = MessagingModuleConfiguration.getShared().getStorage().getThreadId(recipient);
|
||||
if (threadId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1,
|
||||
duration * 1000L, true,
|
||||
@ -120,10 +128,10 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
//insert the timer update message
|
||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1, true, true);
|
||||
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, threadId, true);
|
||||
|
||||
//set the timer to the conversation
|
||||
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
|
||||
MessagingModuleConfiguration.getShared().getStorage().setExpirationTimer(recipient.getAddress().serialize(), duration);
|
||||
|
||||
} catch (IOException | MmsException ioe) {
|
||||
Log.e("Loki", "Failed to insert expiration update message.");
|
||||
@ -131,28 +139,30 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
}
|
||||
|
||||
private void insertOutgoingExpirationTimerMessage(ExpirationTimerUpdate message) {
|
||||
MmsDatabase database = DatabaseComponent.get(context).mmsDatabase();
|
||||
|
||||
Long sentTimestamp = message.getSentTimestamp();
|
||||
String groupId = message.getGroupPublicKey();
|
||||
int duration = message.getDuration();
|
||||
|
||||
Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : message.getRecipient());
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
Address address;
|
||||
|
||||
try {
|
||||
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId);
|
||||
database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp, true);
|
||||
|
||||
if (groupId != null) {
|
||||
// we need the group ID as recipient for setExpireMessages below
|
||||
recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId)), false);
|
||||
address = Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId));
|
||||
} else {
|
||||
address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : message.getRecipient());
|
||||
}
|
||||
//set the timer to the conversation
|
||||
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
|
||||
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
StorageProtocol storage = MessagingModuleConfiguration.getShared().getStorage();
|
||||
message.setThreadID(storage.getOrCreateThreadIdFor(address));
|
||||
|
||||
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId);
|
||||
mmsDatabase.insertSecureDecryptedMessageOutbox(timerUpdateMessage, message.getThreadID(), sentTimestamp, true);
|
||||
//set the timer to the conversation
|
||||
MessagingModuleConfiguration.getShared().getStorage().setExpirationTimer(recipient.getAddress().serialize(), duration);
|
||||
} catch (MmsException | IOException ioe) {
|
||||
Log.e("Loki", "Failed to insert expiration update message.");
|
||||
Log.e("Loki", "Failed to insert expiration update message.", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,7 +173,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
|
||||
@Override
|
||||
public void startAnyExpiration(long timestamp, @NotNull String author) {
|
||||
MessageRecord messageRecord = DatabaseComponent.get(context).mmsSmsDatabase().getMessageFor(timestamp, author);
|
||||
MessageRecord messageRecord = mmsSmsDatabase.getMessageFor(timestamp, author);
|
||||
if (messageRecord != null) {
|
||||
boolean mms = messageRecord.isMms();
|
||||
Recipient recipient = messageRecord.getRecipient();
|
||||
|
@ -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)
|
||||
}
|
||||
contactUpdatedInternal(contact)
|
||||
if (!hasPendingDownload) {
|
||||
val job = RetrieveProfileAvatarJob(profilePictureURL, recipient.address)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// 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()
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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<Unit, Exception> {
|
||||
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()
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="false" android:color="@color/grey"/>
|
||||
<item android:state_enabled="false" android:color="@color/gray50"/>
|
||||
<item android:color="?prominentButtonColor"/>
|
||||
</selector>
|
@ -216,6 +216,19 @@
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:padding="@dimen/medium_spacing"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textAlignment="center"
|
||||
android:id="@+id/placeholderText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/blockedBanner"
|
||||
android:elevation="8dp"
|
||||
tools:text="@string/activity_conversation_empty_state_default"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/messageRequestBar"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -27,8 +27,9 @@
|
||||
android:layout_marginLeft="20dp"
|
||||
android:layout_marginRight="20dp">
|
||||
|
||||
<include layout="@layout/view_profile_picture"
|
||||
<include
|
||||
android:id="@+id/profileButton"
|
||||
layout="@layout/view_profile_picture"
|
||||
android:layout_width="@dimen/small_profile_picture_size"
|
||||
android:layout_height="@dimen/small_profile_picture_size"
|
||||
android:layout_alignParentLeft="true"
|
||||
@ -38,10 +39,10 @@
|
||||
|
||||
<org.thoughtcrime.securesms.home.PathStatusView
|
||||
android:id="@+id/pathStatusView"
|
||||
android:layout_alignBottom="@+id/profileButton"
|
||||
android:layout_alignEnd="@+id/profileButton"
|
||||
android:layout_width="@dimen/path_status_view_size"
|
||||
android:layout_height="@dimen/path_status_view_size"/>
|
||||
android:layout_height="@dimen/path_status_view_size"
|
||||
android:layout_alignEnd="@+id/profileButton"
|
||||
android:layout_alignBottom="@+id/profileButton" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sessionHeaderImage"
|
||||
@ -75,16 +76,17 @@
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:visibility="gone"
|
||||
android:id="@+id/search_toolbar"
|
||||
android:layout_marginHorizontal="@dimen/medium_spacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize">
|
||||
android:layout_height="?actionBarSize"
|
||||
android:layout_marginHorizontal="@dimen/medium_spacing"
|
||||
android:visibility="gone">
|
||||
|
||||
<org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout
|
||||
android:layout_centerVertical="true"
|
||||
android:id="@+id/globalSearchInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true" />
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
@ -94,19 +96,48 @@
|
||||
android:elevation="1dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.onboarding.SeedReminderView
|
||||
tools:visibility="gone"
|
||||
android:id="@+id/seedReminderView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<FrameLayout
|
||||
tools:visibility="visible"
|
||||
android:visibility="gone"
|
||||
android:id="@+id/configOutdatedView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:layout_gravity="center"
|
||||
android:textColor="?message_sent_text_color"
|
||||
android:background="?colorAccent"
|
||||
android:textSize="9sp"
|
||||
android:paddingVertical="4dp"
|
||||
android:paddingHorizontal="64dp"
|
||||
android:gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/activity_home_outdated_client_config"
|
||||
/>
|
||||
<ImageView
|
||||
android:layout_margin="@dimen/small_spacing"
|
||||
android:layout_gravity="center_vertical|right"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_x"
|
||||
app:tint="@color/black" />
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<RelativeLayout
|
||||
android:focusable="false"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false">
|
||||
android:clipChildren="false"
|
||||
android:focusable="false">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
@ -119,14 +150,14 @@
|
||||
tools:listitem="@layout/view_conversation" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:visibility="gone"
|
||||
android:scrollbars="vertical"
|
||||
android:id="@+id/globalSearchRecycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/view_global_search_result"
|
||||
tools:itemCount="6"
|
||||
android:layout_height="match_parent"/>
|
||||
tools:listitem="@layout/view_global_search_result" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/emptyStateContainer"
|
||||
|
@ -52,6 +52,7 @@
|
||||
android:text="Spiderman" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/nameEditIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="22dp"
|
||||
android:contentDescription="@string/AccessibilityId_edit_user_nickname"
|
||||
|
@ -16,6 +16,7 @@
|
||||
android:layout_height="@dimen/small_profile_picture_size"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/profile_picture_view_small_background" />
|
||||
|
||||
<ImageView
|
||||
@ -24,11 +25,13 @@
|
||||
android:layout_height="@dimen/small_profile_picture_size"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/profile_picture_view_small_background" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageView
|
||||
android:scaleType="centerCrop"
|
||||
android:id="@+id/singleModeImageView"
|
||||
android:layout_width="@dimen/medium_profile_picture_size"
|
||||
android:layout_height="@dimen/medium_profile_picture_size"
|
||||
@ -36,6 +39,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/largeSingleModeImageView"
|
||||
android:scaleType="centerCrop"
|
||||
android:layout_width="@dimen/large_profile_picture_size"
|
||||
android:layout_height="@dimen/large_profile_picture_size"
|
||||
android:background="@drawable/profile_picture_view_large_background" />
|
||||
|
@ -8,6 +8,46 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/unreadMarkerContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/small_spacing"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:layout_marginEnd="@dimen/small_spacing"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/unreadMarker"
|
||||
android:background="?android:colorAccent" />
|
||||
<TextView
|
||||
android:id="@+id/unreadMarker"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:text="@string/unread_marker"
|
||||
android:gravity="center"
|
||||
android:textColor="?android:colorAccent"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textStyle="bold" />
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="@dimen/small_spacing"
|
||||
android:layout_marginEnd="@dimen/medium_spacing"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/unreadMarker"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:background="?android:colorAccent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dateBreakTextView"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -7,7 +7,7 @@
|
||||
android:id="@+id/mainContainerConstraint"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<org.thoughtcrime.securesms.util.MessageBubbleView
|
||||
android:id="@+id/contentParent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@ -111,7 +111,7 @@
|
||||
android:id="@+id/bodyTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</org.thoughtcrime.securesms.util.MessageBubbleView>
|
||||
|
||||
<include layout="@layout/album_thumbnail_view"
|
||||
android:visibility="gone"
|
||||
|
@ -1020,4 +1020,12 @@
|
||||
<string name="delivery_status_failed">Failed to send</string>
|
||||
<string name="giphy_permission_title">Search GIFs?</string>
|
||||
<string name="giphy_permission_message">Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.</string>
|
||||
<string name="activity_home_outdated_client_config">Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.</string>
|
||||
|
||||
<string name="activity_conversation_empty_state_read_only">There are no messages in <b>%s</b>.</string>
|
||||
<string name="activity_conversation_empty_state_note_to_self">You have no messages in Note to Self.</string>
|
||||
<string name="activity_conversation_empty_state_default">You have no messages from <b>%s</b>.\nSend a message to start the conversation!</string>
|
||||
|
||||
<string name="unread_marker">Unread Messages</string>
|
||||
|
||||
</resources>
|
||||
|
@ -642,4 +642,84 @@
|
||||
<item name="message_selected">@color/ocean_light_5</item>
|
||||
</style>
|
||||
|
||||
<!-- For testing / XML rendering -->
|
||||
<style name="Theme.Session.DayNight.NoActionBar.Test" parent="Base.Theme.Session">
|
||||
<item name="dividerVertical">?android:textColorTertiary</item>
|
||||
<item name="dividerHorizontal">?dividerVertical</item>
|
||||
<item name="colorAccent">@color/classic_accent</item>
|
||||
|
||||
<item name="sessionLogoTint">@color/classic_dark_6</item>
|
||||
<item name="colorPrimary">@color/classic_dark_0</item>
|
||||
<item name="colorPrimaryDark">@color/classic_dark_0</item>
|
||||
<item name="colorControlNormal">?android:textColorPrimary</item>
|
||||
<item name="colorControlActivated">?colorAccent</item>
|
||||
<item name="android:colorControlHighlight">?colorAccent</item>
|
||||
<item name="android:textColorPrimary">@color/classic_dark_6</item>
|
||||
<item name="android:textColorSecondary">?android:textColorPrimary</item>
|
||||
<item name="android:textColorTertiary">@color/classic_dark_5</item>
|
||||
<item name="android:textColor">?android:textColorPrimary</item>
|
||||
<item name="android:textColorHint">@color/gray27</item>
|
||||
<item name="android:windowBackground">?colorPrimary</item>
|
||||
<item name="android:navigationBarColor">@color/compose_view_background</item>
|
||||
<item name="dialog_background_color">@color/classic_dark_1</item>
|
||||
<item name="bottomSheetDialogTheme">@style/Classic.Dark.BottomSheet</item>
|
||||
<item name="actionMenuTextColor">?android:textColorPrimary</item>
|
||||
<item name="popupTheme">?actionBarPopupTheme</item>
|
||||
<item name="colorCellBackground">@color/classic_dark_1</item>
|
||||
<item name="colorSettingsBackground">@color/classic_dark_1</item>
|
||||
<item name="colorDividerBackground">@color/classic_dark_3</item>
|
||||
<item name="actionBarPopupTheme">@style/Dark.Popup</item>
|
||||
<item name="actionBarWidgetTheme">@null</item>
|
||||
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
|
||||
<item name="actionBarStyle">@style/Widget.Session.ActionBar</item>
|
||||
<item name="prominentButtonColor">?colorAccent</item>
|
||||
<item name="elementBorderColor">@color/classic_dark_3</item>
|
||||
|
||||
<!-- Home screen -->
|
||||
<item name="searchBackgroundColor">#1B1B1B</item>
|
||||
<item name="searchIconColor">#E5E5E8</item>
|
||||
<item name="searchHintColor">@color/classic_dark_5</item>
|
||||
<item name="searchTextColor">?android:textColorPrimary</item>
|
||||
<item name="searchHighlightTint">?colorAccent</item>
|
||||
<item name="home_gradient_start">#00000000</item>
|
||||
<item name="home_gradient_end">@color/classic_dark_1</item>
|
||||
<item name="conversation_pinned_background_color">?colorCellBackground</item>
|
||||
<item name="conversation_unread_background_color">@color/classic_dark_2</item>
|
||||
<item name="conversation_pinned_icon_color">?android:textColorSecondary</item>
|
||||
<item name="unreadIndicatorBackgroundColor">@color/classic_dark_3</item>
|
||||
<item name="unreadIndicatorTextColor">@color/classic_dark_6</item>
|
||||
|
||||
<!-- New conversation button -->
|
||||
<item name="conversation_color_non_main">@color/classic_dark_2</item>
|
||||
<item name="conversation_shadow_non_main">@color/transparent_black_30</item>
|
||||
<item name="conversation_shadow_main">?colorAccent</item>
|
||||
<item name="conversation_menu_background_color">@color/classic_dark_1</item>
|
||||
<item name="conversation_menu_cell_color">?conversation_menu_background_color</item>
|
||||
<item name="conversation_menu_border_color">@color/classic_dark_3</item>
|
||||
<item name="conversationMenuSearchBackgroundColor">@color/classic_dark_0</item>
|
||||
|
||||
<!-- Conversation -->
|
||||
<item name="message_received_background_color">@color/classic_dark_3</item>
|
||||
<item name="message_received_text_color">@color/classic_dark_6</item>
|
||||
<item name="message_sent_background_color">?colorAccent</item>
|
||||
<item name="message_sent_text_color">@color/classic_dark_0</item>
|
||||
<item name="message_status_color">@color/classic_dark_5</item>
|
||||
<item name="input_bar_background">@color/classic_dark_1</item>
|
||||
<item name="input_bar_text_hint">@color/classic_dark_5</item>
|
||||
<item name="input_bar_text_user">@color/classic_dark_6</item>
|
||||
<item name="input_bar_border">@color/classic_dark_3</item>
|
||||
<item name="input_bar_button_background">@color/classic_dark_2</item>
|
||||
<item name="input_bar_button_text_color">@color/classic_dark_6</item>
|
||||
<item name="input_bar_button_background_opaque">@color/classic_dark_2</item>
|
||||
<item name="input_bar_button_background_opaque_border">@color/classic_dark_3</item>
|
||||
<item name="input_bar_lock_view_background">@color/classic_dark_2</item>
|
||||
<item name="input_bar_lock_view_border">@color/classic_dark_3</item>
|
||||
<item name="mention_candidates_view_background">@color/classic_dark_2</item>
|
||||
<item name="scroll_to_bottom_button_background">@color/classic_dark_1</item>
|
||||
<item name="scroll_to_bottom_button_border">@color/classic_dark_3</item>
|
||||
<item name="conversation_unread_count_indicator_background">@color/classic_dark_4</item>
|
||||
<item name="message_selected">@color/classic_dark_2</item>
|
||||
</style>
|
||||
|
||||
|
||||
</resources>
|
||||
|
@ -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" />
|
||||
|
||||
<!-- TODO: check figure out what is needed for this -->
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="@bool/screen_security_default"
|
||||
android:key="pref_screen_security"
|
||||
|
@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.first
|
||||
import org.hamcrest.CoreMatchers.endsWith
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito.anyLong
|
||||
@ -31,7 +30,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
private lateinit var recipient: Recipient
|
||||
|
||||
private val viewModel: ConversationViewModel by lazy {
|
||||
ConversationViewModel(threadId, edKeyPair, repository, storage)
|
||||
ConversationViewModel(threadId, edKeyPair, mock(), repository, storage)
|
||||
}
|
||||
|
||||
@Before
|
||||
|
@ -26,9 +26,9 @@ class MessageRequestsViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `should clear all message requests`() = runBlockingTest {
|
||||
viewModel.clearAllMessageRequests()
|
||||
viewModel.clearAllMessageRequests(block = false)
|
||||
|
||||
verify(repository).clearAllMessageRequests()
|
||||
verify(repository).clearAllMessageRequests(block = false)
|
||||
}
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ import junit.framework.TestCase;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.RecipientExporter;
|
||||
|
@ -51,7 +51,7 @@ allprojects {
|
||||
|
||||
project.ext {
|
||||
androidMinimumSdkVersion = 23
|
||||
androidTargetSdkVersion = 31
|
||||
androidCompileSdkVersion = 32
|
||||
androidTargetSdkVersion = 33
|
||||
androidCompileSdkVersion = 33
|
||||
}
|
||||
}
|
@ -1,26 +1,38 @@
|
||||
android.useAndroidX=true
|
||||
## For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
#
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
#
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
#Mon Jun 26 09:56:43 AEST 2023
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx8g
|
||||
|
||||
gradlePluginVersion=7.3.1
|
||||
googleServicesVersion=4.3.12
|
||||
kotlinVersion=1.6.21
|
||||
android.useAndroidX=true
|
||||
appcompatVersion=1.6.1
|
||||
coreVersion=1.8.0
|
||||
coroutinesVersion=1.6.4
|
||||
kotlinxJsonVersion=1.3.3
|
||||
lifecycleVersion=2.5.1
|
||||
curve25519Version=0.6.0
|
||||
daggerVersion=2.40.1
|
||||
glideVersion=4.11.0
|
||||
kovenantVersion=3.3.0
|
||||
curve25519Version=0.6.0
|
||||
protobufVersion=2.5.0
|
||||
okhttpVersion=3.12.1
|
||||
googleServicesVersion=4.3.12
|
||||
gradlePluginVersion=7.3.1
|
||||
jacksonDatabindVersion=2.9.8
|
||||
appcompatVersion=1.5.1
|
||||
materialVersion=1.7.0
|
||||
preferenceVersion=1.2.0
|
||||
coreVersion=1.8.0
|
||||
|
||||
junitVersion=4.13.2
|
||||
mockitoKotlinVersion=4.0.0
|
||||
testCoreVersion=1.4.0
|
||||
kotlinVersion=1.6.21
|
||||
kotlinxJsonVersion=1.3.3
|
||||
kovenantVersion=3.3.0
|
||||
lifecycleVersion=2.5.1
|
||||
materialVersion=1.8.0
|
||||
mockitoKotlinVersion=4.1.0
|
||||
okhttpVersion=3.12.1
|
||||
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||
org.gradle.unsafe.configuration-cache=true
|
||||
pagingVersion=3.0.0
|
||||
preferenceVersion=1.2.0
|
||||
protobufVersion=2.5.0
|
||||
testCoreVersion=1.5.0
|
||||
|
2
libsession-util/.gitignore
vendored
Normal file
2
libsession-util/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.cxx/
|
47
libsession-util/build.gradle
Normal file
47
libsession-util/build.gradle
Normal file
@ -0,0 +1,47 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'network.loki.messenger.libsession_util'
|
||||
compileSdkVersion androidCompileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion androidMinimumSdkVersion
|
||||
targetSdkVersion androidCompileSdkVersion
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "src/main/cpp/CMakeLists.txt"
|
||||
version "3.22.1"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
implementation(project(":libsignal"))
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
|
||||
}
|
1
libsession-util/libsession-util
Submodule
1
libsession-util/libsession-util
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 7eb87028355bfc89950102c52d5b2927a25b2e22
|
@ -0,0 +1,584 @@
|
||||
package network.loki.messenger.libsession_util
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import network.loki.messenger.libsession_util.util.*
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class InstrumentedTests {
|
||||
|
||||
val seed =
|
||||
Hex.fromStringCondensed("0123456789abcdef0123456789abcdef00000000000000000000000000000000")
|
||||
|
||||
private val keyPair: KeyPair
|
||||
get() {
|
||||
return Sodium.ed25519KeyPair(seed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("network.loki.messenger.libsession_util.test", appContext.packageName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_test_sodium_kp_ed_curve() {
|
||||
val kp = keyPair
|
||||
val curvePkBytes = Sodium.ed25519PkToCurve25519(kp.pubKey)
|
||||
|
||||
val edPk = kp.pubKey
|
||||
val curvePk = curvePkBytes
|
||||
|
||||
assertArrayEquals(Hex.fromStringCondensed("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"), edPk)
|
||||
assertArrayEquals(Hex.fromStringCondensed("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"), curvePk)
|
||||
assertArrayEquals(kp.secretKey.take(32).toByteArray(), seed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDirtyEmptyString() {
|
||||
val contacts = Contacts.newInstance(keyPair.secretKey)
|
||||
val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000"
|
||||
val contact = contacts.getOrConstruct(definitelyRealId)
|
||||
contacts.set(contact)
|
||||
assertTrue(contacts.dirty())
|
||||
contacts.set(contact.copy(name = "test"))
|
||||
assertTrue(contacts.dirty())
|
||||
val push = contacts.push()
|
||||
contacts.confirmPushed(push.seqNo, "abc123")
|
||||
contacts.dump()
|
||||
contacts.set(contact.copy(name = "test2"))
|
||||
contacts.set(contact.copy(name = "test"))
|
||||
assertTrue(contacts.dirty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_contacts() {
|
||||
val contacts = Contacts.newInstance(keyPair.secretKey)
|
||||
val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000"
|
||||
assertNull(contacts.get(definitelyRealId))
|
||||
|
||||
// Should be an uninitialized contact apart from ID
|
||||
val c = contacts.getOrConstruct(definitelyRealId)
|
||||
assertEquals(definitelyRealId, c.id)
|
||||
assertTrue(c.name.isEmpty())
|
||||
assertTrue(c.nickname.isEmpty())
|
||||
assertFalse(c.approved)
|
||||
assertFalse(c.approvedMe)
|
||||
assertFalse(c.blocked)
|
||||
assertEquals(UserPic.DEFAULT, c.profilePicture)
|
||||
|
||||
assertFalse(contacts.needsPush())
|
||||
assertFalse(contacts.needsDump())
|
||||
assertEquals(0, contacts.push().seqNo)
|
||||
|
||||
c.name = "Joe"
|
||||
c.nickname = "Joey"
|
||||
c.approved = true
|
||||
c.approvedMe = true
|
||||
|
||||
contacts.set(c)
|
||||
|
||||
val cSaved = contacts.get(definitelyRealId)!!
|
||||
assertEquals("Joe", cSaved.name)
|
||||
assertEquals("Joey", cSaved.nickname)
|
||||
assertTrue(cSaved.approved)
|
||||
assertTrue(cSaved.approvedMe)
|
||||
assertFalse(cSaved.blocked)
|
||||
assertEquals(UserPic.DEFAULT, cSaved.profilePicture)
|
||||
|
||||
val push1 = contacts.push()
|
||||
|
||||
assertEquals(1, push1.seqNo)
|
||||
contacts.confirmPushed(push1.seqNo, "fakehash1")
|
||||
assertFalse(contacts.needsPush())
|
||||
assertTrue(contacts.needsDump())
|
||||
|
||||
val contacts2 = Contacts.newInstance(keyPair.secretKey, contacts.dump())
|
||||
assertFalse(contacts.needsDump())
|
||||
assertFalse(contacts2.needsPush())
|
||||
assertFalse(contacts2.needsDump())
|
||||
|
||||
val anotherId = "051111111111111111111111111111111111111111111111111111111111111111"
|
||||
val c2 = contacts2.getOrConstruct(anotherId)
|
||||
contacts2.set(c2)
|
||||
val push2 = contacts2.push()
|
||||
assertEquals(2, push2.seqNo)
|
||||
contacts2.confirmPushed(push2.seqNo, "fakehash2")
|
||||
assertFalse(contacts2.needsPush())
|
||||
|
||||
contacts.merge("fakehash2" to push2.config)
|
||||
|
||||
|
||||
assertFalse(contacts.needsPush())
|
||||
assertEquals(push2.seqNo, contacts.push().seqNo)
|
||||
|
||||
val contactList = contacts.all().toList()
|
||||
assertEquals(definitelyRealId, contactList[0].id)
|
||||
assertEquals(anotherId, contactList[1].id)
|
||||
assertEquals("Joey", contactList[0].nickname)
|
||||
assertEquals("", contactList[1].nickname)
|
||||
|
||||
contacts.erase(definitelyRealId)
|
||||
|
||||
val thirdId ="052222222222222222222222222222222222222222222222222222222222222222"
|
||||
val third = Contact(
|
||||
id = thirdId,
|
||||
nickname = "Nickname 3",
|
||||
approved = true,
|
||||
blocked = true,
|
||||
profilePicture = UserPic("http://example.com/huge.bmp", "qwertyuio01234567890123456789012".encodeToByteArray()),
|
||||
expiryMode = ExpiryMode.NONE
|
||||
)
|
||||
contacts2.set(third)
|
||||
assertTrue(contacts.needsPush())
|
||||
assertTrue(contacts2.needsPush())
|
||||
val toPush = contacts.push()
|
||||
val toPush2 = contacts2.push()
|
||||
assertEquals(toPush.seqNo, toPush2.seqNo)
|
||||
assertThat(toPush2.config, not(equals(toPush.config)))
|
||||
|
||||
contacts.confirmPushed(toPush.seqNo, "fakehash3a")
|
||||
contacts2.confirmPushed(toPush2.seqNo, "fakehash3b")
|
||||
|
||||
contacts.merge("fakehash3b" to toPush2.config)
|
||||
contacts2.merge("fakehash3a" to toPush.config)
|
||||
|
||||
assertTrue(contacts.needsPush())
|
||||
assertTrue(contacts2.needsPush())
|
||||
|
||||
val mergePush = contacts.push()
|
||||
val mergePush2 = contacts2.push()
|
||||
|
||||
assertEquals(mergePush.seqNo, mergePush2.seqNo)
|
||||
assertArrayEquals(mergePush.config, mergePush2.config)
|
||||
|
||||
assertTrue(mergePush.obsoleteHashes.containsAll(listOf("fakehash3b", "fakehash3a")))
|
||||
assertTrue(mergePush2.obsoleteHashes.containsAll(listOf("fakehash3b", "fakehash3a")))
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_accessible() {
|
||||
val userProfile = UserProfile.newInstance(keyPair.secretKey)
|
||||
assertNotNull(userProfile)
|
||||
userProfile.free()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_user_profile_c_api() {
|
||||
val edSk = keyPair.secretKey
|
||||
val userProfile = UserProfile.newInstance(edSk)
|
||||
|
||||
// these should be false as empty config
|
||||
assertFalse(userProfile.needsPush())
|
||||
assertFalse(userProfile.needsDump())
|
||||
|
||||
// Since it's empty there shouldn't be a name
|
||||
assertNull(userProfile.getName())
|
||||
|
||||
// Don't need to push yet so this is just for testing
|
||||
val (_, seqNo) = userProfile.push() // disregarding encrypted
|
||||
assertEquals("UserProfile", userProfile.encryptionDomain())
|
||||
assertEquals(0, seqNo)
|
||||
|
||||
// This should also be unset:
|
||||
assertEquals(UserPic.DEFAULT, userProfile.getPic())
|
||||
|
||||
// Now let's go set a profile name and picture:
|
||||
// not sending keylen like c api so cutting off the NOTSECRET in key for testing purposes
|
||||
userProfile.setName("Kallie")
|
||||
val newUserPic = UserPic("http://example.org/omg-pic-123.bmp", "secret78901234567890123456789012".encodeToByteArray())
|
||||
userProfile.setPic(newUserPic)
|
||||
userProfile.setNtsPriority(9)
|
||||
|
||||
// Retrieve them just to make sure they set properly:
|
||||
assertEquals("Kallie", userProfile.getName())
|
||||
val pic = userProfile.getPic()
|
||||
assertEquals("http://example.org/omg-pic-123.bmp", pic.url)
|
||||
assertEquals("secret78901234567890123456789012", pic.key.decodeToString())
|
||||
|
||||
// Since we've made changes, we should need to push new config to the swarm, *and* should need
|
||||
// to dump the updated state:
|
||||
assertTrue(userProfile.needsPush())
|
||||
assertTrue(userProfile.needsDump())
|
||||
val (newToPush, newSeqNo) = userProfile.push()
|
||||
|
||||
val expHash0 =
|
||||
Hex.fromStringCondensed("ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
|
||||
|
||||
val expectedPush1Decrypted = ("d" +
|
||||
"1:#"+ "i1e" +
|
||||
"1:&"+ "d"+
|
||||
"1:+"+ "i9e"+
|
||||
"1:n"+ "6:Kallie"+
|
||||
"1:p"+ "34:http://example.org/omg-pic-123.bmp"+
|
||||
"1:q"+ "32:secret78901234567890123456789012"+
|
||||
"e"+
|
||||
"1:<"+ "l"+
|
||||
"l"+ "i0e"+ "32:").encodeToByteArray() + expHash0 + ("de"+ "e"+
|
||||
"e"+
|
||||
"1:="+ "d"+
|
||||
"1:+" +"0:"+
|
||||
"1:n" +"0:"+
|
||||
"1:p" +"0:"+
|
||||
"1:q" +"0:"+
|
||||
"e"+
|
||||
"e").encodeToByteArray()
|
||||
|
||||
assertEquals(1, newSeqNo)
|
||||
// We haven't dumped, so still need to dump:
|
||||
assertTrue(userProfile.needsDump())
|
||||
// We did call push but we haven't confirmed it as stored yet, so this will still return true:
|
||||
assertTrue(userProfile.needsPush())
|
||||
|
||||
val dump = userProfile.dump()
|
||||
// (in a real client we'd now store this to disk)
|
||||
assertFalse(userProfile.needsDump())
|
||||
val expectedDump = ("d" +
|
||||
"1:!"+ "i2e" +
|
||||
"1:$").encodeToByteArray() + expectedPush1Decrypted.size.toString().encodeToByteArray() +
|
||||
":".encodeToByteArray() + expectedPush1Decrypted +
|
||||
"1:(0:1:)le".encodeToByteArray()+
|
||||
"e".encodeToByteArray()
|
||||
|
||||
assertArrayEquals(expectedDump, dump)
|
||||
|
||||
userProfile.confirmPushed(newSeqNo, "fakehash1")
|
||||
|
||||
val newConf = UserProfile.newInstance(edSk)
|
||||
|
||||
val accepted = newConf.merge("fakehash1" to newToPush)
|
||||
assertEquals(1, accepted)
|
||||
|
||||
assertTrue(newConf.needsDump())
|
||||
assertFalse(newConf.needsPush())
|
||||
val _ignore = newConf.dump()
|
||||
assertFalse(newConf.needsDump())
|
||||
|
||||
|
||||
userProfile.setName("Raz")
|
||||
newConf.setName("Nibbler")
|
||||
newConf.setPic(UserPic("http://new.example.com/pic", "qwertyuio01234567890123456789012".encodeToByteArray()))
|
||||
|
||||
val conf = userProfile.push()
|
||||
val conf2 = newConf.push()
|
||||
|
||||
userProfile.confirmPushed(conf.seqNo, "fakehash2")
|
||||
newConf.confirmPushed(conf2.seqNo, "fakehash3")
|
||||
|
||||
userProfile.dump()
|
||||
|
||||
assertFalse(conf.config.contentEquals(conf2.config))
|
||||
|
||||
newConf.merge("fakehash2" to conf.config)
|
||||
userProfile.merge("fakehash3" to conf2.config)
|
||||
|
||||
assertTrue(newConf.needsPush())
|
||||
assertTrue(userProfile.needsPush())
|
||||
|
||||
val newSeq1 = userProfile.push()
|
||||
|
||||
assertEquals(3, newSeq1.seqNo)
|
||||
|
||||
userProfile.confirmPushed(newSeq1.seqNo, "fakehash4")
|
||||
|
||||
// assume newConf push gets rejected as it was last to write and clear previous config by hash on oxenss
|
||||
newConf.merge("fakehash4" to newSeq1.config)
|
||||
|
||||
val newSeqMerge = newConf.push()
|
||||
|
||||
newConf.confirmPushed(newSeqMerge.seqNo, "fakehash5")
|
||||
|
||||
assertEquals("Raz", newConf.getName())
|
||||
assertEquals(3, newSeqMerge.seqNo)
|
||||
|
||||
// userProfile device polls and merges
|
||||
userProfile.merge("fakehash5" to newSeqMerge.config)
|
||||
|
||||
val userConfigMerge = userProfile.push()
|
||||
|
||||
assertEquals(3, userConfigMerge.seqNo)
|
||||
|
||||
assertEquals("Raz", newConf.getName())
|
||||
assertEquals("Raz", userProfile.getName())
|
||||
|
||||
userProfile.free()
|
||||
newConf.free()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_resolves_conflicts() {
|
||||
val kp = keyPair
|
||||
val a = UserProfile.newInstance(kp.secretKey)
|
||||
val b = UserProfile.newInstance(kp.secretKey)
|
||||
a.setName("A")
|
||||
val (aPush, aSeq) = a.push()
|
||||
a.confirmPushed(aSeq, "hashfroma")
|
||||
b.setName("B")
|
||||
// polls and sees invalid state, has to merge
|
||||
b.merge("hashfroma" to aPush)
|
||||
val (bPush, bSeq) = b.push()
|
||||
b.confirmPushed(bSeq, "hashfromb")
|
||||
assertEquals("B", b.getName())
|
||||
assertEquals(1, aSeq)
|
||||
assertEquals(2, bSeq)
|
||||
a.merge("hashfromb" to bPush)
|
||||
assertEquals(2, a.push().seqNo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_setting_getting() {
|
||||
val userProfile = UserProfile.newInstance(keyPair.secretKey)
|
||||
val newName = "test"
|
||||
println("Name being set via JNI call: $newName")
|
||||
userProfile.setName(newName)
|
||||
val nameFromNative = userProfile.getName()
|
||||
assertEquals(newName, nameFromNative)
|
||||
println("Name received by JNI call: $nameFromNative")
|
||||
assertTrue(userProfile.dirty())
|
||||
userProfile.free()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_remove_all_test() {
|
||||
val convos = ConversationVolatileConfig.newInstance(keyPair.secretKey)
|
||||
assertEquals(0 /* number removed */, convos.eraseAll { true /* 'erase' every item */ })
|
||||
|
||||
val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000"
|
||||
val definitelyRealConvo = Conversation.OneToOne(definitelyRealId, System.currentTimeMillis(), false)
|
||||
convos.set(definitelyRealConvo)
|
||||
|
||||
val anotherDefinitelyReadId = "051111111111111111111111111111111111111111111111111111111111111111"
|
||||
val anotherDefinitelyRealConvo = Conversation.OneToOne(anotherDefinitelyReadId, System.currentTimeMillis(), false)
|
||||
convos.set(anotherDefinitelyRealConvo)
|
||||
|
||||
assertEquals(2, convos.sizeOneToOnes())
|
||||
|
||||
val numErased = convos.eraseAll { convo ->
|
||||
convo is Conversation.OneToOne && convo.sessionId == definitelyRealId
|
||||
}
|
||||
assertEquals(1, numErased)
|
||||
assertEquals(1, convos.sizeOneToOnes())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_open_group_urls() {
|
||||
val (base1, room1, pk1) = BaseCommunityInfo.parseFullUrl(
|
||||
"https://example.com/" +
|
||||
"someroom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
)!!
|
||||
|
||||
val (base2, room2, pk2) = BaseCommunityInfo.parseFullUrl(
|
||||
"HTTPS://EXAMPLE.COM/" +
|
||||
"someroom?public_key=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"
|
||||
)!!
|
||||
|
||||
val (base3, room3, pk3) = BaseCommunityInfo.parseFullUrl(
|
||||
"HTTPS://EXAMPLE.COM/r/" +
|
||||
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
|
||||
)!!
|
||||
|
||||
val (base4, room4, pk4) = BaseCommunityInfo.parseFullUrl(
|
||||
"http://example.com/r/" +
|
||||
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
|
||||
)!!
|
||||
|
||||
val (base5, room5, pk5) = BaseCommunityInfo.parseFullUrl(
|
||||
"HTTPS://EXAMPLE.com:443/r/" +
|
||||
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
|
||||
)!!
|
||||
|
||||
val (base6, room6, pk6) = BaseCommunityInfo.parseFullUrl(
|
||||
"HTTP://EXAMPLE.com:80/r/" +
|
||||
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
|
||||
)!!
|
||||
|
||||
val (base7, room7, pk7) = BaseCommunityInfo.parseFullUrl(
|
||||
"http://example.com:80/r/" +
|
||||
"someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8"
|
||||
)!!
|
||||
val (base8, room8, pk8) = BaseCommunityInfo.parseFullUrl(
|
||||
"http://example.com:80/r/" +
|
||||
"someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo"
|
||||
)!!
|
||||
|
||||
assertEquals("https://example.com", base1)
|
||||
assertEquals("http://example.com", base4)
|
||||
assertEquals(base1, base2)
|
||||
assertEquals(base1, base3)
|
||||
assertNotEquals(base1, base4)
|
||||
assertEquals(base1, base5)
|
||||
assertEquals(base4, base6)
|
||||
assertEquals(base4, base7)
|
||||
assertEquals(base4, base8)
|
||||
assertEquals("someroom", room1)
|
||||
assertEquals("someroom", room2)
|
||||
assertEquals("someroom", room3)
|
||||
assertEquals("someroom", room4)
|
||||
assertEquals("someroom", room5)
|
||||
assertEquals("someroom", room6)
|
||||
assertEquals("someroom", room7)
|
||||
assertEquals("someroom", room8)
|
||||
assertEquals(Hex.toStringCondensed(pk1), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk2), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk3), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk4), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk5), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk6), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk7), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk8), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_conversations() {
|
||||
val convos = ConversationVolatileConfig.newInstance(keyPair.secretKey)
|
||||
val definitelyRealId = "055000000000000000000000000000000000000000000000000000000000000000"
|
||||
assertNull(convos.getOneToOne(definitelyRealId))
|
||||
assertTrue(convos.empty())
|
||||
assertEquals(0, convos.size())
|
||||
|
||||
val c = convos.getOrConstructOneToOne(definitelyRealId)
|
||||
|
||||
assertEquals(definitelyRealId, c.sessionId)
|
||||
assertEquals(0, c.lastRead)
|
||||
|
||||
assertFalse(convos.needsPush())
|
||||
assertFalse(convos.needsDump())
|
||||
assertEquals(0, convos.push().seqNo)
|
||||
|
||||
val nowMs = System.currentTimeMillis()
|
||||
|
||||
c.lastRead = nowMs
|
||||
|
||||
convos.set(c)
|
||||
|
||||
assertNull(convos.getLegacyClosedGroup(definitelyRealId))
|
||||
assertNotNull(convos.getOneToOne(definitelyRealId))
|
||||
assertEquals(nowMs, convos.getOneToOne(definitelyRealId)?.lastRead)
|
||||
|
||||
assertTrue(convos.needsPush())
|
||||
assertTrue(convos.needsDump())
|
||||
|
||||
val openGroupPubKey = Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
|
||||
val og = convos.getOrConstructCommunity("http://Example.ORG:5678", "SudokuRoom", openGroupPubKey)
|
||||
val ogCommunity = og.baseCommunityInfo
|
||||
|
||||
assertEquals("http://example.org:5678", ogCommunity.baseUrl) // Note: lower-case
|
||||
assertEquals("sudokuroom", ogCommunity.room) // Note: lower-case
|
||||
assertEquals(64, ogCommunity.pubKeyHex.length)
|
||||
assertEquals("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ogCommunity.pubKeyHex)
|
||||
|
||||
og.unread = true
|
||||
|
||||
convos.set(og)
|
||||
|
||||
val (_, seqNo) = convos.push()
|
||||
|
||||
assertEquals(1, seqNo)
|
||||
|
||||
convos.confirmPushed(seqNo, "fakehash1")
|
||||
|
||||
assertTrue(convos.needsDump())
|
||||
assertFalse(convos.needsPush())
|
||||
|
||||
val convos2 = ConversationVolatileConfig.newInstance(keyPair.secretKey, convos.dump())
|
||||
assertFalse(convos.needsPush())
|
||||
assertFalse(convos.needsDump())
|
||||
assertEquals(1, convos.push().seqNo)
|
||||
assertFalse(convos.needsDump())
|
||||
|
||||
val x1 = convos2.getOneToOne(definitelyRealId)!!
|
||||
assertEquals(nowMs, x1.lastRead)
|
||||
assertEquals(definitelyRealId, x1.sessionId)
|
||||
assertEquals(false, x1.unread)
|
||||
|
||||
val x2 = convos2.getCommunity("http://EXAMPLE.org:5678", "sudokuRoom")!!
|
||||
val x2Info = x2.baseCommunityInfo
|
||||
assertEquals("http://example.org:5678", x2Info.baseUrl)
|
||||
assertEquals("sudokuroom", x2Info.room)
|
||||
assertEquals(x2Info.pubKeyHex, Hex.toStringCondensed(openGroupPubKey))
|
||||
assertTrue(x2.unread)
|
||||
|
||||
val anotherId = "051111111111111111111111111111111111111111111111111111111111111111"
|
||||
val c2 = convos.getOrConstructOneToOne(anotherId)
|
||||
c2.unread = true
|
||||
convos2.set(c2)
|
||||
|
||||
val c3 = convos.getOrConstructLegacyGroup(
|
||||
"05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||
)
|
||||
c3.lastRead = nowMs - 50
|
||||
convos2.set(c3)
|
||||
|
||||
assertTrue(convos2.needsPush())
|
||||
|
||||
val (toPush2, seqNo2) = convos2.push()
|
||||
assertEquals(2, seqNo2)
|
||||
|
||||
convos2.confirmPushed(seqNo2, "fakehash2")
|
||||
convos.merge("fakehash2" to toPush2)
|
||||
|
||||
assertFalse(convos.needsPush())
|
||||
assertEquals(seqNo2, convos.push().seqNo)
|
||||
|
||||
val seen = mutableListOf<String>()
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user