mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 16:37:41 +00:00
Merge pull request #1451 from bemusementpark/on-2
[SES-48 SES-824] Onboarding Overhaul
This commit is contained in:
@@ -376,14 +376,25 @@ dependencies {
|
|||||||
|
|
||||||
|
|
||||||
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
|
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
|
||||||
implementation 'androidx.compose.ui:ui:1.5.2'
|
implementation "androidx.compose.ui:ui:$composeVersion"
|
||||||
implementation 'androidx.compose.ui:ui-tooling:1.5.2'
|
implementation "androidx.compose.animation:animation:$composeVersion"
|
||||||
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
|
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
|
||||||
implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha"
|
implementation "androidx.compose.runtime:runtime-livedata:$composeVersion"
|
||||||
implementation "androidx.compose.runtime:runtime-livedata:1.5.2"
|
implementation "androidx.compose.foundation:foundation-layout:$composeVersion"
|
||||||
|
implementation "androidx.compose.material:material:$composeVersion"
|
||||||
|
androidTestImplementation "androidx.compose.ui:ui-test-junit4-android:$composeVersion"
|
||||||
|
debugImplementation "androidx.compose.ui:ui-test-manifest:$composeVersion"
|
||||||
|
|
||||||
implementation 'androidx.compose.foundation:foundation-layout:1.5.2'
|
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
|
||||||
implementation 'androidx.compose.material:material:1.5.2'
|
implementation "com.google.accompanist:accompanist-permissions:0.33.1-alpha"
|
||||||
|
implementation "com.google.accompanist:accompanist-drawablepainter:0.33.1-alpha"
|
||||||
|
|
||||||
|
implementation "androidx.camera:camera-camera2:1.3.2"
|
||||||
|
implementation "androidx.camera:camera-lifecycle:1.3.2"
|
||||||
|
implementation "androidx.camera:camera-view:1.3.2"
|
||||||
|
|
||||||
|
implementation 'com.google.firebase:firebase-core:21.1.1'
|
||||||
|
implementation "com.google.mlkit:barcode-scanning:17.2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
static def getLastCommitTimestamp() {
|
static def getLastCommitTimestamp() {
|
||||||
|
@@ -22,6 +22,8 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.uiautomator.By
|
||||||
|
import androidx.test.uiautomator.UiDevice
|
||||||
import com.adevinta.android.barista.interaction.PermissionGranter
|
import com.adevinta.android.barista.interaction.PermissionGranter
|
||||||
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
||||||
import org.hamcrest.Matcher
|
import org.hamcrest.Matcher
|
||||||
@@ -49,9 +51,14 @@ class HomeActivityTests {
|
|||||||
|
|
||||||
private val activityMonitor = Instrumentation.ActivityMonitor(ConversationActivityV2::class.java.name, null, false)
|
private val activityMonitor = Instrumentation.ActivityMonitor(ConversationActivityV2::class.java.name, null, false)
|
||||||
|
|
||||||
|
private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||||
|
|
||||||
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor)
|
InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -72,25 +79,34 @@ class HomeActivityTests {
|
|||||||
onView(isRoot()).perform(waitFor(500))
|
onView(isRoot()).perform(waitFor(500))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun objectFromDesc(id: Int) = device.findObject(By.desc(context.getString(id)))
|
||||||
|
|
||||||
private fun setupLoggedInState(hasViewedSeed: Boolean = false) {
|
private fun setupLoggedInState(hasViewedSeed: Boolean = false) {
|
||||||
// landing activity
|
// landing activity
|
||||||
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
objectFromDesc(R.string.onboardingAccountCreate).click()
|
||||||
// session ID - register activity
|
|
||||||
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
|
||||||
// display name selection
|
// display name selection
|
||||||
onView(withId(R.id.displayNameEditText)).perform(ViewActions.typeText("test-user123"))
|
objectFromDesc(R.string.displayNameEnter).click()
|
||||||
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
device.pressKeyCode(65)
|
||||||
|
device.pressKeyCode(66)
|
||||||
|
device.pressKeyCode(67)
|
||||||
|
|
||||||
|
// Continue with display name
|
||||||
|
objectFromDesc(R.string.continue_2).click()
|
||||||
|
|
||||||
|
// Continue with default push notification setting
|
||||||
|
objectFromDesc(R.string.continue_2).click()
|
||||||
|
|
||||||
// PN select
|
// PN select
|
||||||
if (hasViewedSeed) {
|
if (hasViewedSeed) {
|
||||||
// has viewed seed is set to false after register activity
|
// has viewed seed is set to false after register activity
|
||||||
TextSecurePreferences.setHasViewedSeed(InstrumentationRegistry.getInstrumentation().targetContext, true)
|
TextSecurePreferences.setHasViewedSeed(InstrumentationRegistry.getInstrumentation().targetContext, true)
|
||||||
}
|
}
|
||||||
onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click())
|
|
||||||
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
|
||||||
// allow notification permission
|
// allow notification permission
|
||||||
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
|
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun goToMyChat() {
|
private fun goToMyChat() {
|
||||||
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
||||||
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
||||||
@@ -111,8 +127,8 @@ class HomeActivityTests {
|
|||||||
@Test
|
@Test
|
||||||
fun testLaunches_dismiss_seedView() {
|
fun testLaunches_dismiss_seedView() {
|
||||||
setupLoggedInState()
|
setupLoggedInState()
|
||||||
onView(allOf(withId(R.id.button), isDescendantOfA(withId(R.id.seedReminderView)))).perform(ViewActions.click())
|
objectFromDesc(R.string.continue_2).click()
|
||||||
onView(withId(R.id.copyButton)).perform(ViewActions.click())
|
objectFromDesc(R.string.copy).click()
|
||||||
pressBack()
|
pressBack()
|
||||||
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
||||||
}
|
}
|
||||||
@@ -133,7 +149,7 @@ class HomeActivityTests {
|
|||||||
fun testChat_withSelf() {
|
fun testChat_withSelf() {
|
||||||
setupLoggedInState()
|
setupLoggedInState()
|
||||||
goToMyChat()
|
goToMyChat()
|
||||||
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
|
TextSecurePreferences.setLinkPreviewsEnabled(context, true)
|
||||||
sendMessage("howdy")
|
sendMessage("howdy")
|
||||||
sendMessage("test")
|
sendMessage("test")
|
||||||
// tests url rewriter doesn't crash
|
// tests url rewriter doesn't crash
|
||||||
|
@@ -39,7 +39,7 @@ class LibSessionTests {
|
|||||||
|
|
||||||
private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
||||||
private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
|
private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
|
||||||
private fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
|
private fun randomAccountId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
|
||||||
|
|
||||||
private var fakeHashI = 0
|
private var fakeHashI = 0
|
||||||
private val nextFakeHash: String
|
private val nextFakeHash: String
|
||||||
@@ -102,7 +102,7 @@ class LibSessionTests {
|
|||||||
val storageSpy = spy(app.storage)
|
val storageSpy = spy(app.storage)
|
||||||
app.storage = storageSpy
|
app.storage = storageSpy
|
||||||
|
|
||||||
val newContactId = randomSessionId()
|
val newContactId = randomAccountId()
|
||||||
val singleContact = Contact(
|
val singleContact = Contact(
|
||||||
id = newContactId,
|
id = newContactId,
|
||||||
approved = true,
|
approved = true,
|
||||||
@@ -123,7 +123,7 @@ class LibSessionTests {
|
|||||||
val storageSpy = spy(app.storage)
|
val storageSpy = spy(app.storage)
|
||||||
app.storage = storageSpy
|
app.storage = storageSpy
|
||||||
|
|
||||||
val randomRecipient = randomSessionId()
|
val randomRecipient = randomAccountId()
|
||||||
val newContact = Contact(
|
val newContact = Contact(
|
||||||
id = randomRecipient,
|
id = randomRecipient,
|
||||||
approved = true,
|
approved = true,
|
||||||
@@ -158,7 +158,7 @@ class LibSessionTests {
|
|||||||
app.storage = storageSpy
|
app.storage = storageSpy
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
val randomRecipient = randomSessionId()
|
val randomRecipient = randomAccountId()
|
||||||
val currentContact = Contact(
|
val currentContact = Contact(
|
||||||
id = randomRecipient,
|
id = randomRecipient,
|
||||||
approved = true,
|
approved = true,
|
||||||
|
@@ -136,29 +136,29 @@ class SodiumUtilitiesTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun sessionIdSuccess() {
|
fun accountIdSuccess() {
|
||||||
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", serverPublicKey)
|
val result = SodiumUtilities.accountId("05$publicKey", "15$blindedPublicKey", serverPublicKey)
|
||||||
|
|
||||||
assertTrue(result)
|
assertTrue(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun sessionIdFailureInvalidSessionId() {
|
fun accountIdFailureInvalidAccountId() {
|
||||||
val result = SodiumUtilities.sessionId("AB$publicKey", "15$blindedPublicKey", serverPublicKey)
|
val result = SodiumUtilities.accountId("AB$publicKey", "15$blindedPublicKey", serverPublicKey)
|
||||||
|
|
||||||
assertFalse(result)
|
assertFalse(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun sessionIdFailureInvalidBlindedId() {
|
fun accountIdFailureInvalidBlindedId() {
|
||||||
val result = SodiumUtilities.sessionId("05$publicKey", "AB$blindedPublicKey", serverPublicKey)
|
val result = SodiumUtilities.accountId("05$publicKey", "AB$blindedPublicKey", serverPublicKey)
|
||||||
|
|
||||||
assertFalse(result)
|
assertFalse(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun sessionIdFailureBlindingFactor() {
|
fun accountIdFailureBlindingFactor() {
|
||||||
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", "Test")
|
val result = SodiumUtilities.accountId("05$publicKey", "15$blindedPublicKey", "Test")
|
||||||
|
|
||||||
assertFalse(result)
|
assertFalse(result)
|
||||||
}
|
}
|
||||||
|
@@ -100,31 +100,32 @@
|
|||||||
android:value="false" />
|
android:value="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.onboarding.LandingActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.landing.LandingActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.onboarding.RegisterActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.loadaccount.LoadAccountActivity"
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.onboarding.LinkDeviceActivity"
|
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.onboarding.DisplayNameActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.loading.LoadingActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.onboarding.PNModeActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.pickname.PickDisplayNameActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name="org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.home.HomeActivity"
|
android:name="org.thoughtcrime.securesms.home.HomeActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="standard"
|
||||||
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"
|
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"
|
||||||
@@ -153,7 +154,7 @@
|
|||||||
android:label="@string/activity_edit_closed_group_title"
|
android:label="@string/activity_edit_closed_group_title"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.onboarding.SeedActivity"
|
android:name="org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.contacts.SelectContactsActivity"
|
android:name="org.thoughtcrime.securesms.contacts.SelectContactsActivity"
|
||||||
|
@@ -18,6 +18,7 @@ package org.thoughtcrime.securesms;
|
|||||||
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
|
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
|
||||||
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
|
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -137,7 +138,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
public MessageNotifier messageNotifier = null;
|
public MessageNotifier messageNotifier = null;
|
||||||
public Poller poller = null;
|
public Poller poller = null;
|
||||||
public Broadcaster broadcaster = null;
|
public Broadcaster broadcaster = null;
|
||||||
private Job firebaseInstanceIdJob;
|
|
||||||
private WindowDebouncer conversationListDebouncer;
|
private WindowDebouncer conversationListDebouncer;
|
||||||
private HandlerThread conversationListHandlerThread;
|
private HandlerThread conversationListHandlerThread;
|
||||||
private Handler conversationListHandler;
|
private Handler conversationListHandler;
|
||||||
@@ -261,7 +261,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
|
|
||||||
// If the user account hasn't been created or onboarding wasn't finished then don't start
|
// If the user account hasn't been created or onboarding wasn't finished then don't start
|
||||||
// the pollers
|
// the pollers
|
||||||
if (TextSecurePreferences.getLocalNumber(this) == null || !TextSecurePreferences.hasSeenWelcomeScreen(this)) {
|
if (textSecurePreferences.getLocalNumber() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,6 +451,13 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
ClosedGroupPollerV2.getShared().start();
|
ClosedGroupPollerV2.getShared().start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void retrieveUserProfile() {
|
||||||
|
setUpPollingIfNeeded();
|
||||||
|
if (poller != null) {
|
||||||
|
poller.retrieveUserProfile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void resubmitProfilePictureIfNeeded() {
|
private void resubmitProfilePictureIfNeeded() {
|
||||||
// Files expire on the file server after a while, so we simply re-upload the user's profile picture
|
// Files expire on the file server after a while, so we simply re-upload the user's profile picture
|
||||||
// at a certain interval to ensure it's always available.
|
// at a certain interval to ensure it's always available.
|
||||||
@@ -505,20 +512,11 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all local profile data and message history then restart the app after a brief delay.
|
* Clear all local profile data and message history then restart the app after a brief delay.
|
||||||
* @param isMigratingToV2KeyPair whether we're upgrading to a more recent V2 key pair or not.
|
|
||||||
* @return true on success, false otherwise.
|
* @return true on success, false otherwise.
|
||||||
*/
|
*/
|
||||||
public boolean clearAllData(boolean isMigratingToV2KeyPair) {
|
@SuppressLint("ApplySharedPref")
|
||||||
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
|
public boolean clearAllData() {
|
||||||
firebaseInstanceIdJob.cancel(null);
|
|
||||||
}
|
|
||||||
String displayName = TextSecurePreferences.getProfileName(this);
|
|
||||||
boolean isUsingFCM = TextSecurePreferences.isPushEnabled(this);
|
|
||||||
TextSecurePreferences.clearAll(this);
|
TextSecurePreferences.clearAll(this);
|
||||||
if (isMigratingToV2KeyPair) {
|
|
||||||
TextSecurePreferences.setPushEnabled(this, isUsingFCM);
|
|
||||||
TextSecurePreferences.setProfileName(this, displayName);
|
|
||||||
}
|
|
||||||
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||||
if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
|
if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
|
||||||
Log.d("Loki", "Failed to delete database.");
|
Log.d("Loki", "Failed to delete database.");
|
||||||
|
@@ -15,7 +15,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.onboarding.LandingActivity;
|
import org.thoughtcrime.securesms.onboarding.landing.LandingActivity;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -125,12 +125,12 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int getApplicationState(boolean locked) {
|
private int getApplicationState(boolean locked) {
|
||||||
if (locked) {
|
if (TextSecurePreferences.getLocalNumber(this) == null) {
|
||||||
|
return STATE_WELCOME_SCREEN;
|
||||||
|
} else if (locked) {
|
||||||
return STATE_PROMPT_PASSPHRASE;
|
return STATE_PROMPT_PASSPHRASE;
|
||||||
} else if (DatabaseUpgradeActivity.isUpdate(this)) {
|
} else if (DatabaseUpgradeActivity.isUpdate(this)) {
|
||||||
return STATE_UPGRADE_DATABASE;
|
return STATE_UPGRADE_DATABASE;
|
||||||
} else if (!TextSecurePreferences.hasSeenWelcomeScreen(this)) {
|
|
||||||
return STATE_WELCOME_SCREEN;
|
|
||||||
} else {
|
} else {
|
||||||
return STATE_NORMAL;
|
return STATE_NORMAL;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package org.thoughtcrime.securesms
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
@@ -15,7 +17,7 @@ import androidx.annotation.LayoutRes
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.setMargins
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.core.view.updateMargins
|
import androidx.core.view.updateMargins
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
@@ -80,6 +82,10 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
}.let(topView::addView)
|
}.let(topView::addView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun htmlText(@StringRes id: Int, @StyleRes style: Int = 0, modify: TextView.() -> Unit = {}) {
|
||||||
|
text(HtmlCompat.fromHtml(context.resources.getString(id), 0))
|
||||||
|
}
|
||||||
|
|
||||||
fun view(view: View) = contentView.addView(view)
|
fun view(view: View) = contentView.addView(view)
|
||||||
|
|
||||||
fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView)
|
fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView)
|
||||||
@@ -143,6 +149,20 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
|
|
||||||
fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||||
SessionDialogBuilder(this).apply { build() }.show()
|
SessionDialogBuilder(this).apply { build() }.show()
|
||||||
|
fun Context.showOpenUrlDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||||
|
SessionDialogBuilder(this).apply {
|
||||||
|
title(R.string.urlOpen)
|
||||||
|
text(R.string.urlOpenBrowser)
|
||||||
|
build()
|
||||||
|
}.show()
|
||||||
|
|
||||||
|
fun Context.showOpenUrlDialog(url: String): AlertDialog =
|
||||||
|
showOpenUrlDialog {
|
||||||
|
okButton { openUrl(url) }
|
||||||
|
cancelButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.openUrl(url: String) = Intent(Intent.ACTION_VIEW, Uri.parse(url)).let(::startActivity)
|
||||||
|
|
||||||
fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||||
SessionDialogBuilder(requireContext()).apply { build() }.show()
|
SessionDialogBuilder(requireContext()).apply { build() }.show()
|
||||||
|
@@ -445,7 +445,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
private fun getUserDisplayName(publicKey: String): String {
|
private fun getUserDisplayName(publicKey: String): String {
|
||||||
val contact =
|
val contact =
|
||||||
DatabaseComponent.get(this).sessionContactDatabase().getContactWithSessionID(publicKey)
|
DatabaseComponent.get(this).sessionContactDatabase().getContactWithAccountID(publicKey)
|
||||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ import org.session.libsession.avatars.ProfileContactPhoto
|
|||||||
import org.session.libsession.avatars.ResourceContactPhoto
|
import org.session.libsession.avatars.ResourceContactPhoto
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.AppTextSecurePreferences
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@@ -29,6 +30,8 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
|
|
||||||
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
|
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
|
||||||
private val glide: GlideRequests = GlideApp.with(this)
|
private val glide: GlideRequests = GlideApp.with(this)
|
||||||
|
private val prefs = AppTextSecurePreferences(context)
|
||||||
|
private val userPublicKey = prefs.getLocalNumber()
|
||||||
var publicKey: String? = null
|
var publicKey: String? = null
|
||||||
var displayName: String? = null
|
var displayName: String? = null
|
||||||
var additionalPublicKey: String? = null
|
var additionalPublicKey: String? = null
|
||||||
@@ -40,25 +43,28 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
|
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
|
||||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
constructor(context: Context, sender: Recipient): this(context) {
|
constructor(context: Context, sender: Recipient): this(context) {
|
||||||
update(sender)
|
update(sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Updating
|
|
||||||
fun update(recipient: Recipient) {
|
fun update(recipient: Recipient) {
|
||||||
fun getUserDisplayName(publicKey: String): String {
|
recipient.run { update(address, isClosedGroupRecipient, isOpenGroupInboxRecipient) }
|
||||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
}
|
||||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recipient.isClosedGroupRecipient) {
|
fun update(
|
||||||
|
address: Address,
|
||||||
|
isClosedGroupRecipient: Boolean = false,
|
||||||
|
isOpenGroupInboxRecipient: Boolean = false
|
||||||
|
) {
|
||||||
|
fun getUserDisplayName(publicKey: String): String = prefs.takeIf { userPublicKey == publicKey }?.getProfileName()
|
||||||
|
?: DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)?.displayName(Contact.ContactContext.REGULAR)
|
||||||
|
?: publicKey
|
||||||
|
|
||||||
|
if (isClosedGroupRecipient) {
|
||||||
val members = DatabaseComponent.get(context).groupDatabase()
|
val members = DatabaseComponent.get(context).groupDatabase()
|
||||||
.getGroupMemberAddresses(recipient.address.toGroupString(), true)
|
.getGroupMemberAddresses(address.toGroupString(), true)
|
||||||
.sorted()
|
.sorted()
|
||||||
.take(2)
|
.take(2)
|
||||||
.toMutableList()
|
|
||||||
if (members.size <= 1) {
|
if (members.size <= 1) {
|
||||||
publicKey = ""
|
publicKey = ""
|
||||||
displayName = ""
|
displayName = ""
|
||||||
@@ -72,13 +78,13 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
additionalPublicKey = apk
|
additionalPublicKey = apk
|
||||||
additionalDisplayName = getUserDisplayName(apk)
|
additionalDisplayName = getUserDisplayName(apk)
|
||||||
}
|
}
|
||||||
} else if(recipient.isOpenGroupInboxRecipient) {
|
} else if(isOpenGroupInboxRecipient) {
|
||||||
val publicKey = GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())
|
val publicKey = GroupUtil.getDecodedOpenGroupInboxAccountId(address.serialize())
|
||||||
this.publicKey = publicKey
|
this.publicKey = publicKey
|
||||||
displayName = getUserDisplayName(publicKey)
|
displayName = getUserDisplayName(publicKey)
|
||||||
additionalPublicKey = null
|
additionalPublicKey = null
|
||||||
} else {
|
} else {
|
||||||
val publicKey = recipient.address.toString()
|
val publicKey = address.serialize()
|
||||||
this.publicKey = publicKey
|
this.publicKey = publicKey
|
||||||
displayName = getUserDisplayName(publicKey)
|
displayName = getUserDisplayName(publicKey)
|
||||||
additionalPublicKey = null
|
additionalPublicKey = null
|
||||||
|
@@ -49,7 +49,7 @@ class UserView : LinearLayout {
|
|||||||
val isLocalUser = user.isLocalNumber
|
val isLocalUser = user.isLocalNumber
|
||||||
fun getUserDisplayName(publicKey: String): String {
|
fun getUserDisplayName(publicKey: String): String {
|
||||||
if (isLocalUser) return context.getString(R.string.MessageRecord_you)
|
if (isLocalUser) return context.getString(R.string.MessageRecord_you)
|
||||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
|
||||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||||
}
|
}
|
||||||
val address = user.address.serialize()
|
val address = user.address.serialize()
|
||||||
|
@@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.Disappear
|
|||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.UiState
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.UiState
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.ui.AppTheme
|
import org.thoughtcrime.securesms.ui.setThemedContent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -45,7 +45,7 @@ class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
setUpToolbar()
|
setUpToolbar()
|
||||||
|
|
||||||
binding.container.setContent { DisappearingMessagesScreen() }
|
binding.container.setThemedContent { DisappearingMessagesScreen() }
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
@@ -87,8 +87,6 @@ class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun DisappearingMessagesScreen() {
|
fun DisappearingMessagesScreen() {
|
||||||
val uiState by viewModel.uiState.collectAsState(UiState())
|
val uiState by viewModel.uiState.collectAsState(UiState())
|
||||||
AppTheme {
|
DisappearingMessages(uiState, callbacks = viewModel)
|
||||||
DisappearingMessages(uiState, callbacks = viewModel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,22 +11,21 @@ import androidx.compose.material.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
import org.thoughtcrime.securesms.ui.Callbacks
|
import org.thoughtcrime.securesms.ui.Callbacks
|
||||||
import org.thoughtcrime.securesms.ui.GetString
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
import org.thoughtcrime.securesms.ui.NoOpCallbacks
|
import org.thoughtcrime.securesms.ui.NoOpCallbacks
|
||||||
import org.thoughtcrime.securesms.ui.OptionsCard
|
import org.thoughtcrime.securesms.ui.OptionsCard
|
||||||
import org.thoughtcrime.securesms.ui.OutlineButton
|
|
||||||
import org.thoughtcrime.securesms.ui.RadioOption
|
import org.thoughtcrime.securesms.ui.RadioOption
|
||||||
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.contentDescription
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.extraSmall
|
||||||
import org.thoughtcrime.securesms.ui.fadingEdges
|
import org.thoughtcrime.securesms.ui.fadingEdges
|
||||||
|
|
||||||
typealias ExpiryCallbacks = Callbacks<ExpiryMode>
|
typealias ExpiryCallbacks = Callbacks<ExpiryMode>
|
||||||
@@ -40,33 +39,34 @@ fun DisappearingMessages(
|
|||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
Column(modifier = modifier.padding(horizontal = 32.dp)) {
|
Column(modifier = modifier.padding(horizontal = LocalDimensions.current.margin)) {
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 20.dp)
|
.padding(bottom = 20.dp)
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.fadingEdges(scrollState),
|
.fadingEdges(scrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallItemSpacing)
|
||||||
) {
|
) {
|
||||||
state.cards.forEach {
|
state.cards.forEach {
|
||||||
OptionsCard(it, callbacks)
|
OptionsCard(it, callbacks)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.showGroupFooter) Text(text = stringResource(R.string.activity_disappearing_messages_group_footer),
|
if (state.showGroupFooter) Text(
|
||||||
style = TextStyle(
|
text = stringResource(R.string.activity_disappearing_messages_group_footer),
|
||||||
fontSize = 11.sp,
|
style = extraSmall,
|
||||||
fontWeight = FontWeight(400),
|
fontWeight = FontWeight(400),
|
||||||
color = Color(0xFFA1A2A1),
|
color = LocalColors.current.textSecondary,
|
||||||
textAlign = TextAlign.Center),
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth())
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.showSetButton) OutlineButton(
|
if (state.showSetButton) SlimOutlineButton(
|
||||||
GetString(R.string.disappearing_messages_set_button_title),
|
stringResource(R.string.disappearing_messages_set_button_title),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.contentDescription(GetString(R.string.AccessibilityId_set_button))
|
.contentDescription(R.string.AccessibilityId_set_button)
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
.padding(bottom = 20.dp),
|
.padding(bottom = 20.dp),
|
||||||
onClick = callbacks::onSetClick
|
onClick = callbacks::onSetClick
|
||||||
|
@@ -7,19 +7,19 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.State
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.State
|
||||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
import org.thoughtcrime.securesms.ui.color.Colors
|
||||||
|
import org.thoughtcrime.securesms.ui.SessionColorsParameterProvider
|
||||||
|
|
||||||
@Preview(widthDp = 450, heightDp = 700)
|
@Preview(widthDp = 450, heightDp = 700)
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewStates(
|
fun PreviewStates(
|
||||||
@PreviewParameter(StatePreviewParameterProvider::class) state: State
|
@PreviewParameter(StatePreviewParameterProvider::class) state: State
|
||||||
) {
|
) {
|
||||||
PreviewTheme(R.style.Classic_Dark) {
|
PreviewTheme {
|
||||||
DisappearingMessages(
|
DisappearingMessages(
|
||||||
state.toUiState()
|
state.toUiState()
|
||||||
)
|
)
|
||||||
@@ -51,9 +51,9 @@ class StatePreviewParameterProvider : PreviewParameterProvider<State> {
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewThemes(
|
fun PreviewThemes(
|
||||||
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
|
||||||
) {
|
) {
|
||||||
PreviewTheme(themeResId) {
|
PreviewTheme(colors) {
|
||||||
DisappearingMessages(
|
DisappearingMessages(
|
||||||
State(expiryMode = ExpiryMode.AfterSend(43200)).toUiState(),
|
State(expiryMode = ExpiryMode.AfterSend(43200)).toUiState(),
|
||||||
modifier = Modifier.size(400.dp, 600.dp)
|
modifier = Modifier.size(400.dp, 600.dp)
|
||||||
|
@@ -1,129 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.paging
|
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import androidx.paging.Pager
|
|
||||||
import androidx.paging.PagingConfig
|
|
||||||
import androidx.paging.PagingSource
|
|
||||||
import androidx.paging.PagingState
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
|
||||||
|
|
||||||
private const val TIME_BUCKET = 600000L // bucket into 10 minute increments
|
|
||||||
|
|
||||||
private fun config() = PagingConfig(
|
|
||||||
pageSize = 25,
|
|
||||||
maxSize = 100,
|
|
||||||
enablePlaceholders = false
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Long.bucketed(): Long = (TIME_BUCKET - this % TIME_BUCKET) + this
|
|
||||||
|
|
||||||
fun conversationPager(threadId: Long, initialKey: PageLoad? = null, db: MmsSmsDatabase, contactDb: SessionContactDatabase) = Pager(config(), initialKey = initialKey) {
|
|
||||||
ConversationPagingSource(threadId, db, contactDb)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConversationPagerDiffCallback: DiffUtil.ItemCallback<MessageAndContact>() {
|
|
||||||
override fun areItemsTheSame(oldItem: MessageAndContact, newItem: MessageAndContact): Boolean =
|
|
||||||
oldItem.message.id == newItem.message.id && oldItem.message.isMms == newItem.message.isMms
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: MessageAndContact, newItem: MessageAndContact): Boolean =
|
|
||||||
oldItem == newItem
|
|
||||||
}
|
|
||||||
|
|
||||||
data class MessageAndContact(val message: MessageRecord,
|
|
||||||
val contact: Contact?)
|
|
||||||
|
|
||||||
data class PageLoad(val fromTime: Long, val toTime: Long? = null)
|
|
||||||
|
|
||||||
class ConversationPagingSource(
|
|
||||||
private val threadId: Long,
|
|
||||||
private val messageDb: MmsSmsDatabase,
|
|
||||||
private val contactDb: SessionContactDatabase
|
|
||||||
): PagingSource<PageLoad, MessageAndContact>() {
|
|
||||||
|
|
||||||
override fun getRefreshKey(state: PagingState<PageLoad, MessageAndContact>): PageLoad? {
|
|
||||||
val anchorPosition = state.anchorPosition ?: return null
|
|
||||||
val anchorPage = state.closestPageToPosition(anchorPosition) ?: return null
|
|
||||||
val next = anchorPage.nextKey?.fromTime
|
|
||||||
val previous = anchorPage.prevKey?.fromTime ?: anchorPage.data.firstOrNull()?.message?.dateSent ?: return null
|
|
||||||
return PageLoad(previous, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val contactCache = mutableMapOf<String, Contact>()
|
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
private fun getContact(sessionId: String): Contact? {
|
|
||||||
contactCache[sessionId]?.let { contact ->
|
|
||||||
return contact
|
|
||||||
} ?: run {
|
|
||||||
contactDb.getContactWithSessionID(sessionId)?.let { contact ->
|
|
||||||
contactCache[sessionId] = contact
|
|
||||||
return contact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun load(params: LoadParams<PageLoad>): LoadResult<PageLoad, MessageAndContact> {
|
|
||||||
val pageLoad = params.key ?: withContext(Dispatchers.IO) {
|
|
||||||
messageDb.getConversationSnippet(threadId).use {
|
|
||||||
val reader = messageDb.readerFor(it)
|
|
||||||
var record: MessageRecord? = null
|
|
||||||
if (reader != null) {
|
|
||||||
record = reader.next
|
|
||||||
while (record != null && record.isDeleted) {
|
|
||||||
record = reader.next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
record?.dateSent?.let { fromTime ->
|
|
||||||
PageLoad(fromTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return LoadResult.Page(emptyList(), null, null)
|
|
||||||
|
|
||||||
val result = withContext(Dispatchers.IO) {
|
|
||||||
val cursor = messageDb.getConversationPage(
|
|
||||||
threadId,
|
|
||||||
pageLoad.fromTime,
|
|
||||||
pageLoad.toTime ?: -1L,
|
|
||||||
params.loadSize
|
|
||||||
)
|
|
||||||
val processedList = mutableListOf<MessageAndContact>()
|
|
||||||
val reader = messageDb.readerFor(cursor)
|
|
||||||
while (reader.next != null && !invalid) {
|
|
||||||
reader.current?.let { item ->
|
|
||||||
val contact = getContact(item.individualRecipient.address.serialize())
|
|
||||||
processedList += MessageAndContact(item, contact)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.close()
|
|
||||||
processedList.toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasNext = withContext(Dispatchers.IO) {
|
|
||||||
if (result.isEmpty()) return@withContext false
|
|
||||||
val lastTime = result.last().message.dateSent
|
|
||||||
messageDb.hasNextPage(threadId, lastTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
val nextCheckTime = if (hasNext) {
|
|
||||||
val lastSent = result.last().message.dateSent
|
|
||||||
if (lastSent == pageLoad.fromTime) null else lastSent
|
|
||||||
} else null
|
|
||||||
|
|
||||||
val hasPrevious = withContext(Dispatchers.IO) { messageDb.hasPreviousPage(threadId, pageLoad.fromTime) }
|
|
||||||
val nextKey = if (!hasNext) null else nextCheckTime
|
|
||||||
val prevKey = if (!hasPrevious) null else messageDb.getPreviousPage(threadId, pageLoad.fromTime, params.loadSize)
|
|
||||||
|
|
||||||
return LoadResult.Page(
|
|
||||||
data = result, // next check time is not null if drop is true
|
|
||||||
prevKey = prevKey?.let { PageLoad(it, pageLoad.fromTime) },
|
|
||||||
nextKey = nextKey?.let { PageLoad(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,103 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.start
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import network.loki.messenger.databinding.ContactSectionHeaderBinding
|
|
||||||
import network.loki.messenger.databinding.ViewContactBinding
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
|
||||||
|
|
||||||
sealed class ContactListItem {
|
|
||||||
class Header(val name: String) : ContactListItem()
|
|
||||||
class Contact(val recipient: Recipient, val displayName: String) : ContactListItem()
|
|
||||||
}
|
|
||||||
|
|
||||||
class ContactListAdapter(
|
|
||||||
private val context: Context,
|
|
||||||
private val glide: GlideRequests,
|
|
||||||
private val listener: (Recipient) -> Unit
|
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|
||||||
var items = listOf<ContactListItem>()
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
private object ViewType {
|
|
||||||
const val Contact = 0
|
|
||||||
const val Header = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
class ContactViewHolder(private val binding: ViewContactBinding) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
fun bind(contact: ContactListItem.Contact, glide: GlideRequests, listener: (Recipient) -> Unit) {
|
|
||||||
binding.profilePictureView.update(contact.recipient)
|
|
||||||
binding.nameTextView.text = contact.displayName
|
|
||||||
binding.root.setOnClickListener { listener(contact.recipient) }
|
|
||||||
|
|
||||||
// TODO: When we implement deleting contacts (hide might be safest for now) then probably set a long-click listener here w/ something like:
|
|
||||||
/*
|
|
||||||
binding.root.setOnLongClickListener {
|
|
||||||
Log.w("[ACL]", "Long clicked on contact ${contact.recipient.name}")
|
|
||||||
binding.contentView.context.showSessionDialog {
|
|
||||||
title("Delete Contact")
|
|
||||||
text("Are you sure you want to delete this contact?")
|
|
||||||
button(R.string.delete) {
|
|
||||||
val contacts = configFactory.contacts ?: return
|
|
||||||
contacts.upsertContact(contact.recipient.address.serialize()) { priority = PRIORITY_HIDDEN }
|
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
|
||||||
endActionMode()
|
|
||||||
}
|
|
||||||
cancelButton(::endActionMode)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unbind() { binding.profilePictureView.recycle() }
|
|
||||||
}
|
|
||||||
|
|
||||||
class HeaderViewHolder(
|
|
||||||
private val binding: ContactSectionHeaderBinding
|
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
fun bind(item: ContactListItem.Header) {
|
|
||||||
with(binding) {
|
|
||||||
label.text = item.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int { return items.size }
|
|
||||||
|
|
||||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
|
||||||
super.onViewRecycled(holder)
|
|
||||||
if (holder is ContactViewHolder) { holder.unbind() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
return when (items[position]) {
|
|
||||||
is ContactListItem.Header -> ViewType.Header
|
|
||||||
else -> ViewType.Contact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
return if (viewType == ViewType.Contact) {
|
|
||||||
ContactViewHolder(ViewContactBinding.inflate(LayoutInflater.from(context), parent, false))
|
|
||||||
} else {
|
|
||||||
HeaderViewHolder(ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
val item = items[position]
|
|
||||||
if (viewHolder is ContactViewHolder) {
|
|
||||||
viewHolder.bind(item as ContactListItem.Contact, glide, listener)
|
|
||||||
} else if (viewHolder is HeaderViewHolder) {
|
|
||||||
viewHolder.bind(item as ContactListItem.Header)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,10 +1,21 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.start
|
package org.thoughtcrime.securesms.conversation.start
|
||||||
|
|
||||||
interface NewConversationDelegate {
|
interface StartConversationDelegate {
|
||||||
fun onNewMessageSelected()
|
fun onNewMessageSelected()
|
||||||
fun onCreateGroupSelected()
|
fun onCreateGroupSelected()
|
||||||
fun onJoinCommunitySelected()
|
fun onJoinCommunitySelected()
|
||||||
fun onContactSelected(address: String)
|
fun onContactSelected(address: String)
|
||||||
fun onDialogBackPressed()
|
fun onDialogBackPressed()
|
||||||
fun onDialogClosePressed()
|
fun onDialogClosePressed()
|
||||||
|
fun onInviteFriend()
|
||||||
|
}
|
||||||
|
|
||||||
|
object NullStartConversationDelegate: StartConversationDelegate {
|
||||||
|
override fun onNewMessageSelected() {}
|
||||||
|
override fun onCreateGroupSelected() {}
|
||||||
|
override fun onJoinCommunitySelected() {}
|
||||||
|
override fun onContactSelected(address: String) {}
|
||||||
|
override fun onDialogBackPressed() {}
|
||||||
|
override fun onDialogClosePressed() {}
|
||||||
|
override fun onInviteFriend() {}
|
||||||
}
|
}
|
@@ -7,6 +7,7 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.LayoutParams
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
@@ -15,13 +16,16 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.modifyLayoutParams
|
||||||
|
import org.thoughtcrime.securesms.conversation.start.home.StartConversationHomeFragment
|
||||||
|
import org.thoughtcrime.securesms.conversation.start.invitefriend.InviteFriendFragment
|
||||||
|
import org.thoughtcrime.securesms.conversation.start.newmessage.NewMessageFragment
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.dms.NewMessageFragment
|
|
||||||
import org.thoughtcrime.securesms.groups.CreateGroupFragment
|
import org.thoughtcrime.securesms.groups.CreateGroupFragment
|
||||||
import org.thoughtcrime.securesms.groups.JoinCommunityFragment
|
import org.thoughtcrime.securesms.groups.JoinCommunityFragment
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NewConversationFragment : BottomSheetDialogFragment(), NewConversationDelegate {
|
class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate {
|
||||||
|
|
||||||
private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * 0.94).toInt() }
|
private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * 0.94).toInt() }
|
||||||
|
|
||||||
@@ -35,38 +39,34 @@ class NewConversationFragment : BottomSheetDialogFragment(), NewConversationDele
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
replaceFragment(
|
replaceFragment(
|
||||||
fragment = NewConversationHomeFragment().apply { delegate = this@NewConversationFragment },
|
fragment = StartConversationHomeFragment().also { it.delegate.value = this },
|
||||||
fragmentKey = NewConversationHomeFragment::class.java.simpleName
|
fragmentKey = StartConversationHomeFragment::class.java.simpleName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||||
val dialog = BottomSheetDialog(requireContext(), R.style.Theme_Session_BottomSheet)
|
BottomSheetDialog(requireContext(), R.style.Theme_Session_BottomSheet).apply {
|
||||||
dialog.setOnShowListener {
|
setOnShowListener { _ ->
|
||||||
val bottomSheetDialog = it as BottomSheetDialog
|
findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)?.apply {
|
||||||
val parentLayout =
|
modifyLayoutParams<LayoutParams> { height = defaultPeekHeight }
|
||||||
bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
|
}?.let { BottomSheetBehavior.from(it) }?.apply {
|
||||||
parentLayout?.let { it ->
|
skipCollapsed = true
|
||||||
val behaviour = BottomSheetBehavior.from(it)
|
state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
val layoutParams = it.layoutParams
|
}
|
||||||
layoutParams.height = defaultPeekHeight
|
|
||||||
it.layoutParams = layoutParams
|
|
||||||
behaviour.state = BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewMessageSelected() {
|
override fun onNewMessageSelected() {
|
||||||
replaceFragment(NewMessageFragment().apply { delegate = this@NewConversationFragment })
|
replaceFragment(NewMessageFragment().also { it.delegate = this })
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateGroupSelected() {
|
override fun onCreateGroupSelected() {
|
||||||
replaceFragment(CreateGroupFragment().apply { delegate = this@NewConversationFragment })
|
replaceFragment(CreateGroupFragment().also { it.delegate = this })
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onJoinCommunitySelected() {
|
override fun onJoinCommunitySelected() {
|
||||||
replaceFragment(JoinCommunityFragment().apply { delegate = this@NewConversationFragment })
|
replaceFragment(JoinCommunityFragment().also { it.delegate = this })
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContactSelected(address: String) {
|
override fun onContactSelected(address: String) {
|
||||||
@@ -80,6 +80,10 @@ class NewConversationFragment : BottomSheetDialogFragment(), NewConversationDele
|
|||||||
childFragmentManager.popBackStack()
|
childFragmentManager.popBackStack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onInviteFriend() {
|
||||||
|
replaceFragment(InviteFriendFragment().also { it.delegate = this })
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDialogClosePressed() {
|
override fun onDialogClosePressed() {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
@@ -1,70 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.start
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.FragmentNewConversationHomeBinding
|
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.utilities.PublicKeyValidation
|
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class NewConversationHomeFragment : Fragment() {
|
|
||||||
|
|
||||||
private lateinit var binding: FragmentNewConversationHomeBinding
|
|
||||||
private val viewModel: NewConversationHomeViewModel by viewModels()
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var textSecurePreferences: TextSecurePreferences
|
|
||||||
|
|
||||||
lateinit var delegate: NewConversationDelegate
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
binding = FragmentNewConversationHomeBinding.inflate(inflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
|
||||||
binding.createPrivateChatButton.setOnClickListener { delegate.onNewMessageSelected() }
|
|
||||||
binding.createClosedGroupButton.setOnClickListener { delegate.onCreateGroupSelected() }
|
|
||||||
binding.joinCommunityButton.setOnClickListener { delegate.onJoinCommunitySelected() }
|
|
||||||
val adapter = ContactListAdapter(requireContext(), GlideApp.with(requireContext())) {
|
|
||||||
delegate.onContactSelected(it.address.serialize())
|
|
||||||
}
|
|
||||||
val unknownSectionTitle = getString(R.string.new_conversation_unknown_contacts_section_title)
|
|
||||||
val recipients = viewModel.recipients.value?.filter { !it.isGroupRecipient && it.address.serialize() != textSecurePreferences.getLocalNumber()!! } ?: emptyList()
|
|
||||||
val contactGroups = recipients.map {
|
|
||||||
val sessionId = it.address.serialize()
|
|
||||||
val contact = DatabaseComponent.get(requireContext()).sessionContactDatabase().getContactWithSessionID(sessionId)
|
|
||||||
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.firstOrNull()?.uppercase() ?: unknownSectionTitle }
|
|
||||||
.toMutableMap()
|
|
||||||
contactGroups.remove(unknownSectionTitle)?.let { contactGroups.put(unknownSectionTitle, it) }
|
|
||||||
adapter.items = contactGroups.flatMap { entry -> listOf(ContactListItem.Header(entry.key)) + entry.value }
|
|
||||||
binding.contactsRecyclerView.adapter = adapter
|
|
||||||
val divider = ContextCompat.getDrawable(requireActivity(), R.drawable.conversation_menu_divider)!!.let {
|
|
||||||
DividerItemDecoration(requireActivity(), RecyclerView.VERTICAL).apply {
|
|
||||||
setDrawable(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.contactsRecyclerView.addItemDecoration(divider)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.start
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class NewConversationHomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
|
|
||||||
|
|
||||||
private val _recipients = MutableLiveData<List<Recipient>>()
|
|
||||||
val recipients: LiveData<List<Recipient>> = _recipients
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
threadDb.approvedConversationList.use { openCursor ->
|
|
||||||
val reader = threadDb.readerFor(openCursor)
|
|
||||||
val threads = mutableListOf<Recipient>()
|
|
||||||
while (true) {
|
|
||||||
threads += reader.next?.recipient ?: break
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
_recipients.value = threads
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,111 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.start.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.conversation.start.NullStartConversationDelegate
|
||||||
|
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||||
|
import org.thoughtcrime.securesms.ui.Divider
|
||||||
|
import org.thoughtcrime.securesms.ui.ItemButton
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.SessionColorsParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.color.Colors
|
||||||
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.components.AppBar
|
||||||
|
import org.thoughtcrime.securesms.ui.components.QrImage
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.small
|
||||||
|
import org.thoughtcrime.securesms.ui.xl
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun StartConversationScreen(
|
||||||
|
accountId: String,
|
||||||
|
delegate: StartConversationDelegate
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.background(LocalColors.current.backgroundSecondary)) {
|
||||||
|
AppBar(stringResource(R.string.dialog_start_conversation_title), onClose = delegate::onDialogClosePressed)
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()),
|
||||||
|
color = LocalColors.current.backgroundSecondary
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
ItemButton(
|
||||||
|
textId = R.string.messageNew,
|
||||||
|
icon = R.drawable.ic_message,
|
||||||
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_new_direct_message),
|
||||||
|
onClick = delegate::onNewMessageSelected)
|
||||||
|
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||||
|
ItemButton(
|
||||||
|
textId = R.string.activity_create_group_title,
|
||||||
|
icon = R.drawable.ic_group,
|
||||||
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group),
|
||||||
|
onClick = delegate::onCreateGroupSelected
|
||||||
|
)
|
||||||
|
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||||
|
ItemButton(
|
||||||
|
textId = R.string.dialog_join_community_title,
|
||||||
|
icon = R.drawable.ic_globe,
|
||||||
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community),
|
||||||
|
onClick = delegate::onJoinCommunitySelected
|
||||||
|
)
|
||||||
|
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||||
|
ItemButton(
|
||||||
|
textId = R.string.activity_settings_invite_button_title,
|
||||||
|
icon = R.drawable.ic_invite_friend,
|
||||||
|
Modifier.contentDescription(R.string.AccessibilityId_invite_friend_button),
|
||||||
|
onClick = delegate::onInviteFriend
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = LocalDimensions.current.margin)
|
||||||
|
.padding(top = LocalDimensions.current.itemSpacing)
|
||||||
|
.padding(bottom = LocalDimensions.current.margin)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.accountIdYours), style = xl)
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsItemSpacing))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.qrYoursDescription),
|
||||||
|
color = LocalColors.current.textSecondary,
|
||||||
|
style = small
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.smallItemSpacing))
|
||||||
|
QrImage(
|
||||||
|
string = accountId,
|
||||||
|
Modifier.contentDescription(R.string.AccessibilityId_qr_code),
|
||||||
|
icon = R.drawable.session
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PreviewStartConversationScreen(
|
||||||
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
|
||||||
|
) {
|
||||||
|
PreviewTheme(colors) {
|
||||||
|
StartConversationScreen(
|
||||||
|
accountId = "059287129387123",
|
||||||
|
NullStartConversationDelegate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.start.home
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||||
|
import org.thoughtcrime.securesms.conversation.start.NullStartConversationDelegate
|
||||||
|
import org.thoughtcrime.securesms.ui.createThemedComposeView
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class StartConversationHomeFragment : Fragment() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var textSecurePreferences: TextSecurePreferences
|
||||||
|
|
||||||
|
var delegate = MutableStateFlow<StartConversationDelegate>(NullStartConversationDelegate)
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View = createThemedComposeView {
|
||||||
|
StartConversationScreen(
|
||||||
|
accountId = TextSecurePreferences.getLocalNumber(requireContext())!!,
|
||||||
|
delegate = delegate.collectAsState().value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,89 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.start.invitefriend
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.base
|
||||||
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.components.AppBar
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SlimOutlineCopyButton
|
||||||
|
import org.thoughtcrime.securesms.ui.components.border
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.small
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun InviteFriend(
|
||||||
|
accountId: String,
|
||||||
|
onBack: () -> Unit = {},
|
||||||
|
onClose: () -> Unit = {},
|
||||||
|
copyPublicKey: () -> Unit = {},
|
||||||
|
sendInvitation: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.background(LocalColors.current.backgroundSecondary)) {
|
||||||
|
AppBar(stringResource(R.string.invite_a_friend), onBack = onBack, onClose = onClose)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = LocalDimensions.current.itemSpacing),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
accountId,
|
||||||
|
modifier = Modifier
|
||||||
|
.contentDescription(R.string.AccessibilityId_account_id)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border()
|
||||||
|
.padding(LocalDimensions.current.smallMargin),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = base
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.xsItemSpacing))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.invite_your_friend_to_chat_with_you_on_session_by_sharing_your_account_id_with_them),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = small,
|
||||||
|
color = LocalColors.current.textSecondary,
|
||||||
|
modifier = Modifier.padding(horizontal = LocalDimensions.current.smallItemSpacing)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.smallItemSpacing))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = spacedBy(LocalDimensions.current.smallItemSpacing)) {
|
||||||
|
SlimOutlineButton(
|
||||||
|
stringResource(R.string.share),
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.contentDescription("Share button"),
|
||||||
|
onClick = sendInvitation
|
||||||
|
)
|
||||||
|
|
||||||
|
SlimOutlineCopyButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = copyPublicKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PreviewInviteFriend() {
|
||||||
|
PreviewTheme {
|
||||||
|
InviteFriend("050000000")
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.start.invitefriend
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||||
|
import org.thoughtcrime.securesms.preferences.copyPublicKey
|
||||||
|
import org.thoughtcrime.securesms.preferences.sendInvitationToUseSession
|
||||||
|
import org.thoughtcrime.securesms.ui.createThemedComposeView
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class InviteFriendFragment : Fragment() {
|
||||||
|
lateinit var delegate: StartConversationDelegate
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View = createThemedComposeView {
|
||||||
|
InviteFriend(
|
||||||
|
TextSecurePreferences.getLocalNumber(LocalContext.current)!!,
|
||||||
|
onBack = { delegate.onDialogBackPressed() },
|
||||||
|
onClose = { delegate.onDialogClosePressed() },
|
||||||
|
copyPublicKey = requireContext()::copyPublicKey,
|
||||||
|
sendInvitation = requireContext()::sendInvitationToUseSession,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.start.newmessage
|
||||||
|
|
||||||
|
internal interface Callbacks {
|
||||||
|
fun onChange(value: String) {}
|
||||||
|
fun onContinue() {}
|
||||||
|
fun onScanQrCode(value: String) {}
|
||||||
|
}
|
@@ -0,0 +1,135 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.start.newmessage
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.ui.LoadingArcOr
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.SessionColorsParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.color.Colors
|
||||||
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.components.AppBar
|
||||||
|
import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
|
||||||
|
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
||||||
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.small
|
||||||
|
|
||||||
|
private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun NewMessage(
|
||||||
|
state: State,
|
||||||
|
qrErrors: Flow<String> = emptyFlow(),
|
||||||
|
callbacks: Callbacks = object: Callbacks {},
|
||||||
|
onClose: () -> Unit = {},
|
||||||
|
onBack: () -> Unit = {},
|
||||||
|
onHelp: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val pagerState = rememberPagerState { TITLES.size }
|
||||||
|
|
||||||
|
Column(modifier = Modifier.background(LocalColors.current.backgroundSecondary)) {
|
||||||
|
AppBar(stringResource(R.string.messageNew), onClose = onClose, onBack = onBack)
|
||||||
|
SessionTabRow(pagerState, TITLES)
|
||||||
|
HorizontalPager(pagerState) {
|
||||||
|
when (TITLES[it]) {
|
||||||
|
R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp)
|
||||||
|
R.string.qrScan -> MaybeScanQrCode(qrErrors, onScan = callbacks::onScanQrCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EnterAccountId(
|
||||||
|
state: State,
|
||||||
|
callbacks: Callbacks,
|
||||||
|
onHelp: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.imePadding()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = LocalDimensions.current.xxsMargin, vertical = LocalDimensions.current.xsMargin),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.xsMargin)
|
||||||
|
) {
|
||||||
|
SessionOutlinedTextField(
|
||||||
|
text = state.newMessageIdOrOns,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = LocalDimensions.current.smallMargin),
|
||||||
|
contentDescription = "Session id input box",
|
||||||
|
placeholder = stringResource(R.string.accountIdOrOnsEnter),
|
||||||
|
onChange = callbacks::onChange,
|
||||||
|
onContinue = callbacks::onContinue,
|
||||||
|
error = state.error?.string(),
|
||||||
|
isTextErrorColor = state.isTextErrorColor
|
||||||
|
)
|
||||||
|
|
||||||
|
BorderlessButtonWithIcon(
|
||||||
|
text = stringResource(R.string.messageNewDescription),
|
||||||
|
modifier = Modifier
|
||||||
|
.contentDescription(R.string.AccessibilityId_help_desk_link)
|
||||||
|
.padding(horizontal = LocalDimensions.current.margin)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
style = small,
|
||||||
|
color = LocalColors.current.textSecondary,
|
||||||
|
iconRes = R.drawable.ic_circle_question_mark,
|
||||||
|
onClick = onHelp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(state.isNextButtonVisible) {
|
||||||
|
PrimaryOutlineButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(horizontal = LocalDimensions.current.largeMargin)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.contentDescription(R.string.next),
|
||||||
|
onClick = callbacks::onContinue
|
||||||
|
) {
|
||||||
|
LoadingArcOr(state.loading) {
|
||||||
|
Text(stringResource(R.string.next))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PreviewNewMessage(
|
||||||
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
|
||||||
|
) {
|
||||||
|
PreviewTheme(colors) {
|
||||||
|
NewMessage(State("z"))
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,61 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.start.newmessage
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
import org.thoughtcrime.securesms.openUrl
|
||||||
|
import org.thoughtcrime.securesms.ui.createThemedComposeView
|
||||||
|
|
||||||
|
class NewMessageFragment : Fragment() {
|
||||||
|
private val viewModel: NewMessageViewModel by viewModels()
|
||||||
|
|
||||||
|
lateinit var delegate: StartConversationDelegate
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.success.collect {
|
||||||
|
createPrivateChat(it.publicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View = createThemedComposeView {
|
||||||
|
val uiState by viewModel.state.collectAsState(State())
|
||||||
|
NewMessage(
|
||||||
|
uiState,
|
||||||
|
viewModel.qrErrors,
|
||||||
|
viewModel,
|
||||||
|
onClose = { delegate.onDialogClosePressed() },
|
||||||
|
onBack = { delegate.onDialogBackPressed() },
|
||||||
|
onHelp = { requireContext().openUrl("https://sessionapp.zendesk.com/hc/en-us/articles/4439132747033-How-do-Account-ID-usernames-work") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPrivateChat(hexEncodedPublicKey: String) {
|
||||||
|
val recipient = Recipient.from(requireContext(), Address.fromSerialized(hexEncodedPublicKey), false)
|
||||||
|
Intent(requireContext(), ConversationActivityV2::class.java).apply {
|
||||||
|
putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||||
|
setDataAndType(requireActivity().intent.data, requireActivity().intent.type)
|
||||||
|
putExtra(ConversationActivityV2.THREAD_ID, DatabaseComponent.get(requireContext()).threadDatabase().getThreadIdIfExistsFor(recipient))
|
||||||
|
}.let(requireContext()::startActivity)
|
||||||
|
delegate.onDialogClosePressed()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,118 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.start.newmessage
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
|
import org.session.libsignal.utilities.PublicKeyValidation
|
||||||
|
import org.session.libsignal.utilities.timeout
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
internal class NewMessageViewModel @Inject constructor(
|
||||||
|
private val application: Application
|
||||||
|
): AndroidViewModel(application), Callbacks {
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(State())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val _success = MutableSharedFlow<Success>()
|
||||||
|
val success get() = _success.asSharedFlow()
|
||||||
|
|
||||||
|
private val _qrErrors = MutableSharedFlow<String>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
val qrErrors = _qrErrors.asSharedFlow()
|
||||||
|
|
||||||
|
private var loadOnsJob: Job? = null
|
||||||
|
|
||||||
|
override fun onChange(value: String) {
|
||||||
|
loadOnsJob?.cancel()
|
||||||
|
loadOnsJob = null
|
||||||
|
|
||||||
|
_state.update { it.copy(newMessageIdOrOns = value, isTextErrorColor = false, loading = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContinue() {
|
||||||
|
val idOrONS = state.value.newMessageIdOrOns
|
||||||
|
|
||||||
|
if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) {
|
||||||
|
onUnvalidatedPublicKey(publicKey = idOrONS)
|
||||||
|
} else {
|
||||||
|
resolveONS(ons = idOrONS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScanQrCode(value: String) {
|
||||||
|
if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) {
|
||||||
|
onPublicKey(value)
|
||||||
|
} else {
|
||||||
|
_qrErrors.tryEmit(application.getString(R.string.this_qr_code_does_not_contain_an_account_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveONS(ons: String) {
|
||||||
|
if (loadOnsJob?.isActive == true) return
|
||||||
|
|
||||||
|
// This could be an ONS name
|
||||||
|
_state.update { it.copy(isTextErrorColor = false, error = null, loading = true) }
|
||||||
|
|
||||||
|
loadOnsJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val publicKey = SnodeAPI.getAccountID(ons).timeout(30_000).get()
|
||||||
|
if (isActive) onPublicKey(publicKey)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (isActive) onError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onError(e: Exception) {
|
||||||
|
_state.update { it.copy(loading = false, isTextErrorColor = true, error = GetString(e) { it.toMessage() }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPublicKey(publicKey: String) {
|
||||||
|
_state.update { it.copy(loading = false) }
|
||||||
|
viewModelScope.launch { _success.emit(Success(publicKey)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onUnvalidatedPublicKey(publicKey: String) {
|
||||||
|
if (PublicKeyValidation.hasValidPrefix(publicKey)) {
|
||||||
|
onPublicKey(publicKey)
|
||||||
|
} else {
|
||||||
|
_state.update { it.copy(isTextErrorColor = true, error = GetString(R.string.accountIdErrorInvalid), loading = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Exception.toMessage() = when (this) {
|
||||||
|
is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized)
|
||||||
|
is TimeoutException -> application.getString(R.string.onsErrorUnableToSearch)
|
||||||
|
else -> application.getString(R.string.fragment_enter_public_key_error_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class State(
|
||||||
|
val newMessageIdOrOns: String = "",
|
||||||
|
val isTextErrorColor: Boolean = false,
|
||||||
|
val error: GetString? = null,
|
||||||
|
val loading: Boolean = false
|
||||||
|
) {
|
||||||
|
val isNextButtonVisible: Boolean get() = newMessageIdOrOns.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class Success(val publicKey: String)
|
@@ -78,7 +78,7 @@ import org.session.libsession.messaging.sending_receiving.MessageSender
|
|||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
import org.session.libsession.messaging.utilities.AccountId
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
@@ -112,7 +112,6 @@ import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companio
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND
|
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
|
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
|
||||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
|
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate
|
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate
|
||||||
@@ -166,6 +165,7 @@ import org.thoughtcrime.securesms.mms.VideoSlide
|
|||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
|
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
|
||||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
|
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
|
||||||
|
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
@@ -173,13 +173,12 @@ import org.thoughtcrime.securesms.util.DateUtils
|
|||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import org.thoughtcrime.securesms.util.NetworkUtils
|
import org.thoughtcrime.securesms.util.NetworkUtils
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||||
import org.thoughtcrime.securesms.util.SimpleTextWatcher
|
|
||||||
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
||||||
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
|
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
|
import org.thoughtcrime.securesms.util.start
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import org.thoughtcrime.securesms.webrtc.NetworkChangeReceiver
|
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
@@ -240,12 +239,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
intent.getParcelableExtra<Address>(ADDRESS)?.let { it ->
|
intent.getParcelableExtra<Address>(ADDRESS)?.let { it ->
|
||||||
threadId = threadDb.getThreadIdIfExistsFor(it.serialize())
|
threadId = threadDb.getThreadIdIfExistsFor(it.serialize())
|
||||||
if (threadId == -1L) {
|
if (threadId == -1L) {
|
||||||
val sessionId = SessionId(it.serialize())
|
val accountId = AccountId(it.serialize())
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(intent.getLongExtra(FROM_GROUP_THREAD_ID, -1))
|
val openGroup = lokiThreadDb.getOpenGroupChat(intent.getLongExtra(FROM_GROUP_THREAD_ID, -1))
|
||||||
val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) {
|
val address = if (accountId.prefix == IdPrefix.BLINDED && openGroup != null) {
|
||||||
storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let {
|
storage.getOrCreateBlindedIdMapping(accountId.hexString, openGroup.server, openGroup.publicKey).accountId?.let {
|
||||||
fromSerialized(it)
|
fromSerialized(it)
|
||||||
} ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, sessionId)
|
} ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, accountId)
|
||||||
} else {
|
} else {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
@@ -733,12 +732,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
viewContainer.setTypists(recipients)
|
viewContainer.setTypists(recipients)
|
||||||
}
|
}
|
||||||
if (textSecurePreferences.isTypingIndicatorsEnabled()) {
|
if (textSecurePreferences.isTypingIndicatorsEnabled()) {
|
||||||
binding.inputBar.addTextChangedListener(object : SimpleTextWatcher() {
|
binding.inputBar.addTextChangedListener {
|
||||||
|
ApplicationContext.getInstance(this).typingStatusSender.onTypingStarted(viewModel.threadId)
|
||||||
override fun onTextChanged(text: String?) {
|
}
|
||||||
ApplicationContext.getInstance(this@ConversationActivityV2).typingStatusSender.onTypingStarted(viewModel.threadId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,8 +757,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
// called from onCreate
|
// called from onCreate
|
||||||
private fun setUpBlockedBanner() {
|
private fun setUpBlockedBanner() {
|
||||||
val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return
|
val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return
|
||||||
val sessionID = recipient.address.toString()
|
val accountID = recipient.address.toString()
|
||||||
val name = sessionContactDb.getContactWithSessionID(sessionID)?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
val name = sessionContactDb.getContactWithAccountID(accountID)?.displayName(Contact.ContactContext.REGULAR) ?: accountID
|
||||||
binding.blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
|
binding.blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
|
||||||
binding.blockedBanner.isVisible = recipient.isBlocked
|
binding.blockedBanner.isVisible = recipient.isBlocked
|
||||||
binding.blockedBanner.setOnClickListener { viewModel.unblock() }
|
binding.blockedBanner.setOnClickListener { viewModel.unblock() }
|
||||||
@@ -1135,8 +1131,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun copySessionID(sessionId: String) {
|
override fun copyAccountID(accountId: String) {
|
||||||
val clip = ClipData.newPlainText("Session ID", sessionId)
|
val clip = ClipData.newPlainText("Account ID", accountId)
|
||||||
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
@@ -1594,9 +1590,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val userPublicKey = textSecurePreferences.getLocalNumber()
|
val userPublicKey = textSecurePreferences.getLocalNumber()
|
||||||
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
|
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
|
||||||
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
|
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
|
||||||
val dialog = SendSeedDialog { sendTextOnlyMessage(true) }
|
start<RecoveryPasswordActivity>()
|
||||||
dialog.show(supportFragmentManager, "Send Seed Dialog")
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
// Create the message
|
// Create the message
|
||||||
val message = VisibleMessage().applyExpiryMode(viewModel.threadId)
|
val message = VisibleMessage().applyExpiryMode(viewModel.threadId)
|
||||||
@@ -2045,9 +2039,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
endActionMode()
|
endActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun copySessionID(messages: Set<MessageRecord>) {
|
override fun copyAccountID(messages: Set<MessageRecord>) {
|
||||||
val sessionID = messages.first().individualRecipient.address.toString()
|
val accountID = messages.first().individualRecipient.address.toString()
|
||||||
val clip = ClipData.newPlainText("Session ID", sessionID)
|
val clip = ClipData.newPlainText("Account ID", accountID)
|
||||||
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
@@ -2247,7 +2241,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
ConversationReactionOverlay.Action.DELETE -> deleteMessages(selectedItems)
|
ConversationReactionOverlay.Action.DELETE -> deleteMessages(selectedItems)
|
||||||
ConversationReactionOverlay.Action.BAN_AND_DELETE_ALL -> banAndDeleteAll(selectedItems)
|
ConversationReactionOverlay.Action.BAN_AND_DELETE_ALL -> banAndDeleteAll(selectedItems)
|
||||||
ConversationReactionOverlay.Action.BAN_USER -> banUser(selectedItems)
|
ConversationReactionOverlay.Action.BAN_USER -> banUser(selectedItems)
|
||||||
ConversationReactionOverlay.Action.COPY_SESSION_ID -> copySessionID(selectedItems)
|
ConversationReactionOverlay.Action.COPY_ACCOUNT_ID -> copyAccountID(selectedItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -70,7 +70,7 @@ class ConversationAdapter(
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getSenderInfo(sender: String): Contact? {
|
private fun getSenderInfo(sender: String): Contact? {
|
||||||
return contactDB.getContactWithSessionID(sender)
|
return contactDB.getContactWithAccountID(sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class ViewType(val rawValue: Int) {
|
sealed class ViewType(val rawValue: Int) {
|
||||||
|
@@ -539,9 +539,9 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
if (!containsControlMessage && hasText) {
|
if (!containsControlMessage && hasText) {
|
||||||
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
|
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
|
||||||
}
|
}
|
||||||
// Copy Session ID
|
// Copy Account ID
|
||||||
if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
|
if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
|
||||||
items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_session_id, { handleActionItemClicked(Action.COPY_SESSION_ID) })
|
items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_account_id, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
|
||||||
}
|
}
|
||||||
// Delete message
|
// Delete message
|
||||||
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||||
@@ -689,7 +689,7 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
RESYNC,
|
RESYNC,
|
||||||
DOWNLOAD,
|
DOWNLOAD,
|
||||||
COPY_MESSAGE,
|
COPY_MESSAGE,
|
||||||
COPY_SESSION_ID,
|
COPY_ACCOUNT_ID,
|
||||||
VIEW_INFO,
|
VIEW_INFO,
|
||||||
SELECT,
|
SELECT,
|
||||||
DELETE,
|
DELETE,
|
||||||
|
@@ -18,7 +18,7 @@ import org.session.libsession.messaging.messages.ExpirationConfiguration
|
|||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
import org.session.libsession.messaging.utilities.AccountId
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
@@ -77,7 +77,7 @@ class ConversationViewModel(
|
|||||||
val blindedPublicKey: String?
|
val blindedPublicKey: String?
|
||||||
get() = if (openGroup == null || edKeyPair == null || !serverCapabilities.contains(OpenGroupApi.Capability.BLIND.name.lowercase())) null else {
|
get() = if (openGroup == null || edKeyPair == null || !serverCapabilities.contains(OpenGroupApi.Capability.BLIND.name.lowercase())) null else {
|
||||||
SodiumUtilities.blindedKeyPair(openGroup!!.publicKey, edKeyPair)?.publicKey?.asBytes
|
SodiumUtilities.blindedKeyPair(openGroup!!.publicKey, edKeyPair)?.publicKey?.asBytes
|
||||||
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
|
?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString
|
||||||
}
|
}
|
||||||
|
|
||||||
val isMessageRequestThread : Boolean
|
val isMessageRequestThread : Boolean
|
||||||
|
@@ -26,7 +26,7 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
|||||||
val contact by lazy {
|
val contact by lazy {
|
||||||
val senderId = recipient.address.serialize()
|
val senderId = recipient.address.serialize()
|
||||||
// this dialog won't show for open group contacts
|
// this dialog won't show for open group contacts
|
||||||
contactDatabase.getContactWithSessionID(senderId)
|
contactDatabase.getContactWithAccountID(senderId)
|
||||||
?.displayName(Contact.ContactContext.REGULAR)
|
?.displayName(Contact.ContactContext.REGULAR)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -28,7 +28,6 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.LocalTextStyle
|
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -38,15 +37,11 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||||
@@ -59,7 +54,6 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt
|
|||||||
import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
|
import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.ui.AppTheme
|
|
||||||
import org.thoughtcrime.securesms.ui.Avatar
|
import org.thoughtcrime.securesms.ui.Avatar
|
||||||
import org.thoughtcrime.securesms.ui.CarouselNextButton
|
import org.thoughtcrime.securesms.ui.CarouselNextButton
|
||||||
import org.thoughtcrime.securesms.ui.CarouselPrevButton
|
import org.thoughtcrime.securesms.ui.CarouselPrevButton
|
||||||
@@ -70,12 +64,19 @@ import org.thoughtcrime.securesms.ui.Divider
|
|||||||
import org.thoughtcrime.securesms.ui.GetString
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator
|
import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator
|
||||||
import org.thoughtcrime.securesms.ui.ItemButton
|
import org.thoughtcrime.securesms.ui.ItemButton
|
||||||
|
import org.thoughtcrime.securesms.ui.LargeItemButton
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
import org.thoughtcrime.securesms.ui.SessionColorsParameterProvider
|
||||||
import org.thoughtcrime.securesms.ui.TitledText
|
import org.thoughtcrime.securesms.ui.TitledText
|
||||||
import org.thoughtcrime.securesms.ui.blackAlpha40
|
import org.thoughtcrime.securesms.ui.base
|
||||||
import org.thoughtcrime.securesms.ui.colorDestructive
|
import org.thoughtcrime.securesms.ui.baseBold
|
||||||
import org.thoughtcrime.securesms.ui.destructiveButtonColors
|
import org.thoughtcrime.securesms.ui.baseMonospace
|
||||||
|
import org.thoughtcrime.securesms.ui.color.Colors
|
||||||
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.color.blackAlpha40
|
||||||
|
import org.thoughtcrime.securesms.ui.color.destructiveButtonColors
|
||||||
|
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -102,9 +103,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
||||||
|
|
||||||
ComposeView(this)
|
setComposeContent { MessageDetailsScreen() }
|
||||||
.apply { setContent { MessageDetailsScreen() } }
|
|
||||||
.let(::setContentView)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.eventFlow.collect {
|
viewModel.eventFlow.collect {
|
||||||
@@ -121,16 +120,14 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun MessageDetailsScreen() {
|
private fun MessageDetailsScreen() {
|
||||||
val state by viewModel.stateFlow.collectAsState()
|
val state by viewModel.stateFlow.collectAsState()
|
||||||
AppTheme {
|
MessageDetails(
|
||||||
MessageDetails(
|
state = state,
|
||||||
state = state,
|
onReply = if (state.canReply) { { setResultAndFinish(ON_REPLY) } } else null,
|
||||||
onReply = if (state.canReply) { { setResultAndFinish(ON_REPLY) } } else null,
|
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
|
||||||
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
|
onDelete = { setResultAndFinish(ON_DELETE) },
|
||||||
onDelete = { setResultAndFinish(ON_DELETE) },
|
onClickImage = { viewModel.onClickImage(it) },
|
||||||
onClickImage = { viewModel.onClickImage(it) },
|
onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
|
||||||
onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setResultAndFinish(code: Int) {
|
private fun setResultAndFinish(code: Int) {
|
||||||
@@ -155,12 +152,12 @@ fun MessageDetails(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 16.dp),
|
.padding(vertical = LocalDimensions.current.smallItemSpacing),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallItemSpacing)
|
||||||
) {
|
) {
|
||||||
state.record?.let { message ->
|
state.record?.let { message ->
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier.padding(horizontal = 32.dp),
|
modifier = Modifier.padding(horizontal = LocalDimensions.current.margin),
|
||||||
factory = {
|
factory = {
|
||||||
ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply {
|
ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply {
|
||||||
bind(
|
bind(
|
||||||
@@ -196,7 +193,7 @@ fun CellMetadata(
|
|||||||
state.apply {
|
state.apply {
|
||||||
if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return
|
if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return
|
||||||
CellWithPaddingAndMargin {
|
CellWithPaddingAndMargin {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallItemSpacing)) {
|
||||||
TitledText(sent)
|
TitledText(sent)
|
||||||
TitledText(received)
|
TitledText(received)
|
||||||
TitledErrorText(error)
|
TitledErrorText(error)
|
||||||
@@ -222,23 +219,23 @@ fun CellButtons(
|
|||||||
Cell {
|
Cell {
|
||||||
Column {
|
Column {
|
||||||
onReply?.let {
|
onReply?.let {
|
||||||
ItemButton(
|
LargeItemButton(
|
||||||
stringResource(R.string.reply),
|
R.string.reply,
|
||||||
R.drawable.ic_message_details__reply,
|
R.drawable.ic_message_details__reply,
|
||||||
onClick = it
|
onClick = it
|
||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
onResend?.let {
|
onResend?.let {
|
||||||
ItemButton(
|
LargeItemButton(
|
||||||
stringResource(R.string.resend),
|
R.string.resend,
|
||||||
R.drawable.ic_message_details__refresh,
|
R.drawable.ic_message_details__refresh,
|
||||||
onClick = it
|
onClick = it
|
||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
ItemButton(
|
LargeItemButton(
|
||||||
stringResource(R.string.delete),
|
R.string.delete,
|
||||||
R.drawable.ic_message_details__trash,
|
R.drawable.ic_message_details__trash,
|
||||||
colors = destructiveButtonColors(),
|
colors = destructiveButtonColors(),
|
||||||
onClick = onDelete
|
onClick = onDelete
|
||||||
@@ -254,7 +251,7 @@ fun Carousel(attachments: List<Attachment>, onClick: (Int) -> Unit) {
|
|||||||
|
|
||||||
val pagerState = rememberPagerState { attachments.size }
|
val pagerState = rememberPagerState { attachments.size }
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallItemSpacing)) {
|
||||||
Row {
|
Row {
|
||||||
CarouselPrevButton(pagerState)
|
CarouselPrevButton(pagerState)
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
@@ -263,7 +260,7 @@ fun Carousel(attachments: List<Attachment>, onClick: (Int) -> Unit) {
|
|||||||
ExpandButton(
|
ExpandButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(8.dp)
|
.padding(LocalDimensions.current.xxsItemSpacing)
|
||||||
) { onClick(pagerState.currentPage) }
|
) { onClick(pagerState.currentPage) }
|
||||||
}
|
}
|
||||||
CarouselNextButton(pagerState)
|
CarouselNextButton(pagerState)
|
||||||
@@ -316,9 +313,9 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewMessageDetails(
|
fun PreviewMessageDetails(
|
||||||
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
|
||||||
) {
|
) {
|
||||||
PreviewTheme(themeResId) {
|
PreviewTheme(colors) {
|
||||||
MessageDetails(
|
MessageDetails(
|
||||||
state = MessageDetailsState(
|
state = MessageDetailsState(
|
||||||
nonImageAttachmentFileDetails = listOf(
|
nonImageAttachmentFileDetails = listOf(
|
||||||
@@ -341,10 +338,10 @@ fun PreviewMessageDetails(
|
|||||||
fun FileDetails(fileDetails: List<TitledText>) {
|
fun FileDetails(fileDetails: List<TitledText>) {
|
||||||
if (fileDetails.isEmpty()) return
|
if (fileDetails.isEmpty()) return
|
||||||
|
|
||||||
CellWithPaddingAndMargin(padding = 0.dp) {
|
Cell {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.padding(vertical = 24.dp, horizontal = 12.dp),
|
modifier = Modifier.padding(horizontal = LocalDimensions.current.xsItemSpacing, vertical = LocalDimensions.current.itemSpacing),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallItemSpacing)
|
||||||
) {
|
) {
|
||||||
fileDetails.forEach {
|
fileDetails.forEach {
|
||||||
BoxWithConstraints {
|
BoxWithConstraints {
|
||||||
@@ -352,7 +349,7 @@ fun FileDetails(fileDetails: List<TitledText>) {
|
|||||||
it,
|
it,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(min = maxWidth.div(2))
|
.widthIn(min = maxWidth.div(2))
|
||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = LocalDimensions.current.xsItemSpacing)
|
||||||
.width(IntrinsicSize.Max)
|
.width(IntrinsicSize.Max)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -365,7 +362,8 @@ fun FileDetails(fileDetails: List<TitledText>) {
|
|||||||
fun TitledErrorText(titledText: TitledText?) {
|
fun TitledErrorText(titledText: TitledText?) {
|
||||||
TitledText(
|
TitledText(
|
||||||
titledText,
|
titledText,
|
||||||
valueStyle = LocalTextStyle.current.copy(color = colorDestructive)
|
style = base,
|
||||||
|
color = LocalColors.current.danger
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +371,7 @@ fun TitledErrorText(titledText: TitledText?) {
|
|||||||
fun TitledMonospaceText(titledText: TitledText?) {
|
fun TitledMonospaceText(titledText: TitledText?) {
|
||||||
TitledText(
|
TitledText(
|
||||||
titledText,
|
titledText,
|
||||||
valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
|
style = baseMonospace
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,24 +379,25 @@ fun TitledMonospaceText(titledText: TitledText?) {
|
|||||||
fun TitledText(
|
fun TitledText(
|
||||||
titledText: TitledText?,
|
titledText: TitledText?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
valueStyle: TextStyle = LocalTextStyle.current,
|
style: TextStyle = base,
|
||||||
|
color: Color = Color.Unspecified
|
||||||
) {
|
) {
|
||||||
titledText?.apply {
|
titledText?.apply {
|
||||||
TitledView(title, modifier) {
|
TitledView(title, modifier) {
|
||||||
Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth())
|
Text(
|
||||||
|
text,
|
||||||
|
style = style,
|
||||||
|
color = color,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TitledView(title: GetString, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
fun TitledView(title: GetString, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
||||||
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.xxxsItemSpacing)) {
|
||||||
Title(title)
|
Text(title.string(), style = baseBold)
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Title(title: GetString) {
|
|
||||||
Text(title.string(), fontWeight = FontWeight.Bold)
|
|
||||||
}
|
|
||||||
|
@@ -20,9 +20,9 @@ class BlockedDialog(private val recipient: Recipient, private val context: Conte
|
|||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||||
val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
|
val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
|
||||||
val sessionID = recipient.address.toString()
|
val accountID = recipient.address.toString()
|
||||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
val contact = contactDB.getContactWithAccountID(accountID)
|
||||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
|
||||||
|
|
||||||
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
|
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
|
||||||
val spannable = SpannableStringBuilder(explanation)
|
val spannable = SpannableStringBuilder(explanation)
|
||||||
|
@@ -26,9 +26,9 @@ class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
|
|||||||
@Inject lateinit var contactDB: SessionContactDatabase
|
@Inject lateinit var contactDB: SessionContactDatabase
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||||
val sessionID = recipient.address.toString()
|
val accountID = recipient.address.toString()
|
||||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
val contact = contactDB.getContactWithAccountID(accountID)
|
||||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
|
||||||
title(resources.getString(R.string.dialog_download_title, name))
|
title(resources.getString(R.string.dialog_download_title, name))
|
||||||
|
|
||||||
val explanation = resources.getString(R.string.dialog_download_explanation, name)
|
val explanation = resources.getString(R.string.dialog_download_explanation, name)
|
||||||
@@ -42,8 +42,8 @@ class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun trust() {
|
private fun trust() {
|
||||||
val sessionID = recipient.address.toString()
|
val accountID = recipient.address.toString()
|
||||||
val contact = contactDB.getContactWithSessionID(sessionID) ?: return
|
val contact = contactDB.getContactWithAccountID(accountID) ?: return
|
||||||
val threadID = DatabaseComponent.get(requireContext()).threadDatabase().getThreadIdIfExistsFor(recipient)
|
val threadID = DatabaseComponent.get(requireContext()).threadDatabase().getThreadIdIfExistsFor(recipient)
|
||||||
contactDB.setContactIsTrusted(contact, true, threadID)
|
contactDB.setContactIsTrusted(contact, true, threadID)
|
||||||
JobQueue.shared.resumePendingJobs(AttachmentDownloadJob.KEY)
|
JobQueue.shared.resumePendingJobs(AttachmentDownloadJob.KEY)
|
||||||
|
@@ -2,12 +2,10 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -30,9 +28,8 @@ import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.util.addTextChangedListener
|
||||||
import org.thoughtcrime.securesms.util.contains
|
import org.thoughtcrime.securesms.util.contains
|
||||||
import org.thoughtcrime.securesms.util.toDp
|
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
|
||||||
|
|
||||||
// Enums to keep track of the state of our voice recording mechanism as the user can
|
// Enums to keep track of the state of our voice recording mechanism as the user can
|
||||||
// manipulate the UI faster than we can setup & teardown.
|
// manipulate the UI faster than we can setup & teardown.
|
||||||
@@ -43,16 +40,24 @@ enum class VoiceRecorderState {
|
|||||||
ShuttingDownAfterRecord
|
ShuttingDownAfterRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate,
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
class InputBar @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : RelativeLayout(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleAttr
|
||||||
|
), InputBarEditTextDelegate,
|
||||||
|
QuoteViewDelegate,
|
||||||
|
LinkPreviewDraftViewDelegate,
|
||||||
TextView.OnEditorActionListener {
|
TextView.OnEditorActionListener {
|
||||||
private lateinit var binding: ViewInputBarBinding
|
|
||||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
private var binding: ViewInputBarBinding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
private val vMargin by lazy { toDp(4, resources) }
|
|
||||||
private val minHeight by lazy { toPx(56, resources) }
|
|
||||||
private var linkPreviewDraftView: LinkPreviewDraftView? = null
|
private var linkPreviewDraftView: LinkPreviewDraftView? = null
|
||||||
private var quoteView: QuoteView? = null
|
private var quoteView: QuoteView? = null
|
||||||
var delegate: InputBarDelegate? = null
|
var delegate: InputBarDelegate? = null
|
||||||
var additionalContentHeight = 0
|
|
||||||
var quote: MessageRecord? = null
|
var quote: MessageRecord? = null
|
||||||
var linkPreview: LinkPreview? = null
|
var linkPreview: LinkPreview? = null
|
||||||
var showInput: Boolean = true
|
var showInput: Boolean = true
|
||||||
@@ -65,7 +70,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
}
|
}
|
||||||
|
|
||||||
var text: String
|
var text: String
|
||||||
get() { return binding.inputBarEditText.text?.toString() ?: "" }
|
get() = binding.inputBarEditText.text?.toString() ?: ""
|
||||||
set(value) { binding.inputBarEditText.setText(value) }
|
set(value) { binding.inputBarEditText.setText(value) }
|
||||||
|
|
||||||
// Keep track of when the user pressed the record voice message button, the duration that
|
// Keep track of when the user pressed the record voice message button, the duration that
|
||||||
@@ -74,21 +79,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
var voiceMessageDurationMS = 0L
|
var voiceMessageDurationMS = 0L
|
||||||
var voiceRecorderState = VoiceRecorderState.Idle
|
var voiceRecorderState = VoiceRecorderState.Idle
|
||||||
|
|
||||||
val attachmentButtonsContainerHeight: Int
|
private val attachmentsButton = InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachments_button)}
|
||||||
get() = binding.attachmentsButtonContainer.height
|
val microphoneButton = InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_microphone_button)}
|
||||||
|
private val sendButton = InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send_message_button)}
|
||||||
|
|
||||||
private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachments_button)} }
|
init {
|
||||||
private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_microphone_button)} }
|
|
||||||
private val sendButton by lazy { InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send_message_button)} }
|
|
||||||
|
|
||||||
// region Lifecycle
|
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
private fun initialize() {
|
|
||||||
binding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
// Attachments button
|
// Attachments button
|
||||||
binding.attachmentsButtonContainer.addView(attachmentsButton)
|
binding.attachmentsButtonContainer.addView(attachmentsButton)
|
||||||
attachmentsButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
attachmentsButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||||
@@ -107,6 +102,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
// `microphoneButton.onUp` and tap the button then the logged output order is onUp and THEN onPress!
|
// `microphoneButton.onUp` and tap the button then the logged output order is onUp and THEN onPress!
|
||||||
microphoneButton.setOnTouchListener(object : OnTouchListener {
|
microphoneButton.setOnTouchListener(object : OnTouchListener {
|
||||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||||
|
if (!microphoneButton.snIsEnabled) return true
|
||||||
|
|
||||||
// We only handle single finger touch events so just consume the event and bail if there are more
|
// We only handle single finger touch events so just consume the event and bail if there are more
|
||||||
if (event.pointerCount > 1) return true
|
if (event.pointerCount > 1) return true
|
||||||
@@ -157,12 +153,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
binding.inputBarEditText.setOnEditorActionListener(this)
|
binding.inputBarEditText.setOnEditorActionListener(this)
|
||||||
if (TextSecurePreferences.isEnterSendsEnabled(context)) {
|
if (TextSecurePreferences.isEnterSendsEnabled(context)) {
|
||||||
binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_SEND
|
binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_SEND
|
||||||
binding.inputBarEditText.inputType =
|
binding.inputBarEditText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||||
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
||||||
} else {
|
} else {
|
||||||
binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_NONE
|
binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_NONE
|
||||||
binding.inputBarEditText.inputType =
|
binding.inputBarEditText.inputType =
|
||||||
binding.inputBarEditText.inputType or
|
binding.inputBarEditText.inputType
|
||||||
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||||
}
|
}
|
||||||
val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0
|
val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0
|
||||||
@@ -179,9 +174,6 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Updating
|
|
||||||
override fun inputBarEditTextContentChanged(text: CharSequence) {
|
override fun inputBarEditTextContentChanged(text: CharSequence) {
|
||||||
microphoneButton.isVisible = text.trim().isEmpty()
|
microphoneButton.isVisible = text.trim().isEmpty()
|
||||||
sendButton.isVisible = microphoneButton.isGone
|
sendButton.isVisible = microphoneButton.isGone
|
||||||
@@ -276,19 +268,16 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
setOf(attachmentsButton, microphoneButton).forEach { it.snIsEnabled = showMediaControls }
|
setOf(attachmentsButton, microphoneButton).forEach { it.snIsEnabled = showMediaControls }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addTextChangedListener(textWatcher: TextWatcher) {
|
fun addTextChangedListener(listener: (String) -> Unit) {
|
||||||
binding.inputBarEditText.addTextChangedListener(textWatcher)
|
binding.inputBarEditText.addTextChangedListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setInputBarEditableFactory(factory: Editable.Factory) {
|
fun setInputBarEditableFactory(factory: Editable.Factory) {
|
||||||
binding.inputBarEditText.setEditableFactory(factory)
|
binding.inputBarEditText.setEditableFactory(factory)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputBarDelegate {
|
interface InputBarDelegate {
|
||||||
|
|
||||||
fun inputBarEditTextContentChanged(newContent: CharSequence)
|
fun inputBarEditTextContentChanged(newContent: CharSequence)
|
||||||
fun toggleAttachmentOptions()
|
fun toggleAttachmentOptions()
|
||||||
fun showVoiceMessageUI()
|
fun showVoiceMessageUI()
|
||||||
|
@@ -117,9 +117,9 @@ class MentionViewModel(
|
|||||||
|
|
||||||
contactDatabase.getContacts(memberIDs).map { contact ->
|
contactDatabase.getContacts(memberIDs).map { contact ->
|
||||||
Member(
|
Member(
|
||||||
publicKey = contact.sessionID,
|
publicKey = contact.accountID,
|
||||||
name = contact.displayName(contactContext).orEmpty(),
|
name = contact.displayName(contactContext).orEmpty(),
|
||||||
isModerator = contact.sessionID in moderatorIDs,
|
isModerator = contact.accountID in moderatorIDs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
import org.session.libsession.messaging.utilities.AccountId
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
@@ -39,7 +39,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
val edKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
|
val edKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
|
||||||
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
|
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
|
||||||
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
|
?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString
|
||||||
fun userCanDeleteSelectedItems(): Boolean {
|
fun userCanDeleteSelectedItems(): Boolean {
|
||||||
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
|
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
|
||||||
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
|
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
|
||||||
@@ -63,7 +63,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
menu.findItem(R.id.menu_context_ban_and_delete_all).isVisible = userCanBanSelectedUsers()
|
menu.findItem(R.id.menu_context_ban_and_delete_all).isVisible = userCanBanSelectedUsers()
|
||||||
// Copy message text
|
// Copy message text
|
||||||
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
|
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
|
||||||
// Copy Session ID
|
// Copy Account ID
|
||||||
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
||||||
(thread.isGroupRecipient && !thread.isCommunityRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
|
(thread.isGroupRecipient && !thread.isCommunityRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
|
||||||
// Message detail
|
// Message detail
|
||||||
@@ -91,7 +91,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
R.id.menu_context_ban_user -> delegate?.banUser(selectedItems)
|
R.id.menu_context_ban_user -> delegate?.banUser(selectedItems)
|
||||||
R.id.menu_context_ban_and_delete_all -> delegate?.banAndDeleteAll(selectedItems)
|
R.id.menu_context_ban_and_delete_all -> delegate?.banAndDeleteAll(selectedItems)
|
||||||
R.id.menu_context_copy -> delegate?.copyMessages(selectedItems)
|
R.id.menu_context_copy -> delegate?.copyMessages(selectedItems)
|
||||||
R.id.menu_context_copy_public_key -> delegate?.copySessionID(selectedItems)
|
R.id.menu_context_copy_public_key -> delegate?.copyAccountID(selectedItems)
|
||||||
R.id.menu_context_resync -> delegate?.resyncMessage(selectedItems)
|
R.id.menu_context_resync -> delegate?.resyncMessage(selectedItems)
|
||||||
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
|
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
|
||||||
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
|
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
|
||||||
@@ -115,7 +115,7 @@ interface ConversationActionModeCallbackDelegate {
|
|||||||
fun banUser(messages: Set<MessageRecord>)
|
fun banUser(messages: Set<MessageRecord>)
|
||||||
fun banAndDeleteAll(messages: Set<MessageRecord>)
|
fun banAndDeleteAll(messages: Set<MessageRecord>)
|
||||||
fun copyMessages(messages: Set<MessageRecord>)
|
fun copyMessages(messages: Set<MessageRecord>)
|
||||||
fun copySessionID(messages: Set<MessageRecord>)
|
fun copyAccountID(messages: Set<MessageRecord>)
|
||||||
fun resyncMessage(messages: Set<MessageRecord>)
|
fun resyncMessage(messages: Set<MessageRecord>)
|
||||||
fun resendMessage(messages: Set<MessageRecord>)
|
fun resendMessage(messages: Set<MessageRecord>)
|
||||||
fun showMessageDetail(messages: Set<MessageRecord>)
|
fun showMessageDetail(messages: Set<MessageRecord>)
|
||||||
|
@@ -57,9 +57,9 @@ object ConversationMenuHelper {
|
|||||||
if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) {
|
if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) {
|
||||||
inflater.inflate(R.menu.menu_conversation_expiration, menu)
|
inflater.inflate(R.menu.menu_conversation_expiration, menu)
|
||||||
}
|
}
|
||||||
// One-on-one chat menu allows copying the session id
|
// One-on-one chat menu allows copying the account id
|
||||||
if (thread.isContactRecipient) {
|
if (thread.isContactRecipient) {
|
||||||
inflater.inflate(R.menu.menu_conversation_copy_session_id, menu)
|
inflater.inflate(R.menu.menu_conversation_copy_account_id, menu)
|
||||||
}
|
}
|
||||||
// One-on-one chat menu (options that should only be present for one-on-one chats)
|
// One-on-one chat menu (options that should only be present for one-on-one chats)
|
||||||
if (thread.isContactRecipient) {
|
if (thread.isContactRecipient) {
|
||||||
@@ -135,7 +135,7 @@ object ConversationMenuHelper {
|
|||||||
R.id.menu_unblock -> { unblock(context, thread) }
|
R.id.menu_unblock -> { unblock(context, thread) }
|
||||||
R.id.menu_block -> { block(context, thread, deleteThread = false) }
|
R.id.menu_block -> { block(context, thread, deleteThread = false) }
|
||||||
R.id.menu_block_delete -> { blockAndDelete(context, thread) }
|
R.id.menu_block_delete -> { blockAndDelete(context, thread) }
|
||||||
R.id.menu_copy_session_id -> { copySessionID(context, thread) }
|
R.id.menu_copy_account_id -> { copyAccountID(context, thread) }
|
||||||
R.id.menu_copy_open_group_url -> { copyOpenGroupUrl(context, thread) }
|
R.id.menu_copy_open_group_url -> { copyOpenGroupUrl(context, thread) }
|
||||||
R.id.menu_edit_group -> { editClosedGroup(context, thread) }
|
R.id.menu_edit_group -> { editClosedGroup(context, thread) }
|
||||||
R.id.menu_leave_group -> { leaveClosedGroup(context, thread) }
|
R.id.menu_leave_group -> { leaveClosedGroup(context, thread) }
|
||||||
@@ -246,10 +246,10 @@ object ConversationMenuHelper {
|
|||||||
listener.block(deleteThread = true)
|
listener.block(deleteThread = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copySessionID(context: Context, thread: Recipient) {
|
private fun copyAccountID(context: Context, thread: Recipient) {
|
||||||
if (!thread.isContactRecipient) { return }
|
if (!thread.isContactRecipient) { return }
|
||||||
val listener = context as? ConversationMenuListener ?: return
|
val listener = context as? ConversationMenuListener ?: return
|
||||||
listener.copySessionID(thread.address.toString())
|
listener.copyAccountID(thread.address.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyOpenGroupUrl(context: Context, thread: Recipient) {
|
private fun copyOpenGroupUrl(context: Context, thread: Recipient) {
|
||||||
@@ -271,8 +271,8 @@ object ConversationMenuHelper {
|
|||||||
|
|
||||||
val group = DatabaseComponent.get(context).groupDatabase().getGroup(thread.address.toGroupString()).orNull()
|
val group = DatabaseComponent.get(context).groupDatabase().getGroup(thread.address.toGroupString()).orNull()
|
||||||
val admins = group.admins
|
val admins = group.admins
|
||||||
val sessionID = TextSecurePreferences.getLocalNumber(context)
|
val accountID = TextSecurePreferences.getLocalNumber(context)
|
||||||
val isCurrentUserAdmin = admins.any { it.toString() == sessionID }
|
val isCurrentUserAdmin = admins.any { it.toString() == accountID }
|
||||||
val message = if (isCurrentUserAdmin) {
|
val message = if (isCurrentUserAdmin) {
|
||||||
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
||||||
} else {
|
} else {
|
||||||
@@ -325,7 +325,7 @@ object ConversationMenuHelper {
|
|||||||
interface ConversationMenuListener {
|
interface ConversationMenuListener {
|
||||||
fun block(deleteThread: Boolean = false)
|
fun block(deleteThread: Boolean = false)
|
||||||
fun unblock()
|
fun unblock()
|
||||||
fun copySessionID(sessionId: String)
|
fun copyAccountID(accountId: String)
|
||||||
fun copyOpenGroupUrl(thread: Recipient)
|
fun copyOpenGroupUrl(thread: Recipient)
|
||||||
fun showDisappearingMessages(thread: Recipient)
|
fun showDisappearingMessages(thread: Recipient)
|
||||||
}
|
}
|
||||||
|
@@ -70,7 +70,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long,
|
isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long,
|
||||||
isOriginalMissing: Boolean, glide: GlideRequests) {
|
isOriginalMissing: Boolean, glide: GlideRequests) {
|
||||||
// Author
|
// Author
|
||||||
val author = contactDb.getContactWithSessionID(authorPublicKey)
|
val author = contactDb.getContactWithAccountID(authorPublicKey)
|
||||||
val localNumber = TextSecurePreferences.getLocalNumber(context)
|
val localNumber = TextSecurePreferences.getLocalNumber(context)
|
||||||
val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber
|
val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber
|
||||||
|
|
||||||
|
@@ -143,7 +143,7 @@ class VisibleMessageView : FrameLayout {
|
|||||||
glide: GlideRequests = GlideApp.with(this),
|
glide: GlideRequests = GlideApp.with(this),
|
||||||
searchQuery: String? = null,
|
searchQuery: String? = null,
|
||||||
contact: Contact? = null,
|
contact: Contact? = null,
|
||||||
senderSessionID: String,
|
senderAccountID: String,
|
||||||
lastSeen: Long,
|
lastSeen: Long,
|
||||||
delegate: VisibleMessageViewDelegate? = null,
|
delegate: VisibleMessageViewDelegate? = null,
|
||||||
onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit,
|
onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit,
|
||||||
@@ -178,30 +178,30 @@ class VisibleMessageView : FrameLayout {
|
|||||||
|
|
||||||
if (isGroupThread && !message.isOutgoing) {
|
if (isGroupThread && !message.isOutgoing) {
|
||||||
if (isEndOfMessageCluster) {
|
if (isEndOfMessageCluster) {
|
||||||
binding.profilePictureView.publicKey = senderSessionID
|
binding.profilePictureView.publicKey = senderAccountID
|
||||||
binding.profilePictureView.update(message.individualRecipient)
|
binding.profilePictureView.update(message.individualRecipient)
|
||||||
binding.profilePictureView.setOnClickListener {
|
binding.profilePictureView.setOnClickListener {
|
||||||
if (thread.isCommunityRecipient) {
|
if (thread.isCommunityRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
||||||
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
if (IdPrefix.fromValue(senderAccountID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
||||||
// TODO: support v2 soon
|
// TODO: support v2 soon
|
||||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
val intent = Intent(context, ConversationActivityV2::class.java)
|
||||||
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
|
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
|
||||||
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderSessionID))
|
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderAccountID))
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
maybeShowUserDetails(senderSessionID, threadID)
|
maybeShowUserDetails(senderAccountID, threadID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (thread.isCommunityRecipient) {
|
if (thread.isCommunityRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||||
var standardPublicKey = ""
|
var standardPublicKey = ""
|
||||||
var blindedPublicKey: String? = null
|
var blindedPublicKey: String? = null
|
||||||
if (IdPrefix.fromValue(senderSessionID)?.isBlinded() == true) {
|
if (IdPrefix.fromValue(senderAccountID)?.isBlinded() == true) {
|
||||||
blindedPublicKey = senderSessionID
|
blindedPublicKey = senderAccountID
|
||||||
} else {
|
} else {
|
||||||
standardPublicKey = senderSessionID
|
standardPublicKey = senderAccountID
|
||||||
}
|
}
|
||||||
val isModerator = OpenGroupManager.isUserModerator(context, openGroup.groupId, standardPublicKey, blindedPublicKey)
|
val isModerator = OpenGroupManager.isUserModerator(context, openGroup.groupId, standardPublicKey, blindedPublicKey)
|
||||||
binding.moderatorIconImageView.isVisible = !message.isOutgoing && isModerator
|
binding.moderatorIconImageView.isVisible = !message.isOutgoing && isModerator
|
||||||
@@ -211,7 +211,7 @@ class VisibleMessageView : FrameLayout {
|
|||||||
binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected))
|
binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected))
|
||||||
val contactContext =
|
val contactContext =
|
||||||
if (thread.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
if (thread.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||||
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
|
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderAccountID
|
||||||
|
|
||||||
// Unread marker
|
// Unread marker
|
||||||
val shouldShowUnreadMarker = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing
|
val shouldShowUnreadMarker = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing
|
||||||
|
@@ -62,7 +62,7 @@ object MentionUtilities {
|
|||||||
val userDisplayName: String? = if (isYou) {
|
val userDisplayName: String? = if (isYou) {
|
||||||
context.getString(R.string.MessageRecord_you)
|
context.getString(R.string.MessageRecord_you)
|
||||||
} else {
|
} else {
|
||||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
|
||||||
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
|
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
|
||||||
contact?.displayName(context) ?: truncateIdForDisplay(publicKey)
|
contact?.displayName(context) ?: truncateIdForDisplay(publicKey)
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ object MentionUtilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isYou(mentionedPublicKey: String, userPublicKey: String, openGroup: OpenGroup?): Boolean {
|
private fun isYou(mentionedPublicKey: String, userPublicKey: String, openGroup: OpenGroup?): Boolean {
|
||||||
val isUserBlindedPublicKey = openGroup?.let { SodiumUtilities.sessionId(userPublicKey, mentionedPublicKey, it.publicKey) } ?: false
|
val isUserBlindedPublicKey = openGroup?.let { SodiumUtilities.accountId(userPublicKey, mentionedPublicKey, it.publicKey) } ?: false
|
||||||
return mentionedPublicKey.equals(userPublicKey, ignoreCase = true) || isUserBlindedPublicKey
|
return mentionedPublicKey.equals(userPublicKey, ignoreCase = true) || isUserBlindedPublicKey
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -31,7 +31,7 @@ class BlindedIdMappingDatabase(context: Context, helper: SQLCipherOpenHelper) :
|
|||||||
private fun readBlindedIdMapping(cursor: Cursor): BlindedIdMapping {
|
private fun readBlindedIdMapping(cursor: Cursor): BlindedIdMapping {
|
||||||
return BlindedIdMapping(
|
return BlindedIdMapping(
|
||||||
blindedId = cursor.getString(cursor.getColumnIndexOrThrow(BLINDED_PK)),
|
blindedId = cursor.getString(cursor.getColumnIndexOrThrow(BLINDED_PK)),
|
||||||
sessionId = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(SESSION_PK)),
|
accountId = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(SESSION_PK)),
|
||||||
serverUrl = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_URL)),
|
serverUrl = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_URL)),
|
||||||
serverId = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_PK)),
|
serverId = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_PK)),
|
||||||
)
|
)
|
||||||
@@ -58,7 +58,7 @@ class BlindedIdMappingDatabase(context: Context, helper: SQLCipherOpenHelper) :
|
|||||||
try {
|
try {
|
||||||
val values = ContentValues().apply {
|
val values = ContentValues().apply {
|
||||||
put(BLINDED_PK, blindedIdMapping.blindedId)
|
put(BLINDED_PK, blindedIdMapping.blindedId)
|
||||||
put(SERVER_PK, blindedIdMapping.sessionId)
|
put(SERVER_PK, blindedIdMapping.accountId)
|
||||||
put(SERVER_URL, blindedIdMapping.serverUrl)
|
put(SERVER_URL, blindedIdMapping.serverUrl)
|
||||||
put(SERVER_PK, blindedIdMapping.serverId)
|
put(SERVER_PK, blindedIdMapping.serverId)
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import android.database.Cursor
|
|||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
import org.session.libsession.messaging.utilities.AccountId
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
@@ -15,7 +15,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val sessionContactTable = "session_contact_database"
|
private const val sessionContactTable = "session_contact_database"
|
||||||
const val sessionID = "session_id"
|
const val accountID = "session_id"
|
||||||
const val name = "name"
|
const val name = "name"
|
||||||
const val nickname = "nickname"
|
const val nickname = "nickname"
|
||||||
const val profilePictureURL = "profile_picture_url"
|
const val profilePictureURL = "profile_picture_url"
|
||||||
@@ -25,7 +25,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||||||
const val isTrusted = "is_trusted"
|
const val isTrusted = "is_trusted"
|
||||||
@JvmStatic val createSessionContactTableCommand =
|
@JvmStatic val createSessionContactTableCommand =
|
||||||
"CREATE TABLE $sessionContactTable " +
|
"CREATE TABLE $sessionContactTable " +
|
||||||
"($sessionID STRING PRIMARY KEY, " +
|
"($accountID STRING PRIMARY KEY, " +
|
||||||
"$name TEXT DEFAULT NULL, " +
|
"$name TEXT DEFAULT NULL, " +
|
||||||
"$nickname TEXT DEFAULT NULL, " +
|
"$nickname TEXT DEFAULT NULL, " +
|
||||||
"$profilePictureURL TEXT DEFAULT NULL, " +
|
"$profilePictureURL TEXT DEFAULT NULL, " +
|
||||||
@@ -35,19 +35,19 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||||||
"$isTrusted INTEGER DEFAULT 0);"
|
"$isTrusted INTEGER DEFAULT 0);"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContactWithSessionID(sessionID: String): Contact? {
|
fun getContactWithAccountID(accountID: String): Contact? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(sessionContactTable, "${Companion.sessionID} = ?", arrayOf( sessionID )) { cursor ->
|
return database.get(sessionContactTable, "${Companion.accountID} = ?", arrayOf( accountID )) { cursor ->
|
||||||
contactFromCursor(cursor)
|
contactFromCursor(cursor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContacts(sessionIDs: Collection<String>): List<Contact> {
|
fun getContacts(accountIDs: Collection<String>): List<Contact> {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.getAll(
|
return database.getAll(
|
||||||
sessionContactTable,
|
sessionContactTable,
|
||||||
"$sessionID IN (SELECT value FROM json_each(?))",
|
"$accountID IN (SELECT value FROM json_each(?))",
|
||||||
arrayOf(JSONArray(sessionIDs).toString())
|
arrayOf(JSONArray(accountIDs).toString())
|
||||||
) { cursor -> contactFromCursor(cursor) }
|
) { cursor -> contactFromCursor(cursor) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +56,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||||||
return database.getAll(sessionContactTable, null, null) { cursor ->
|
return database.getAll(sessionContactTable, null, null) { cursor ->
|
||||||
contactFromCursor(cursor)
|
contactFromCursor(cursor)
|
||||||
}.filter { contact ->
|
}.filter { contact ->
|
||||||
val sessionId = SessionId(contact.sessionID)
|
contact.accountID.let(::AccountId).prefix == IdPrefix.STANDARD
|
||||||
sessionId.prefix == IdPrefix.STANDARD
|
|
||||||
}.toSet()
|
}.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +64,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(1)
|
val contentValues = ContentValues(1)
|
||||||
contentValues.put(Companion.isTrusted, if (isTrusted) 1 else 0)
|
contentValues.put(Companion.isTrusted, if (isTrusted) 1 else 0)
|
||||||
database.update(sessionContactTable, contentValues, "$sessionID = ?", arrayOf( contact.sessionID ))
|
database.update(sessionContactTable, contentValues, "$accountID = ?", arrayOf( contact.accountID ))
|
||||||
if (threadID >= 0) {
|
if (threadID >= 0) {
|
||||||
notifyConversationListeners(threadID)
|
notifyConversationListeners(threadID)
|
||||||
}
|
}
|
||||||
@@ -75,7 +74,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||||||
fun setContact(contact: Contact) {
|
fun setContact(contact: Contact) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(8)
|
val contentValues = ContentValues(8)
|
||||||
contentValues.put(sessionID, contact.sessionID)
|
contentValues.put(accountID, contact.accountID)
|
||||||
contentValues.put(name, contact.name)
|
contentValues.put(name, contact.name)
|
||||||
contentValues.put(nickname, contact.nickname)
|
contentValues.put(nickname, contact.nickname)
|
||||||
contentValues.put(profilePictureURL, contact.profilePictureURL)
|
contentValues.put(profilePictureURL, contact.profilePictureURL)
|
||||||
@@ -85,13 +84,13 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||||||
}
|
}
|
||||||
contentValues.put(threadID, contact.threadID)
|
contentValues.put(threadID, contact.threadID)
|
||||||
contentValues.put(isTrusted, if (contact.isTrusted) 1 else 0)
|
contentValues.put(isTrusted, if (contact.isTrusted) 1 else 0)
|
||||||
database.insertOrUpdate(sessionContactTable, contentValues, "$sessionID = ?", arrayOf( contact.sessionID ))
|
database.insertOrUpdate(sessionContactTable, contentValues, "$accountID = ?", arrayOf( contact.accountID ))
|
||||||
notifyConversationListListeners()
|
notifyConversationListListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contactFromCursor(cursor: Cursor): Contact {
|
fun contactFromCursor(cursor: Cursor): Contact {
|
||||||
val sessionID = cursor.getString(cursor.getColumnIndexOrThrow(sessionID))
|
val accountID = cursor.getString(cursor.getColumnIndexOrThrow(accountID))
|
||||||
val contact = Contact(sessionID)
|
val contact = Contact(accountID)
|
||||||
contact.name = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(name))
|
contact.name = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(name))
|
||||||
contact.nickname = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(nickname))
|
contact.nickname = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(nickname))
|
||||||
contact.profilePictureURL = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureURL))
|
contact.profilePictureURL = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureURL))
|
||||||
|
@@ -6,6 +6,7 @@ import java.security.MessageDigest
|
|||||||
import network.loki.messenger.libsession_util.ConfigBase
|
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_HIDDEN
|
||||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED
|
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED
|
||||||
|
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE
|
||||||
import network.loki.messenger.libsession_util.Contacts
|
import network.loki.messenger.libsession_util.Contacts
|
||||||
import network.loki.messenger.libsession_util.ConversationVolatileConfig
|
import network.loki.messenger.libsession_util.ConversationVolatileConfig
|
||||||
import network.loki.messenger.libsession_util.UserGroupsConfig
|
import network.loki.messenger.libsession_util.UserGroupsConfig
|
||||||
@@ -57,7 +58,7 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
|
|||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
import org.session.libsession.messaging.utilities.AccountId
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsession.messaging.utilities.UpdateMessageData
|
import org.session.libsession.messaging.utilities.UpdateMessageData
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
@@ -68,6 +69,7 @@ import org.session.libsession.utilities.GroupRecord
|
|||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.ProfileKeyUtil
|
import org.session.libsession.utilities.ProfileKeyUtil
|
||||||
import org.session.libsession.utilities.SSKEnvironment
|
import org.session.libsession.utilities.SSKEnvironment
|
||||||
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsession.utilities.recipients.Recipient.DisappearingState
|
import org.session.libsession.utilities.recipients.Recipient.DisappearingState
|
||||||
@@ -110,12 +112,12 @@ open class Storage(
|
|||||||
if (address.isGroup) {
|
if (address.isGroup) {
|
||||||
val groups = configFactory.userGroups ?: return
|
val groups = configFactory.userGroups ?: return
|
||||||
if (address.isClosedGroup) {
|
if (address.isClosedGroup) {
|
||||||
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
val accountId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
||||||
val closedGroup = getGroup(address.toGroupString())
|
val closedGroup = getGroup(address.toGroupString())
|
||||||
if (closedGroup != null && closedGroup.isActive) {
|
if (closedGroup != null && closedGroup.isActive) {
|
||||||
val legacyGroup = groups.getOrConstructLegacyGroupInfo(sessionId)
|
val legacyGroup = groups.getOrConstructLegacyGroupInfo(accountId)
|
||||||
groups.set(legacyGroup)
|
groups.set(legacyGroup)
|
||||||
val newVolatileParams = volatile.getOrConstructLegacyGroup(sessionId).copy(
|
val newVolatileParams = volatile.getOrConstructLegacyGroup(accountId).copy(
|
||||||
lastRead = SnodeAPI.nowWithOffset,
|
lastRead = SnodeAPI.nowWithOffset,
|
||||||
)
|
)
|
||||||
volatile.set(newVolatileParams)
|
volatile.set(newVolatileParams)
|
||||||
@@ -126,16 +128,16 @@ open class Storage(
|
|||||||
}
|
}
|
||||||
} else if (address.isContact) {
|
} else if (address.isContact) {
|
||||||
// non-standard contact prefixes: 15, 00 etc shouldn't be stored in config
|
// non-standard contact prefixes: 15, 00 etc shouldn't be stored in config
|
||||||
if (SessionId(address.serialize()).prefix != IdPrefix.STANDARD) return
|
if (AccountId(address.serialize()).prefix != IdPrefix.STANDARD) return
|
||||||
// don't update our own address into the contacts DB
|
// don't update our own address into the contacts DB
|
||||||
if (getUserPublicKey() != address.serialize()) {
|
if (getUserPublicKey() != address.serialize()) {
|
||||||
val contacts = configFactory.contacts ?: return
|
val contacts = configFactory.contacts ?: return
|
||||||
contacts.upsertContact(address.serialize()) {
|
contacts.upsertContact(address.serialize()) {
|
||||||
priority = ConfigBase.PRIORITY_VISIBLE
|
priority = PRIORITY_VISIBLE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val userProfile = configFactory.user ?: return
|
val userProfile = configFactory.user ?: return
|
||||||
userProfile.setNtsPriority(ConfigBase.PRIORITY_VISIBLE)
|
userProfile.setNtsPriority(PRIORITY_VISIBLE)
|
||||||
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true)
|
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true)
|
||||||
}
|
}
|
||||||
val newVolatileParams = volatile.getOrConstructOneToOne(address.serialize())
|
val newVolatileParams = volatile.getOrConstructOneToOne(address.serialize())
|
||||||
@@ -148,16 +150,16 @@ open class Storage(
|
|||||||
if (address.isGroup) {
|
if (address.isGroup) {
|
||||||
val groups = configFactory.userGroups ?: return
|
val groups = configFactory.userGroups ?: return
|
||||||
if (address.isClosedGroup) {
|
if (address.isClosedGroup) {
|
||||||
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
val accountId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
||||||
volatile.eraseLegacyClosedGroup(sessionId)
|
volatile.eraseLegacyClosedGroup(accountId)
|
||||||
groups.eraseLegacyGroup(sessionId)
|
groups.eraseLegacyGroup(accountId)
|
||||||
} else if (address.isCommunity) {
|
} else if (address.isCommunity) {
|
||||||
// these should be removed in the group leave / handling new configs
|
// 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")
|
Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// non-standard contact prefixes: 15, 00 etc shouldn't be stored in config
|
// non-standard contact prefixes: 15, 00 etc shouldn't be stored in config
|
||||||
if (SessionId(address.serialize()).prefix != IdPrefix.STANDARD) return
|
if (AccountId(address.serialize()).prefix != IdPrefix.STANDARD) return
|
||||||
volatile.eraseOneToOne(address.serialize())
|
volatile.eraseOneToOne(address.serialize())
|
||||||
if (getUserPublicKey() != address.serialize()) {
|
if (getUserPublicKey() != address.serialize()) {
|
||||||
val contacts = configFactory.contacts ?: return
|
val contacts = configFactory.contacts ?: return
|
||||||
@@ -264,10 +266,8 @@ open class Storage(
|
|||||||
}
|
}
|
||||||
// otherwise recipient is one to one
|
// otherwise recipient is one to one
|
||||||
recipient.isContactRecipient -> {
|
recipient.isContactRecipient -> {
|
||||||
// don't process non-standard session IDs though
|
// don't process non-standard account IDs though
|
||||||
val sessionId = SessionId(recipient.address.serialize())
|
if (AccountId(recipient.address.serialize()).prefix != IdPrefix.STANDARD) return
|
||||||
if (sessionId.prefix != IdPrefix.STANDARD) return
|
|
||||||
|
|
||||||
config.getOrConstructOneToOne(recipient.address.serialize())
|
config.getOrConstructOneToOne(recipient.address.serialize())
|
||||||
}
|
}
|
||||||
else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.serialize()}")
|
else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.serialize()}")
|
||||||
@@ -298,8 +298,8 @@ open class Storage(
|
|||||||
var messageID: Long? = null
|
var messageID: Long? = null
|
||||||
val senderAddress = fromSerialized(message.sender!!)
|
val senderAddress = fromSerialized(message.sender!!)
|
||||||
val isUserSender = (message.sender!! == getUserPublicKey())
|
val isUserSender = (message.sender!! == getUserPublicKey())
|
||||||
val isUserBlindedSender = message.threadID?.takeIf { it >= 0 }?.let { getOpenGroup(it)?.publicKey }
|
val isUserBlindedSender = message.threadID?.takeIf { it >= 0 }?.let(::getOpenGroup)?.publicKey
|
||||||
?.let { SodiumUtilities.sessionId(getUserPublicKey()!!, message.sender!!, it) } ?: false
|
?.let { SodiumUtilities.accountId(getUserPublicKey()!!, message.sender!!, it) } ?: false
|
||||||
val group: Optional<SignalServiceGroup> = when {
|
val group: Optional<SignalServiceGroup> = when {
|
||||||
openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT))
|
openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT))
|
||||||
groupPublicKey != null -> {
|
groupPublicKey != null -> {
|
||||||
@@ -476,9 +476,11 @@ open class Storage(
|
|||||||
val name = userProfile.getName() ?: return
|
val name = userProfile.getName() ?: return
|
||||||
val userPic = userProfile.getPic()
|
val userPic = userProfile.getPic()
|
||||||
val profileManager = SSKEnvironment.shared.profileManager
|
val profileManager = SSKEnvironment.shared.profileManager
|
||||||
if (name.isNotEmpty()) {
|
|
||||||
TextSecurePreferences.setProfileName(context, name)
|
name.takeUnless { it.isEmpty() }?.truncate(NAME_PADDED_LENGTH)?.let {
|
||||||
profileManager.setName(context, recipient, name)
|
TextSecurePreferences.setProfileName(context, it)
|
||||||
|
profileManager.setName(context, recipient, it)
|
||||||
|
if (it != name) userProfile.setName(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update profile picture
|
// Update profile picture
|
||||||
@@ -537,7 +539,7 @@ open class Storage(
|
|||||||
val extracted = convos.all()
|
val extracted = convos.all()
|
||||||
for (conversation in extracted) {
|
for (conversation in extracted) {
|
||||||
val threadId = when (conversation) {
|
val threadId = when (conversation) {
|
||||||
is Conversation.OneToOne -> getThreadIdFor(conversation.sessionId, null, null, createThread = false)
|
is Conversation.OneToOne -> getThreadIdFor(conversation.accountId, null, null, createThread = false)
|
||||||
is Conversation.LegacyGroup -> getThreadIdFor("", conversation.groupId,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)
|
is Conversation.Community -> getThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl.removeSuffix("/")}.${conversation.baseCommunityInfo.room}", createThread = false)
|
||||||
}
|
}
|
||||||
@@ -568,7 +570,7 @@ open class Storage(
|
|||||||
val existingJoinUrls = existingCommunities.values.map { it.joinURL }
|
val existingJoinUrls = existingCommunities.values.map { it.joinURL }
|
||||||
|
|
||||||
val existingClosedGroups = getAllGroups(includeInactive = true).filter { it.isClosedGroup }
|
val existingClosedGroups = getAllGroups(includeInactive = true).filter { it.isClosedGroup }
|
||||||
val lgcIds = lgc.map { it.sessionId }
|
val lgcIds = lgc.map { it.accountId }
|
||||||
val toDeleteClosedGroups = existingClosedGroups.filter { group ->
|
val toDeleteClosedGroups = existingClosedGroups.filter { group ->
|
||||||
GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds
|
GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds
|
||||||
}
|
}
|
||||||
@@ -602,8 +604,8 @@ open class Storage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (group in lgc) {
|
for (group in lgc) {
|
||||||
val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId)
|
val groupId = GroupUtil.doubleEncodeGroupID(group.accountId)
|
||||||
val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.sessionId }
|
val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.accountId }
|
||||||
val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) }
|
val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) }
|
||||||
if (existingGroup != null) {
|
if (existingGroup != null) {
|
||||||
if (group.priority == PRIORITY_HIDDEN && existingThread != null) {
|
if (group.priority == PRIORITY_HIDDEN && existingThread != null) {
|
||||||
@@ -622,19 +624,19 @@ open class Storage(
|
|||||||
createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp)
|
createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp)
|
||||||
setProfileSharing(Address.fromSerialized(groupId), true)
|
setProfileSharing(Address.fromSerialized(groupId), true)
|
||||||
// Add the group to the user's set of public keys to poll for
|
// Add the group to the user's set of public keys to poll for
|
||||||
addClosedGroupPublicKey(group.sessionId)
|
addClosedGroupPublicKey(group.accountId)
|
||||||
// Store the encryption key pair
|
// Store the encryption key pair
|
||||||
val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey))
|
val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey))
|
||||||
addClosedGroupEncryptionKeyPair(keyPair, group.sessionId, SnodeAPI.nowWithOffset)
|
addClosedGroupEncryptionKeyPair(keyPair, group.accountId, SnodeAPI.nowWithOffset)
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
PushRegistryV1.subscribeGroup(group.sessionId, publicKey = localUserPublicKey)
|
PushRegistryV1.subscribeGroup(group.accountId, publicKey = localUserPublicKey)
|
||||||
// Notify the user
|
// Notify the user
|
||||||
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
||||||
threadDb.setDate(threadID, formationTimestamp)
|
threadDb.setDate(threadID, formationTimestamp)
|
||||||
insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, 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
|
// Don't create config group here, it's from a config update
|
||||||
// Start polling
|
// Start polling
|
||||||
ClosedGroupPollerV2.shared.startPolling(group.sessionId)
|
ClosedGroupPollerV2.shared.startPolling(group.accountId)
|
||||||
}
|
}
|
||||||
getThreadId(Address.fromSerialized(groupId))?.let {
|
getThreadId(Address.fromSerialized(groupId))?.let {
|
||||||
setExpirationConfiguration(
|
setExpirationConfiguration(
|
||||||
@@ -935,10 +937,10 @@ open class Storage(
|
|||||||
groupVolatileConfig.lastRead = formationTimestamp
|
groupVolatileConfig.lastRead = formationTimestamp
|
||||||
volatiles.set(groupVolatileConfig)
|
volatiles.set(groupVolatileConfig)
|
||||||
val groupInfo = GroupInfo.LegacyGroupInfo(
|
val groupInfo = GroupInfo.LegacyGroupInfo(
|
||||||
sessionId = groupPublicKey,
|
accountId = groupPublicKey,
|
||||||
name = name,
|
name = name,
|
||||||
members = members,
|
members = members,
|
||||||
priority = ConfigBase.PRIORITY_VISIBLE,
|
priority = PRIORITY_VISIBLE,
|
||||||
encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
|
encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
|
||||||
encSecKey = encryptionKeyPair.privateKey.serialize(),
|
encSecKey = encryptionKeyPair.privateKey.serialize(),
|
||||||
disappearingTimer = expirationTimer.toLong(),
|
disappearingTimer = expirationTimer.toLong(),
|
||||||
@@ -972,7 +974,7 @@ open class Storage(
|
|||||||
members = membersMap,
|
members = membersMap,
|
||||||
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
|
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
|
||||||
encSecKey = latestKeyPair.privateKey.serialize(),
|
encSecKey = latestKeyPair.privateKey.serialize(),
|
||||||
priority = if (isPinned(threadID)) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
|
priority = if (isPinned(threadID)) PRIORITY_PINNED else PRIORITY_VISIBLE,
|
||||||
disappearingTimer = getExpirationConfiguration(threadID)?.expiryMode?.expirySeconds ?: 0L,
|
disappearingTimer = getExpirationConfiguration(threadID)?.expiryMode?.expirySeconds ?: 0L,
|
||||||
joinedAt = (existingGroup.formationTimestamp / 1000L)
|
joinedAt = (existingGroup.formationTimestamp / 1000L)
|
||||||
)
|
)
|
||||||
@@ -1177,8 +1179,8 @@ open class Storage(
|
|||||||
return threadId ?: -1
|
return threadId ?: -1
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getContactWithSessionID(sessionID: String): Contact? {
|
override fun getContactWithAccountID(accountID: String): Contact? {
|
||||||
return DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(sessionID)
|
return DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllContacts(): Set<Contact> {
|
override fun getAllContacts(): Set<Contact> {
|
||||||
@@ -1187,7 +1189,7 @@ open class Storage(
|
|||||||
|
|
||||||
override fun setContact(contact: Contact) {
|
override fun setContact(contact: Contact) {
|
||||||
DatabaseComponent.get(context).sessionContactDatabase().setContact(contact)
|
DatabaseComponent.get(context).sessionContactDatabase().setContact(contact)
|
||||||
val address = fromSerialized(contact.sessionID)
|
val address = fromSerialized(contact.accountID)
|
||||||
if (!getRecipientApproved(address)) return
|
if (!getRecipientApproved(address)) return
|
||||||
val recipientHash = SSKEnvironment.shared.profileManager.contactUpdatedInternal(contact)
|
val recipientHash = SSKEnvironment.shared.profileManager.contactUpdatedInternal(contact)
|
||||||
val recipient = Recipient.from(context, address, false)
|
val recipient = Recipient.from(context, address, false)
|
||||||
@@ -1205,8 +1207,8 @@ open class Storage(
|
|||||||
override fun addLibSessionContacts(contacts: List<LibSessionContact>, timestamp: Long) {
|
override fun addLibSessionContacts(contacts: List<LibSessionContact>, timestamp: Long) {
|
||||||
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
||||||
val moreContacts = contacts.filter { contact ->
|
val moreContacts = contacts.filter { contact ->
|
||||||
val id = SessionId(contact.id)
|
val id = AccountId(contact.id)
|
||||||
id.prefix?.isBlinded() == false || mappingDb.getBlindedIdMapping(contact.id).none { it.sessionId != null }
|
id.prefix?.isBlinded() == false || mappingDb.getBlindedIdMapping(contact.id).none { it.accountId != null }
|
||||||
}
|
}
|
||||||
val profileManager = SSKEnvironment.shared.profileManager
|
val profileManager = SSKEnvironment.shared.profileManager
|
||||||
moreContacts.forEach { contact ->
|
moreContacts.forEach { contact ->
|
||||||
@@ -1258,8 +1260,8 @@ open class Storage(
|
|||||||
val threadDatabase = DatabaseComponent.get(context).threadDatabase()
|
val threadDatabase = DatabaseComponent.get(context).threadDatabase()
|
||||||
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
||||||
val moreContacts = contacts.filter { contact ->
|
val moreContacts = contacts.filter { contact ->
|
||||||
val id = SessionId(contact.publicKey)
|
val id = AccountId(contact.publicKey)
|
||||||
id.prefix != IdPrefix.BLINDED || mappingDb.getBlindedIdMapping(contact.publicKey).none { it.sessionId != null }
|
id.prefix != IdPrefix.BLINDED || mappingDb.getBlindedIdMapping(contact.publicKey).none { it.accountId != null }
|
||||||
}
|
}
|
||||||
for (contact in moreContacts) {
|
for (contact in moreContacts) {
|
||||||
val address = fromSerialized(contact.publicKey)
|
val address = fromSerialized(contact.publicKey)
|
||||||
@@ -1326,25 +1328,25 @@ open class Storage(
|
|||||||
val threadRecipient = getRecipientForThread(threadID) ?: return
|
val threadRecipient = getRecipientForThread(threadID) ?: return
|
||||||
if (threadRecipient.isLocalNumber) {
|
if (threadRecipient.isLocalNumber) {
|
||||||
val user = configFactory.user ?: return
|
val user = configFactory.user ?: return
|
||||||
user.setNtsPriority(if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE)
|
user.setNtsPriority(if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE)
|
||||||
} else if (threadRecipient.isContactRecipient) {
|
} else if (threadRecipient.isContactRecipient) {
|
||||||
val contacts = configFactory.contacts ?: return
|
val contacts = configFactory.contacts ?: return
|
||||||
contacts.upsertContact(threadRecipient.address.serialize()) {
|
contacts.upsertContact(threadRecipient.address.serialize()) {
|
||||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE
|
||||||
}
|
}
|
||||||
} else if (threadRecipient.isGroupRecipient) {
|
} else if (threadRecipient.isGroupRecipient) {
|
||||||
val groups = configFactory.userGroups ?: return
|
val groups = configFactory.userGroups ?: return
|
||||||
if (threadRecipient.isClosedGroupRecipient) {
|
if (threadRecipient.isClosedGroupRecipient) {
|
||||||
val sessionId = GroupUtil.doubleDecodeGroupId(threadRecipient.address.serialize())
|
threadRecipient.address.serialize()
|
||||||
val newGroupInfo = groups.getOrConstructLegacyGroupInfo(sessionId).copy (
|
.let(GroupUtil::doubleDecodeGroupId)
|
||||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
.let(groups::getOrConstructLegacyGroupInfo)
|
||||||
)
|
.copy (priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE)
|
||||||
groups.set(newGroupInfo)
|
.let(groups::set)
|
||||||
} else if (threadRecipient.isCommunityRecipient) {
|
} else if (threadRecipient.isCommunityRecipient) {
|
||||||
val openGroup = getOpenGroup(threadID) ?: return
|
val openGroup = getOpenGroup(threadID) ?: return
|
||||||
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
|
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
|
||||||
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
|
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
|
||||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE
|
||||||
)
|
)
|
||||||
groups.set(newGroupInfo)
|
groups.set(newGroupInfo)
|
||||||
}
|
}
|
||||||
@@ -1493,14 +1495,8 @@ open class Storage(
|
|||||||
val address = recipient.address.serialize()
|
val address = recipient.address.serialize()
|
||||||
val blindedId = when {
|
val blindedId = when {
|
||||||
recipient.isGroupRecipient -> null
|
recipient.isGroupRecipient -> null
|
||||||
recipient.isOpenGroupInboxRecipient -> {
|
recipient.isOpenGroupInboxRecipient -> GroupUtil.getDecodedOpenGroupInboxAccountId(address)
|
||||||
GroupUtil.getDecodedOpenGroupInboxSessionId(address)
|
else -> address.takeIf { AccountId(it).prefix == IdPrefix.BLINDED }
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
if (SessionId(address).prefix == IdPrefix.BLINDED) {
|
|
||||||
address
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
} ?: continue
|
} ?: continue
|
||||||
mappingDb.getBlindedIdMapping(blindedId).firstOrNull()?.let {
|
mappingDb.getBlindedIdMapping(blindedId).firstOrNull()?.let {
|
||||||
mappings[address] = it
|
mappings[address] = it
|
||||||
@@ -1508,18 +1504,18 @@ open class Storage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (mapping in mappings) {
|
for (mapping in mappings) {
|
||||||
if (!SodiumUtilities.sessionId(senderPublicKey, mapping.value.blindedId, mapping.value.serverId)) {
|
if (!SodiumUtilities.accountId(senderPublicKey, mapping.value.blindedId, mapping.value.serverId)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mappingDb.addBlindedIdMapping(mapping.value.copy(sessionId = senderPublicKey))
|
mappingDb.addBlindedIdMapping(mapping.value.copy(accountId = senderPublicKey))
|
||||||
|
|
||||||
val blindedThreadId = threadDB.getOrCreateThreadIdFor(Recipient.from(context, fromSerialized(mapping.key), false))
|
val blindedThreadId = threadDB.getOrCreateThreadIdFor(Recipient.from(context, fromSerialized(mapping.key), false))
|
||||||
mmsDb.updateThreadId(blindedThreadId, threadId)
|
mmsDb.updateThreadId(blindedThreadId, threadId)
|
||||||
smsDb.updateThreadId(blindedThreadId, threadId)
|
smsDb.updateThreadId(blindedThreadId, threadId)
|
||||||
threadDB.deleteConversation(blindedThreadId)
|
threadDB.deleteConversation(blindedThreadId)
|
||||||
}
|
}
|
||||||
recipientDb.setApproved(sender, true)
|
setRecipientApproved(sender, true)
|
||||||
recipientDb.setApprovedMe(sender, true)
|
setRecipientApprovedMe(sender, true)
|
||||||
val message = IncomingMediaMessage(
|
val message = IncomingMediaMessage(
|
||||||
sender.address,
|
sender.address,
|
||||||
response.sentTimestamp!!,
|
response.sentTimestamp!!,
|
||||||
@@ -1617,20 +1613,20 @@ open class Storage(
|
|||||||
): BlindedIdMapping {
|
): BlindedIdMapping {
|
||||||
val db = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
val db = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
||||||
val mapping = db.getBlindedIdMapping(blindedId).firstOrNull() ?: BlindedIdMapping(blindedId, null, server, serverPublicKey)
|
val mapping = db.getBlindedIdMapping(blindedId).firstOrNull() ?: BlindedIdMapping(blindedId, null, server, serverPublicKey)
|
||||||
if (mapping.sessionId != null) {
|
if (mapping.accountId != null) {
|
||||||
return mapping
|
return mapping
|
||||||
}
|
}
|
||||||
getAllContacts().forEach { contact ->
|
getAllContacts().forEach { contact ->
|
||||||
val sessionId = SessionId(contact.sessionID)
|
val accountId = AccountId(contact.accountID)
|
||||||
if (sessionId.prefix == IdPrefix.STANDARD && SodiumUtilities.sessionId(sessionId.hexString, blindedId, serverPublicKey)) {
|
if (accountId.prefix == IdPrefix.STANDARD && SodiumUtilities.accountId(accountId.hexString, blindedId, serverPublicKey)) {
|
||||||
val contactMapping = mapping.copy(sessionId = sessionId.hexString)
|
val contactMapping = mapping.copy(accountId = accountId.hexString)
|
||||||
db.addBlindedIdMapping(contactMapping)
|
db.addBlindedIdMapping(contactMapping)
|
||||||
return contactMapping
|
return contactMapping
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
db.getBlindedIdMappingsExceptFor(server).forEach {
|
db.getBlindedIdMappingsExceptFor(server).forEach {
|
||||||
if (SodiumUtilities.sessionId(it.sessionId!!, blindedId, serverPublicKey)) {
|
if (SodiumUtilities.accountId(it.accountId!!, blindedId, serverPublicKey)) {
|
||||||
val otherMapping = mapping.copy(sessionId = it.sessionId)
|
val otherMapping = mapping.copy(accountId = it.accountId)
|
||||||
db.addBlindedIdMapping(otherMapping)
|
db.addBlindedIdMapping(otherMapping)
|
||||||
return otherMapping
|
return otherMapping
|
||||||
}
|
}
|
||||||
@@ -1746,7 +1742,7 @@ open class Storage(
|
|||||||
|
|
||||||
if (recipient.isClosedGroupRecipient) {
|
if (recipient.isClosedGroupRecipient) {
|
||||||
val userGroups = configFactory.userGroups ?: return
|
val userGroups = configFactory.userGroups ?: return
|
||||||
val groupPublicKey = GroupUtil.addressToGroupSessionId(recipient.address)
|
val groupPublicKey = GroupUtil.addressToGroupAccountId(recipient.address)
|
||||||
val groupInfo = userGroups.getLegacyGroupInfo(groupPublicKey)
|
val groupInfo = userGroups.getLegacyGroupInfo(groupPublicKey)
|
||||||
?.copy(disappearingTimer = expiryMode.expirySeconds) ?: return
|
?.copy(disappearingTimer = expiryMode.expirySeconds) ?: return
|
||||||
userGroups.set(groupInfo)
|
userGroups.set(groupInfo)
|
||||||
@@ -1807,3 +1803,11 @@ open class Storage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate a string to a specified number of bytes
|
||||||
|
*
|
||||||
|
* This could split multi-byte characters/emojis.
|
||||||
|
*/
|
||||||
|
private fun String.truncate(sizeInBytes: Int): String =
|
||||||
|
toByteArray().takeIf { it.size > sizeInBytes }?.take(sizeInBytes)?.toByteArray()?.let(::String) ?: this
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
|
||||||
|
fun Context.threadDatabase() = DatabaseComponent.get(this).threadDatabase()
|
@@ -1,101 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.dms
|
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.InputType
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.widget.addTextChangedListener
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.FragmentEnterPublicKeyBinding
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.thoughtcrime.securesms.util.QRCodeUtilities
|
|
||||||
import org.thoughtcrime.securesms.util.hideKeyboard
|
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
|
||||||
|
|
||||||
class EnterPublicKeyFragment : Fragment() {
|
|
||||||
private lateinit var binding: FragmentEnterPublicKeyBinding
|
|
||||||
|
|
||||||
var delegate: EnterPublicKeyDelegate? = null
|
|
||||||
|
|
||||||
private val hexEncodedPublicKey: String
|
|
||||||
get() {
|
|
||||||
return TextSecurePreferences.getLocalNumber(requireContext())!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
with(binding) {
|
|
||||||
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
|
||||||
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
|
||||||
publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
|
|
||||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
|
||||||
v.hideKeyboard()
|
|
||||||
handlePublicKeyEntered()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
publicKeyEditText.addTextChangedListener { text -> createPrivateChatButton.isVisible = !text.isNullOrBlank() }
|
|
||||||
publicKeyEditText.setOnFocusChangeListener { _, hasFocus -> optionalContentContainer.isVisible = !hasFocus }
|
|
||||||
mainContainer.setOnTouchListener { _, _ ->
|
|
||||||
binding.optionalContentContainer.isVisible = true
|
|
||||||
publicKeyEditText.clearFocus()
|
|
||||||
publicKeyEditText.hideKeyboard()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
val size = toPx(228, resources)
|
|
||||||
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, isInverted = false, hasTransparentBackground = false)
|
|
||||||
qrCodeImageView.setImageBitmap(qrCode)
|
|
||||||
publicKeyTextView.text = hexEncodedPublicKey
|
|
||||||
publicKeyTextView.setOnCreateContextMenuListener { contextMenu, view, _ ->
|
|
||||||
contextMenu.add(0, view.id, 0, R.string.copy).setOnMenuItemClickListener {
|
|
||||||
copyPublicKey()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
copyButton.setOnClickListener { copyPublicKey() }
|
|
||||||
shareButton.setOnClickListener { sharePublicKey() }
|
|
||||||
createPrivateChatButton.setOnClickListener { handlePublicKeyEntered(); publicKeyEditText.hideKeyboard() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyPublicKey() {
|
|
||||||
val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clip = ClipData.newPlainText("Session ID", hexEncodedPublicKey)
|
|
||||||
clipboard.setPrimaryClip(clip)
|
|
||||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sharePublicKey() {
|
|
||||||
val intent = Intent()
|
|
||||||
intent.action = Intent.ACTION_SEND
|
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
|
|
||||||
intent.type = "text/plain"
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePublicKeyEntered() {
|
|
||||||
val hexEncodedPublicKey = binding.publicKeyEditText.text?.trim()?.toString()
|
|
||||||
if (hexEncodedPublicKey.isNullOrEmpty()) return
|
|
||||||
delegate?.handlePublicKeyEntered(hexEncodedPublicKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface EnterPublicKeyDelegate {
|
|
||||||
fun handlePublicKeyEntered(publicKey: String)
|
|
||||||
}
|
|
@@ -1,107 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.dms
|
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorListenerAdapter
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.FragmentNewMessageBinding
|
|
||||||
import nl.komponents.kovenant.ui.failUi
|
|
||||||
import nl.komponents.kovenant.ui.successUi
|
|
||||||
import org.session.libsession.snode.SnodeAPI
|
|
||||||
import org.session.libsession.utilities.Address
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
|
||||||
import org.session.libsignal.utilities.PublicKeyValidation
|
|
||||||
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class NewMessageFragment : Fragment() {
|
|
||||||
|
|
||||||
private lateinit var binding: FragmentNewMessageBinding
|
|
||||||
|
|
||||||
lateinit var delegate: NewConversationDelegate
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
binding = FragmentNewMessageBinding.inflate(inflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
|
||||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
|
||||||
val onsOrPkDelegate = { onsNameOrPublicKey: String -> createPrivateChatIfPossible(onsNameOrPublicKey)}
|
|
||||||
val adapter = NewMessageFragmentAdapter(
|
|
||||||
parentFragment = this,
|
|
||||||
enterPublicKeyDelegate = onsOrPkDelegate,
|
|
||||||
scanPublicKeyDelegate = onsOrPkDelegate
|
|
||||||
)
|
|
||||||
binding.viewPager.adapter = adapter
|
|
||||||
val mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, pos ->
|
|
||||||
tab.text = when (pos) {
|
|
||||||
0 -> getString(R.string.activity_create_private_chat_enter_session_id_tab_title)
|
|
||||||
1 -> getString(R.string.activity_create_private_chat_scan_qr_code_tab_title)
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mediator.attach()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPrivateChatIfPossible(onsNameOrPublicKey: String) {
|
|
||||||
if (PublicKeyValidation.isValid(onsNameOrPublicKey)) {
|
|
||||||
createPrivateChat(onsNameOrPublicKey)
|
|
||||||
} else {
|
|
||||||
// This could be an ONS name
|
|
||||||
showLoader()
|
|
||||||
SnodeAPI.getSessionID(onsNameOrPublicKey).successUi { hexEncodedPublicKey ->
|
|
||||||
hideLoader()
|
|
||||||
createPrivateChat(hexEncodedPublicKey)
|
|
||||||
}.failUi { exception ->
|
|
||||||
hideLoader()
|
|
||||||
var message = getString(R.string.fragment_enter_public_key_error_message)
|
|
||||||
exception.localizedMessage?.let {
|
|
||||||
message = it
|
|
||||||
}
|
|
||||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPrivateChat(hexEncodedPublicKey: String) {
|
|
||||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(hexEncodedPublicKey), false)
|
|
||||||
val intent = Intent(requireContext(), ConversationActivityV2::class.java)
|
|
||||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
|
||||||
intent.setDataAndType(requireActivity().intent.data, requireActivity().intent.type)
|
|
||||||
val existingThread = DatabaseComponent.get(requireContext()).threadDatabase().getThreadIdIfExistsFor(recipient)
|
|
||||||
intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
|
|
||||||
requireContext().startActivity(intent)
|
|
||||||
delegate.onDialogClosePressed()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLoader() {
|
|
||||||
binding.loader.visibility = View.VISIBLE
|
|
||||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideLoader() {
|
|
||||||
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
|
||||||
super.onAnimationEnd(animation)
|
|
||||||
binding.loader.visibility = View.GONE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.dms
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
|
||||||
|
|
||||||
class NewMessageFragmentAdapter(
|
|
||||||
private val parentFragment: Fragment,
|
|
||||||
private val enterPublicKeyDelegate: EnterPublicKeyDelegate,
|
|
||||||
private val scanPublicKeyDelegate: ScanQRCodeWrapperFragmentDelegate
|
|
||||||
) : FragmentStateAdapter(parentFragment) {
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = 2
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
return when (position) {
|
|
||||||
0 -> EnterPublicKeyFragment().apply { delegate = enterPublicKeyDelegate }
|
|
||||||
1 -> ScanQRCodeWrapperFragment().apply { delegate = scanPublicKeyDelegate }
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -25,7 +25,7 @@ import org.session.libsession.utilities.Device
|
|||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.contacts.SelectContactsAdapter
|
import org.thoughtcrime.securesms.contacts.SelectContactsAdapter
|
||||||
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||||
@@ -43,7 +43,7 @@ class CreateGroupFragment : Fragment() {
|
|||||||
private lateinit var binding: FragmentCreateGroupBinding
|
private lateinit var binding: FragmentCreateGroupBinding
|
||||||
private val viewModel: CreateGroupViewModel by viewModels()
|
private val viewModel: CreateGroupViewModel by viewModels()
|
||||||
|
|
||||||
lateinit var delegate: NewConversationDelegate
|
lateinit var delegate: StartConversationDelegate
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
@@ -24,7 +24,7 @@ import org.session.libsession.utilities.GroupUtil
|
|||||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class JoinCommunityFragment : Fragment() {
|
|||||||
|
|
||||||
private lateinit var binding: FragmentJoinCommunityBinding
|
private lateinit var binding: FragmentJoinCommunityBinding
|
||||||
|
|
||||||
lateinit var delegate: NewConversationDelegate
|
lateinit var delegate: StartConversationDelegate
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
@@ -22,7 +21,6 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_ALL
|
|||||||
import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_NONE
|
import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_NONE
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
import org.thoughtcrime.securesms.util.getAccentColor
|
||||||
import org.thoughtcrime.securesms.util.getConversationUnread
|
import org.thoughtcrime.securesms.util.getConversationUnread
|
||||||
@@ -50,7 +48,7 @@ class ConversationView : LinearLayout {
|
|||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
|
fun bind(thread: ThreadRecord, isTyping: Boolean) {
|
||||||
this.thread = thread
|
this.thread = thread
|
||||||
if (thread.isPinned) {
|
if (thread.isPinned) {
|
||||||
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
@@ -141,11 +139,10 @@ class ConversationView : LinearLayout {
|
|||||||
else -> recipient.toShortString() // Internally uses the Contact API
|
else -> recipient.toShortString() // Internally uses the Contact API
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ThreadRecord.getSnippet(): CharSequence =
|
private fun ThreadRecord.getSnippet(): CharSequence = listOfNotNull(
|
||||||
concatSnippet(getSnippetPrefix(), getDisplayBody(context))
|
getSnippetPrefix(),
|
||||||
|
getDisplayBody(context)
|
||||||
private fun concatSnippet(prefix: CharSequence?, body: CharSequence): CharSequence =
|
).joinToString(": ")
|
||||||
prefix?.let { TextUtils.concat(it, ": ", body) } ?: body
|
|
||||||
|
|
||||||
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
|
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
|
||||||
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null
|
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null
|
||||||
|
@@ -0,0 +1,90 @@
|
|||||||
|
package org.thoughtcrime.securesms.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.ui.Divider
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.SessionColorsParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.base
|
||||||
|
import org.thoughtcrime.securesms.ui.color.Colors
|
||||||
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.h4
|
||||||
|
import org.thoughtcrime.securesms.ui.h8
|
||||||
|
import org.thoughtcrime.securesms.ui.small
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun EmptyView(newAccount: Boolean) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = LocalDimensions.current.homeEmptyViewMargin)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = if (newAccount) R.drawable.emoji_tada_large else R.drawable.ic_logo_large),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
if (newAccount) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.onboardingAccountCreated),
|
||||||
|
style = h4,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.welcome_to_session),
|
||||||
|
style = base,
|
||||||
|
color = LocalColors.current.primary,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(modifier = Modifier.padding(vertical = LocalDimensions.current.xsMargin))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.conversationsNone),
|
||||||
|
style = h8,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(bottom = LocalDimensions.current.xsItemSpacing))
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.onboardingHitThePlusButton),
|
||||||
|
style = small,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(2f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewEmptyView(
|
||||||
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
|
||||||
|
) {
|
||||||
|
PreviewTheme(colors) {
|
||||||
|
EmptyView(newAccount = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewEmptyViewNew(
|
||||||
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
|
||||||
|
) {
|
||||||
|
PreviewTheme(colors) {
|
||||||
|
EmptyView(newAccount = true)
|
||||||
|
}
|
||||||
|
}
|
@@ -4,13 +4,14 @@ import android.Manifest
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableString
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -18,11 +19,10 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
@@ -44,7 +44,7 @@ import org.session.libsignal.utilities.ThreadUtils
|
|||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.conversation.start.NewConversationFragment
|
import org.thoughtcrime.securesms.conversation.start.StartConversationFragment
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
|
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||||
@@ -59,36 +59,37 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
|||||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout
|
import org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout
|
||||||
|
import org.thoughtcrime.securesms.home.search.GlobalSearchResult
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
|
import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
|
||||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.notifications.PushRegistry
|
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||||
import org.thoughtcrime.securesms.onboarding.SeedActivity
|
|
||||||
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
|
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.preferences.SettingsActivity
|
import org.thoughtcrime.securesms.preferences.SettingsActivity
|
||||||
|
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
|
||||||
import org.thoughtcrime.securesms.showMuteDialog
|
import org.thoughtcrime.securesms.showMuteDialog
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.setThemedContent
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
import org.thoughtcrime.securesms.util.IP2Country
|
import org.thoughtcrime.securesms.util.IP2Country
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
|
import org.thoughtcrime.securesms.util.start
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class HomeActivity : PassphraseRequiredActionBarActivity(),
|
class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||||
ConversationClickListener,
|
ConversationClickListener,
|
||||||
SeedReminderViewDelegate,
|
|
||||||
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
|
||||||
const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
|
const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private lateinit var binding: ActivityHomeBinding
|
private lateinit var binding: ActivityHomeBinding
|
||||||
private lateinit var glide: GlideRequests
|
private lateinit var glide: GlideRequests
|
||||||
|
|
||||||
@@ -104,8 +105,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
|
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
|
||||||
private val homeViewModel by viewModels<HomeViewModel>()
|
private val homeViewModel by viewModels<HomeViewModel>()
|
||||||
|
|
||||||
private val publicKey: String
|
private val publicKey: String by lazy { textSecurePreferences.getLocalNumber()!! }
|
||||||
get() = textSecurePreferences.getLocalNumber()!!
|
|
||||||
|
|
||||||
private val homeAdapter: HomeAdapter by lazy {
|
private val homeAdapter: HomeAdapter by lazy {
|
||||||
HomeAdapter(context = this, configFactory = configFactory, listener = this, ::showMessageRequests, ::hideMessageRequests)
|
HomeAdapter(context = this, configFactory = configFactory, listener = this, ::showMessageRequests, ::hideMessageRequests)
|
||||||
@@ -113,47 +113,38 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
private val globalSearchAdapter = GlobalSearchAdapter { model ->
|
private val globalSearchAdapter = GlobalSearchAdapter { model ->
|
||||||
when (model) {
|
when (model) {
|
||||||
is GlobalSearchAdapter.Model.Message -> {
|
is GlobalSearchAdapter.Model.Message -> push<ConversationActivityV2> {
|
||||||
val threadId = model.messageResult.threadId
|
model.messageResult.run {
|
||||||
val timestamp = model.messageResult.sentTimestampMs
|
putExtra(ConversationActivityV2.THREAD_ID, threadId)
|
||||||
val author = model.messageResult.messageRecipient.address
|
putExtra(ConversationActivityV2.SCROLL_MESSAGE_ID, sentTimestampMs)
|
||||||
|
putExtra(ConversationActivityV2.SCROLL_MESSAGE_AUTHOR, messageRecipient.address)
|
||||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
|
||||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
|
|
||||||
intent.putExtra(ConversationActivityV2.SCROLL_MESSAGE_ID, timestamp)
|
|
||||||
intent.putExtra(ConversationActivityV2.SCROLL_MESSAGE_AUTHOR, author)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
is GlobalSearchAdapter.Model.SavedMessages -> {
|
|
||||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
|
||||||
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(model.currentUserPublicKey))
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
is GlobalSearchAdapter.Model.Contact -> {
|
|
||||||
val address = model.contact.sessionID
|
|
||||||
|
|
||||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
|
||||||
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(address))
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
is GlobalSearchAdapter.Model.GroupConversation -> {
|
|
||||||
val groupAddress = Address.fromSerialized(model.groupRecord.encodedId)
|
|
||||||
val threadId = threadDb.getThreadIdIfExistsFor(Recipient.from(this, groupAddress, false))
|
|
||||||
if (threadId >= 0) {
|
|
||||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
|
||||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
|
|
||||||
push(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
is GlobalSearchAdapter.Model.SavedMessages -> push<ConversationActivityV2> {
|
||||||
Log.d("Loki", "callback with model: $model")
|
putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(model.currentUserPublicKey))
|
||||||
}
|
}
|
||||||
|
is GlobalSearchAdapter.Model.Contact -> push<ConversationActivityV2> {
|
||||||
|
putExtra(ConversationActivityV2.ADDRESS, model.contact.accountID.let(Address::fromSerialized))
|
||||||
|
}
|
||||||
|
is GlobalSearchAdapter.Model.GroupConversation -> model.groupRecord.encodedId
|
||||||
|
.let { Recipient.from(this, Address.fromSerialized(it), false) }
|
||||||
|
.let(threadDb::getThreadIdIfExistsFor)
|
||||||
|
.takeIf { it >= 0 }
|
||||||
|
?.let {
|
||||||
|
push<ConversationActivityV2> { putExtra(ConversationActivityV2.THREAD_ID, it) }
|
||||||
|
}
|
||||||
|
else -> Log.d("Loki", "callback with model: $model")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val isNewAccount: Boolean get() = intent.getBooleanExtra(FROM_ONBOARDING, false)
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||||
super.onCreate(savedInstanceState, isReady)
|
super.onCreate(savedInstanceState, isReady)
|
||||||
|
|
||||||
|
if (!isTaskRoot) { finish(); return }
|
||||||
|
|
||||||
// Set content view
|
// Set content view
|
||||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
@@ -164,20 +155,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// Set up toolbar buttons
|
// Set up toolbar buttons
|
||||||
binding.profileButton.setOnClickListener { openSettings() }
|
binding.profileButton.setOnClickListener { openSettings() }
|
||||||
binding.searchViewContainer.setOnClickListener {
|
binding.searchViewContainer.setOnClickListener {
|
||||||
|
globalSearchViewModel.refresh()
|
||||||
binding.globalSearchInputLayout.requestFocus()
|
binding.globalSearchInputLayout.requestFocus()
|
||||||
}
|
}
|
||||||
binding.sessionToolbar.disableClipping()
|
binding.sessionToolbar.disableClipping()
|
||||||
// Set up seed reminder view
|
// Set up seed reminder view
|
||||||
lifecycleScope.launchWhenStarted {
|
lifecycleScope.launchWhenStarted {
|
||||||
val hasViewedSeed = textSecurePreferences.getHasViewedSeed()
|
binding.seedReminderView.setThemedContent {
|
||||||
if (!hasViewedSeed) {
|
if (!textSecurePreferences.getHasViewedSeed()) SeedReminder { start<RecoveryPasswordActivity>() }
|
||||||
binding.seedReminderView.isVisible = true
|
|
||||||
binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated
|
|
||||||
binding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
|
|
||||||
binding.seedReminderView.setProgress(80, false)
|
|
||||||
binding.seedReminderView.delegate = this@HomeActivity
|
|
||||||
} else {
|
|
||||||
binding.seedReminderView.isVisible = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Set up recycler view
|
// Set up recycler view
|
||||||
@@ -193,11 +178,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up empty state view
|
// Set up empty state view
|
||||||
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
binding.emptyStateContainer.setThemedContent {
|
||||||
|
EmptyView(isNewAccount)
|
||||||
|
}
|
||||||
|
|
||||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||||
|
|
||||||
// Set up new conversation button
|
// Set up new conversation button
|
||||||
binding.newConversationButton.setOnClickListener { showNewConversation() }
|
binding.newConversationButton.setOnClickListener { showStartConversation() }
|
||||||
// Observe blocked contacts changed events
|
// Observe blocked contacts changed events
|
||||||
|
|
||||||
// subscribe to outdated config updates, this should be removed after long enough time for device migration
|
// subscribe to outdated config updates, this should be removed after long enough time for device migration
|
||||||
@@ -252,51 +240,29 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// monitor the global search VM query
|
// monitor the global search VM query
|
||||||
launch {
|
launch {
|
||||||
binding.globalSearchInputLayout.query
|
binding.globalSearchInputLayout.query
|
||||||
.onEach(globalSearchViewModel::postQuery)
|
.collect(globalSearchViewModel::setQuery)
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
// Get group results and display them
|
// Get group results and display them
|
||||||
launch {
|
launch {
|
||||||
globalSearchViewModel.result.collect { result ->
|
globalSearchViewModel.result.map { result ->
|
||||||
val currentUserPublicKey = publicKey
|
result.query to when {
|
||||||
val contactAndGroupList = result.contacts.map { GlobalSearchAdapter.Model.Contact(it) } +
|
result.query.isEmpty() -> buildList {
|
||||||
result.threads.map { GlobalSearchAdapter.Model.GroupConversation(it) }
|
add(GlobalSearchAdapter.Model.Header(R.string.contacts))
|
||||||
|
add(GlobalSearchAdapter.Model.SavedMessages(publicKey))
|
||||||
val contactResults = contactAndGroupList.toMutableList()
|
addAll(result.groupedContacts)
|
||||||
|
}
|
||||||
if (contactResults.isEmpty()) {
|
else -> buildList {
|
||||||
contactResults.add(GlobalSearchAdapter.Model.SavedMessages(currentUserPublicKey))
|
result.contactAndGroupList.takeUnless { it.isEmpty() }?.let {
|
||||||
|
add(GlobalSearchAdapter.Model.Header(R.string.contacts))
|
||||||
|
addAll(it)
|
||||||
|
}
|
||||||
|
result.messageResults.takeUnless { it.isEmpty() }?.let {
|
||||||
|
add(GlobalSearchAdapter.Model.Header(R.string.global_search_messages))
|
||||||
|
addAll(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}.collectLatest(globalSearchAdapter::setNewData)
|
||||||
val userIndex = contactResults.indexOfFirst { it is GlobalSearchAdapter.Model.Contact && it.contact.sessionID == currentUserPublicKey }
|
|
||||||
if (userIndex >= 0) {
|
|
||||||
contactResults[userIndex] = GlobalSearchAdapter.Model.SavedMessages(currentUserPublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contactResults.isNotEmpty()) {
|
|
||||||
contactResults.add(0, GlobalSearchAdapter.Model.Header(R.string.global_search_contacts_groups))
|
|
||||||
}
|
|
||||||
|
|
||||||
val unreadThreadMap = result.messages
|
|
||||||
.groupBy { it.threadId }.keys
|
|
||||||
.map { it to mmsSmsDatabase.getUnreadCount(it) }
|
|
||||||
.toMap()
|
|
||||||
|
|
||||||
val messageResults: MutableList<GlobalSearchAdapter.Model> = result.messages
|
|
||||||
.map { messageResult ->
|
|
||||||
GlobalSearchAdapter.Model.Message(
|
|
||||||
messageResult,
|
|
||||||
unreadThreadMap[messageResult.threadId] ?: 0
|
|
||||||
)
|
|
||||||
}.toMutableList()
|
|
||||||
|
|
||||||
if (messageResults.isNotEmpty()) {
|
|
||||||
messageResults.add(0, GlobalSearchAdapter.Model.Header(R.string.global_search_messages))
|
|
||||||
}
|
|
||||||
|
|
||||||
val newData = contactResults + messageResults
|
|
||||||
globalSearchAdapter.setNewData(result.query, newData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventBus.getDefault().register(this@HomeActivity)
|
EventBus.getDefault().register(this@HomeActivity)
|
||||||
@@ -308,20 +274,62 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
.request(Manifest.permission.POST_NOTIFICATIONS)
|
.request(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
configFactory.user?.let { user ->
|
configFactory.user
|
||||||
if (!user.isBlockCommunityMessageRequestsSet()) {
|
?.takeUnless { it.isBlockCommunityMessageRequestsSet() }
|
||||||
user.setCommunityMessageRequests(false)
|
?.setCommunityMessageRequests(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val GlobalSearchResult.groupedContacts: List<GlobalSearchAdapter.Model> get() {
|
||||||
|
class NamedValue<T>(val name: String?, val value: T)
|
||||||
|
|
||||||
|
// Unknown is temporarily to be grouped together with numbers title.
|
||||||
|
// https://optf.atlassian.net/browse/SES-2287
|
||||||
|
val numbersTitle = "#"
|
||||||
|
val unknownTitle = numbersTitle
|
||||||
|
|
||||||
|
return contacts
|
||||||
|
// Remove ourself, we're shown above.
|
||||||
|
.filter { it.accountID != publicKey }
|
||||||
|
// Get the name that we will display and sort by, and uppercase it to
|
||||||
|
// help with sorting and we need the char uppercased later.
|
||||||
|
.map { (it.nickname?.takeIf(String::isNotEmpty) ?: it.name?.takeIf(String::isNotEmpty))
|
||||||
|
.let { name -> NamedValue(name?.uppercase(), it) } }
|
||||||
|
// Digits are all grouped under a #, the rest are grouped by their first character.uppercased()
|
||||||
|
// If there is no name, they go under Unknown
|
||||||
|
.groupBy { it.name?.run { first().takeUnless(Char::isDigit)?.toString() ?: numbersTitle } ?: unknownTitle }
|
||||||
|
// place the # at the end, after all the names starting with alphabetic chars
|
||||||
|
.toSortedMap(compareBy {
|
||||||
|
when (it) {
|
||||||
|
unknownTitle -> Char.MAX_VALUE
|
||||||
|
numbersTitle -> Char.MAX_VALUE - 1
|
||||||
|
else -> it.first()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
// Flatten the map of char to lists into an actual List that can be displayed.
|
||||||
|
.flatMap { (key, contacts) ->
|
||||||
|
listOf(
|
||||||
|
GlobalSearchAdapter.Model.SubHeader(key)
|
||||||
|
) + contacts.sortedBy { it.name ?: it.value.accountID }.map { it.value }.map { GlobalSearchAdapter.Model.Contact(it, it.nickname ?: it.name, it.accountID == publicKey) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val GlobalSearchResult.contactAndGroupList: List<GlobalSearchAdapter.Model> get() =
|
||||||
|
contacts.map { GlobalSearchAdapter.Model.Contact(it, it.nickname ?: it.name, it.accountID == publicKey) } +
|
||||||
|
threads.map(GlobalSearchAdapter.Model::GroupConversation)
|
||||||
|
|
||||||
|
private val GlobalSearchResult.messageResults: List<GlobalSearchAdapter.Model> get() {
|
||||||
|
val unreadThreadMap = messages
|
||||||
|
.map { it.threadId }.toSet()
|
||||||
|
.associateWith { mmsSmsDatabase.getUnreadCount(it) }
|
||||||
|
|
||||||
|
return messages.map {
|
||||||
|
GlobalSearchAdapter.Model.Message(it, unreadThreadMap[it.threadId] ?: 0, it.conversationRecipient.isLocalNumber)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInputFocusChanged(hasFocus: Boolean) {
|
override fun onInputFocusChanged(hasFocus: Boolean) {
|
||||||
if (hasFocus) {
|
setSearchShown(hasFocus || binding.globalSearchInputLayout.query.value.isNotEmpty())
|
||||||
setSearchShown(true)
|
|
||||||
} else {
|
|
||||||
setSearchShown(!binding.globalSearchInputLayout.query.value.isNullOrEmpty())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setSearchShown(isShown: Boolean) {
|
private fun setSearchShown(isShown: Boolean) {
|
||||||
@@ -330,7 +338,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
binding.recyclerView.isVisible = !isShown
|
binding.recyclerView.isVisible = !isShown
|
||||||
binding.emptyStateContainer.isVisible = (binding.recyclerView.adapter as HomeAdapter).itemCount == 0 && binding.recyclerView.isVisible
|
binding.emptyStateContainer.isVisible = (binding.recyclerView.adapter as HomeAdapter).itemCount == 0 && binding.recyclerView.isVisible
|
||||||
binding.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown
|
binding.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown
|
||||||
binding.globalSearchRecycler.isVisible = isShown
|
binding.globalSearchRecycler.isInvisible = !isShown
|
||||||
binding.newConversationButton.isVisible = !isShown
|
binding.newConversationButton.isVisible = !isShown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,16 +405,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// region Interaction
|
// region Interaction
|
||||||
@Deprecated("Deprecated in Java")
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (binding.globalSearchRecycler.isVisible) {
|
if (binding.globalSearchRecycler.isVisible) binding.globalSearchInputLayout.clearSearch(true)
|
||||||
binding.globalSearchInputLayout.clearSearch(true)
|
else super.onBackPressed()
|
||||||
return
|
|
||||||
}
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleSeedReminderViewContinueButtonTapped() {
|
|
||||||
val intent = Intent(this, SeedActivity::class.java)
|
|
||||||
show(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConversationClick(thread: ThreadRecord) {
|
override fun onConversationClick(thread: ThreadRecord) {
|
||||||
@@ -431,17 +431,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
bottomSheet.onCopyConversationId = onCopyConversationId@{
|
bottomSheet.onCopyConversationId = onCopyConversationId@{
|
||||||
bottomSheet.dismiss()
|
bottomSheet.dismiss()
|
||||||
if (!thread.recipient.isGroupRecipient && !thread.recipient.isLocalNumber) {
|
if (!thread.recipient.isGroupRecipient && !thread.recipient.isLocalNumber) {
|
||||||
val clip = ClipData.newPlainText("Session ID", thread.recipient.address.toString())
|
val clip = ClipData.newPlainText("Account ID", thread.recipient.address.toString())
|
||||||
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
else if (thread.recipient.isCommunityRecipient) {
|
else if (thread.recipient.isCommunityRecipient) {
|
||||||
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) ?: return@onCopyConversationId Unit
|
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient)
|
||||||
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit
|
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit
|
||||||
|
|
||||||
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
|
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
|
||||||
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -569,7 +569,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
val message = if (recipient.isGroupRecipient) {
|
val message = if (recipient.isGroupRecipient) {
|
||||||
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
|
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
|
||||||
if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) {
|
if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) {
|
||||||
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
getString(R.string.admin_group_leave_warning)
|
||||||
} else {
|
} else {
|
||||||
resources.getString(R.string.activity_home_leave_group_dialog_message)
|
resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||||
}
|
}
|
||||||
@@ -625,7 +625,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
private fun hideMessageRequests() {
|
private fun hideMessageRequests() {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
text("Hide message requests?")
|
text(getString(R.string.hide_message_requests))
|
||||||
button(R.string.yes) {
|
button(R.string.yes) {
|
||||||
textSecurePreferences.setHasHiddenMessageRequests()
|
textSecurePreferences.setHasHiddenMessageRequests()
|
||||||
homeViewModel.tryReload()
|
homeViewModel.tryReload()
|
||||||
@@ -634,9 +634,15 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showNewConversation() {
|
private fun showStartConversation() {
|
||||||
NewConversationFragment().show(supportFragmentManager, "NewConversationFragment")
|
StartConversationFragment().show(supportFragmentManager, "StartConversationFragment")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// endregion
|
|
||||||
|
fun Context.startHomeActivity(isNewAccount: Boolean) {
|
||||||
|
Intent(this, HomeActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
putExtra(HomeActivity.NEW_ACCOUNT, true)
|
||||||
|
putExtra(HomeActivity.FROM_ONBOARDING, true)
|
||||||
|
}.also(::startActivity)
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.home
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
@@ -12,8 +11,6 @@ import network.loki.messenger.R
|
|||||||
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
|
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class HomeAdapter(
|
class HomeAdapter(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -115,7 +112,7 @@ class HomeAdapter(
|
|||||||
val offset = if (hasHeaderView()) position - 1 else position
|
val offset = if (hasHeaderView()) position - 1 else position
|
||||||
val thread = data.threads[offset]
|
val thread = data.threads[offset]
|
||||||
val isTyping = data.typingThreadIDs.contains(thread.threadId)
|
val isTyping = data.typingThreadIDs.contains(thread.threadId)
|
||||||
holder.view.bind(thread, isTyping, glide)
|
holder.view.bind(thread, isTyping)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,84 @@
|
|||||||
|
package org.thoughtcrime.securesms.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.SessionColorsParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.SessionShieldIcon
|
||||||
|
import org.thoughtcrime.securesms.ui.color.Colors
|
||||||
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SlimPrimaryOutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.h8
|
||||||
|
import org.thoughtcrime.securesms.ui.small
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SeedReminder(startRecoveryPasswordActivity: () -> Unit) {
|
||||||
|
Column {
|
||||||
|
// Color Strip
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(LocalDimensions.current.indicatorHeight)
|
||||||
|
.background(LocalColors.current.primary)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.background(LocalColors.current.backgroundSecondary)
|
||||||
|
.padding(
|
||||||
|
horizontal = LocalDimensions.current.smallMargin,
|
||||||
|
vertical = LocalDimensions.current.xsMargin
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.save_your_recovery_password),
|
||||||
|
style = h8
|
||||||
|
)
|
||||||
|
Spacer(Modifier.requiredWidth(LocalDimensions.current.xxsItemSpacing))
|
||||||
|
SessionShieldIcon()
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.save_your_recovery_password_to_make_sure_you_don_t_lose_access_to_your_account),
|
||||||
|
style = small
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(LocalDimensions.current.xxsMargin))
|
||||||
|
SlimPrimaryOutlineButton(
|
||||||
|
text = stringResource(R.string.continue_2),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.contentDescription(R.string.AccessibilityId_reveal_recovery_phrase_button),
|
||||||
|
onClick = startRecoveryPasswordActivity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PreviewSeedReminder(
|
||||||
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
|
||||||
|
) {
|
||||||
|
PreviewTheme(colors) {
|
||||||
|
SeedReminder {}
|
||||||
|
}
|
||||||
|
}
|
@@ -98,7 +98,7 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
publicKeyTextView.setOnLongClickListener {
|
publicKeyTextView.setOnLongClickListener {
|
||||||
val clipboard =
|
val clipboard =
|
||||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("Session ID", publicKey)
|
val clip = ClipData.newPlainText("Account ID", publicKey)
|
||||||
clipboard.setPrimaryClip(clip)
|
clipboard.setPrimaryClip(clip)
|
||||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
|
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
|
||||||
.show()
|
.show()
|
||||||
@@ -137,7 +137,7 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
else { newNickName = previousContactNickname }
|
else { newNickName = previousContactNickname }
|
||||||
val publicKey = recipient.address.serialize()
|
val publicKey = recipient.address.serialize()
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val contact = storage.getContactWithSessionID(publicKey) ?: Contact(publicKey)
|
val contact = storage.getContactWithAccountID(publicKey) ?: Contact(publicKey)
|
||||||
contact.nickname = newNickName
|
contact.nickname = newNickName
|
||||||
storage.setContact(contact)
|
storage.setContact(contact)
|
||||||
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
|
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
|
||||||
|
@@ -9,23 +9,27 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
|
import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
|
||||||
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
|
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
|
||||||
|
import network.loki.messenger.databinding.ViewGlobalSearchSubheaderBinding
|
||||||
import org.session.libsession.utilities.GroupRecord
|
import org.session.libsession.utilities.GroupRecord
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
|
||||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import java.security.InvalidParameterException
|
import java.security.InvalidParameterException
|
||||||
import org.session.libsession.messaging.contacts.Contact as ContactModel
|
import org.session.libsession.messaging.contacts.Contact as ContactModel
|
||||||
|
|
||||||
class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
class GlobalSearchAdapter(private val modelCallback: (Model)->Unit): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val HEADER_VIEW_TYPE = 0
|
const val HEADER_VIEW_TYPE = 0
|
||||||
const val CONTENT_VIEW_TYPE = 1
|
const val SUB_HEADER_VIEW_TYPE = 1
|
||||||
|
const val CONTENT_VIEW_TYPE = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
private var data: List<Model> = listOf()
|
private var data: List<Model> = listOf()
|
||||||
private var query: String? = null
|
private var query: String? = null
|
||||||
|
|
||||||
|
fun setNewData(data: Pair<String, List<Model>>) = setNewData(data.first, data.second)
|
||||||
|
|
||||||
fun setNewData(query: String, newData: List<Model>) {
|
fun setNewData(query: String, newData: List<Model>) {
|
||||||
val diffResult = DiffUtil.calculateDiff(GlobalSearchDiff(this.query, query, data, newData))
|
val diffResult = DiffUtil.calculateDiff(GlobalSearchDiff(this.query, query, data, newData))
|
||||||
this.query = query
|
this.query = query
|
||||||
@@ -34,21 +38,26 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int =
|
override fun getItemViewType(position: Int): Int =
|
||||||
if (data[position] is Model.Header) HEADER_VIEW_TYPE else CONTENT_VIEW_TYPE
|
when(data[position]) {
|
||||||
|
is Model.Header -> HEADER_VIEW_TYPE
|
||||||
|
is Model.SubHeader -> SUB_HEADER_VIEW_TYPE
|
||||||
|
else -> CONTENT_VIEW_TYPE
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = data.size
|
override fun getItemCount(): Int = data.size
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
||||||
if (viewType == HEADER_VIEW_TYPE) {
|
when (viewType) {
|
||||||
HeaderView(
|
HEADER_VIEW_TYPE -> HeaderView(
|
||||||
LayoutInflater.from(parent.context)
|
LayoutInflater.from(parent.context).inflate(R.layout.view_global_search_header, parent, false)
|
||||||
.inflate(R.layout.view_global_search_header, parent, false)
|
)
|
||||||
|
SUB_HEADER_VIEW_TYPE -> SubHeaderView(
|
||||||
|
LayoutInflater.from(parent.context).inflate(R.layout.view_global_search_subheader, parent, false)
|
||||||
|
)
|
||||||
|
else -> ContentView(
|
||||||
|
LayoutInflater.from(parent.context).inflate(R.layout.view_global_search_result, parent, false),
|
||||||
|
modelCallback
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
ContentView(
|
|
||||||
LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.view_global_search_result, parent, false)
|
|
||||||
, modelCallback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
override fun onBindViewHolder(
|
||||||
@@ -61,10 +70,10 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
holder.bindPayload(newUpdateQuery, data[position])
|
holder.bindPayload(newUpdateQuery, data[position])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (holder is HeaderView) {
|
when (holder) {
|
||||||
holder.bind(data[position] as Model.Header)
|
is HeaderView -> holder.bind(data[position] as Model.Header)
|
||||||
} else if (holder is ContentView) {
|
is SubHeaderView -> holder.bind(data[position] as Model.SubHeader)
|
||||||
holder.bind(query.orEmpty(), data[position])
|
is ContentView -> holder.bind(query.orEmpty(), data[position])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +86,16 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
val binding = ViewGlobalSearchHeaderBinding.bind(view)
|
val binding = ViewGlobalSearchHeaderBinding.bind(view)
|
||||||
|
|
||||||
fun bind(header: Model.Header) {
|
fun bind(header: Model.Header) {
|
||||||
binding.searchHeader.setText(header.title)
|
binding.searchHeader.setText(header.title.string(binding.root.context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubHeaderView(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
val binding = ViewGlobalSearchSubheaderBinding.bind(view)
|
||||||
|
|
||||||
|
fun bind(header: Model.SubHeader) {
|
||||||
|
binding.searchHeader.text = header.title.string(binding.root.context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,25 +120,24 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
is Model.Contact -> bindModel(query, model)
|
is Model.Contact -> bindModel(query, model)
|
||||||
is Model.Message -> bindModel(query, model)
|
is Model.Message -> bindModel(query, model)
|
||||||
is Model.SavedMessages -> bindModel(model)
|
is Model.SavedMessages -> bindModel(model)
|
||||||
is Model.Header -> throw InvalidParameterException("Can't display Model.Header as ContentView")
|
else -> throw InvalidParameterException("Can't display as ContentView")
|
||||||
}
|
}
|
||||||
binding.root.setOnClickListener { modelCallback(model) }
|
binding.root.setOnClickListener { modelCallback(model) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MessageModel(
|
|
||||||
val threadRecipient: Recipient,
|
|
||||||
val messageRecipient: Recipient,
|
|
||||||
val messageSnippet: String
|
|
||||||
)
|
|
||||||
|
|
||||||
sealed class Model {
|
sealed class Model {
|
||||||
data class Header(@StringRes val title: Int) : Model()
|
data class Header(val title: GetString): Model() {
|
||||||
|
constructor(@StringRes title: Int): this(GetString(title))
|
||||||
|
constructor(title: String): this(GetString(title))
|
||||||
|
}
|
||||||
|
data class SubHeader(val title: GetString): Model() {
|
||||||
|
constructor(@StringRes title: Int): this(GetString(title))
|
||||||
|
constructor(title: String): this(GetString(title))
|
||||||
|
}
|
||||||
data class SavedMessages(val currentUserPublicKey: String): Model()
|
data class SavedMessages(val currentUserPublicKey: String): Model()
|
||||||
data class Contact(val contact: ContactModel) : Model()
|
data class Contact(val contact: ContactModel, val name: String?, val isSelf: Boolean): Model()
|
||||||
data class GroupConversation(val groupRecord: GroupRecord) : Model()
|
data class GroupConversation(val groupRecord: GroupRecord): Model()
|
||||||
data class Message(val messageResult: MessageResult, val unread: Int) : Model()
|
data class Message(val messageResult: MessageResult, val unread: Int, val isSelf: Boolean): Model()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -10,11 +10,13 @@ import network.loki.messenger.R
|
|||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsession.utilities.truncateIdForDisplay
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView
|
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.GroupConversation
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Header
|
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.Message
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages
|
||||||
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SubHeader
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -63,7 +65,7 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) {
|
|||||||
))
|
))
|
||||||
binding.searchResultSubtitle.text = textSpannable
|
binding.searchResultSubtitle.text = textSpannable
|
||||||
binding.searchResultSubtitle.isVisible = true
|
binding.searchResultSubtitle.isVisible = true
|
||||||
binding.searchResultTitle.text = model.messageResult.conversationRecipient.toShortString()
|
binding.searchResultTitle.text = model.messageResult.conversationRecipient.getSearchName()
|
||||||
}
|
}
|
||||||
is GroupConversation -> {
|
is GroupConversation -> {
|
||||||
binding.searchResultTitle.text = getHighlight(
|
binding.searchResultTitle.text = getHighlight(
|
||||||
@@ -72,12 +74,12 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val membersString = model.groupRecord.members.joinToString { address ->
|
val membersString = model.groupRecord.members.joinToString { address ->
|
||||||
val recipient = Recipient.from(binding.root.context, address, false)
|
Recipient.from(binding.root.context, address, false).getSearchName()
|
||||||
recipient.name ?: "${address.serialize().take(4)}...${address.serialize().takeLast(4)}"
|
|
||||||
}
|
}
|
||||||
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
||||||
}
|
}
|
||||||
is Header, // do nothing for header
|
is Header, // do nothing for header
|
||||||
|
is SubHeader, // do nothing for subheader
|
||||||
is SavedMessages -> Unit // do nothing for saved messages (displays note to self)
|
is SavedMessages -> Unit // do nothing for saved messages (displays note to self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +90,6 @@ private fun getHighlight(query: String?, toSearch: String): Spannable? {
|
|||||||
|
|
||||||
fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
||||||
binding.searchResultProfilePicture.isVisible = true
|
binding.searchResultProfilePicture.isVisible = true
|
||||||
binding.searchResultSavedMessages.isVisible = false
|
|
||||||
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
|
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
|
||||||
binding.searchResultTimestamp.isVisible = false
|
binding.searchResultTimestamp.isVisible = false
|
||||||
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false)
|
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false)
|
||||||
@@ -98,64 +99,65 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
|||||||
|
|
||||||
val groupRecipients = model.groupRecord.members.map { Recipient.from(binding.root.context, it, false) }
|
val groupRecipients = model.groupRecord.members.map { Recipient.from(binding.root.context, it, false) }
|
||||||
|
|
||||||
val membersString = groupRecipients.joinToString {
|
val membersString = groupRecipients.joinToString(transform = Recipient::getSearchName)
|
||||||
val address = it.address.serialize()
|
|
||||||
it.name ?: "${address.take(4)}...${address.takeLast(4)}"
|
|
||||||
}
|
|
||||||
if (model.groupRecord.isClosedGroup) {
|
if (model.groupRecord.isClosedGroup) {
|
||||||
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ContentView.bindModel(query: String?, model: ContactModel) {
|
fun ContentView.bindModel(query: String?, model: ContactModel) = binding.run {
|
||||||
binding.searchResultProfilePicture.isVisible = true
|
searchResultProfilePicture.isVisible = true
|
||||||
binding.searchResultSavedMessages.isVisible = false
|
searchResultSubtitle.isVisible = false
|
||||||
binding.searchResultSubtitle.isVisible = false
|
searchResultTimestamp.isVisible = false
|
||||||
binding.searchResultTimestamp.isVisible = false
|
searchResultSubtitle.text = null
|
||||||
binding.searchResultSubtitle.text = null
|
val recipient = Recipient.from(root.context, Address.fromSerialized(model.contact.accountID), false)
|
||||||
val recipient =
|
searchResultProfilePicture.update(recipient)
|
||||||
Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false)
|
val nameString = if (model.isSelf) root.context.getString(R.string.note_to_self)
|
||||||
binding.searchResultProfilePicture.update(recipient)
|
else model.contact.getSearchName()
|
||||||
val nameString = model.contact.getSearchName()
|
searchResultTitle.text = getHighlight(query, nameString)
|
||||||
binding.searchResultTitle.text = getHighlight(query, nameString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ContentView.bindModel(model: SavedMessages) {
|
fun ContentView.bindModel(model: SavedMessages) {
|
||||||
binding.searchResultSubtitle.isVisible = false
|
binding.searchResultSubtitle.isVisible = false
|
||||||
binding.searchResultTimestamp.isVisible = false
|
binding.searchResultTimestamp.isVisible = false
|
||||||
binding.searchResultTitle.setText(R.string.note_to_self)
|
binding.searchResultTitle.setText(R.string.note_to_self)
|
||||||
binding.searchResultProfilePicture.isVisible = false
|
binding.searchResultProfilePicture.update(Address.fromSerialized(model.currentUserPublicKey))
|
||||||
binding.searchResultSavedMessages.isVisible = true
|
binding.searchResultProfilePicture.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ContentView.bindModel(query: String?, model: Message) {
|
fun ContentView.bindModel(query: String?, model: Message) = binding.apply {
|
||||||
binding.searchResultProfilePicture.isVisible = true
|
searchResultProfilePicture.isVisible = true
|
||||||
binding.searchResultSavedMessages.isVisible = false
|
searchResultTimestamp.isVisible = true
|
||||||
binding.searchResultTimestamp.isVisible = true
|
|
||||||
// val hasUnreads = model.unread > 0
|
// val hasUnreads = model.unread > 0
|
||||||
// binding.unreadCountIndicator.isVisible = hasUnreads
|
// unreadCountIndicator.isVisible = hasUnreads
|
||||||
// if (hasUnreads) {
|
// if (hasUnreads) {
|
||||||
// binding.unreadCountTextView.text = model.unread.toString()
|
// unreadCountTextView.text = model.unread.toString()
|
||||||
// }
|
// }
|
||||||
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
|
searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
|
||||||
binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient)
|
searchResultProfilePicture.update(model.messageResult.conversationRecipient)
|
||||||
val textSpannable = SpannableStringBuilder()
|
val textSpannable = SpannableStringBuilder()
|
||||||
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
||||||
// group chat, bind
|
// group chat, bind
|
||||||
val text = "${model.messageResult.messageRecipient.getSearchName()}: "
|
val text = "${model.messageResult.messageRecipient.toShortString()}: "
|
||||||
textSpannable.append(text)
|
textSpannable.append(text)
|
||||||
}
|
}
|
||||||
textSpannable.append(getHighlight(
|
textSpannable.append(getHighlight(
|
||||||
query,
|
query,
|
||||||
model.messageResult.bodySnippet
|
model.messageResult.bodySnippet
|
||||||
))
|
))
|
||||||
binding.searchResultSubtitle.text = textSpannable
|
searchResultSubtitle.text = textSpannable
|
||||||
binding.searchResultTitle.text = model.messageResult.conversationRecipient.toShortString()
|
searchResultTitle.text = if (model.isSelf) root.context.getString(R.string.note_to_self)
|
||||||
binding.searchResultSubtitle.isVisible = true
|
else model.messageResult.conversationRecipient.getSearchName()
|
||||||
|
searchResultSubtitle.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Recipient.getSearchName(): String = name ?: address.serialize().let { address -> "${address.take(4)}...${address.takeLast(4)}" }
|
fun Recipient.getSearchName(): String =
|
||||||
|
name?.takeIf { it.isNotEmpty() && !it.looksLikeAccountId }
|
||||||
|
?: address.serialize().let(::truncateIdForDisplay)
|
||||||
|
|
||||||
fun Contact.getSearchName(): String =
|
fun Contact.getSearchName(): String =
|
||||||
if (nickname.isNullOrEmpty()) name ?: "${sessionID.take(4)}...${sessionID.takeLast(4)}"
|
nickname?.takeIf { it.isNotEmpty() && !it.looksLikeAccountId }
|
||||||
else "${name ?: "${sessionID.take(4)}...${sessionID.takeLast(4)}"} ($nickname)"
|
?: name?.takeIf { it.isNotEmpty() && !it.looksLikeAccountId }
|
||||||
|
?: truncateIdForDisplay(accountID)
|
||||||
|
|
||||||
|
private val String.looksLikeAccountId: Boolean get() = length > 60 && all { it.isDigit() || it.isLetter() }
|
||||||
|
@@ -16,42 +16,37 @@ import android.widget.TextView
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import network.loki.messenger.databinding.ViewGlobalSearchInputBinding
|
import network.loki.messenger.databinding.ViewGlobalSearchInputBinding
|
||||||
|
import org.thoughtcrime.securesms.util.SimpleTextWatcher
|
||||||
|
import org.thoughtcrime.securesms.util.addTextChangedListener
|
||||||
|
|
||||||
class GlobalSearchInputLayout @JvmOverloads constructor(
|
class GlobalSearchInputLayout @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null
|
context: Context, attrs: AttributeSet? = null
|
||||||
) : LinearLayout(context, attrs),
|
) : LinearLayout(context, attrs),
|
||||||
View.OnFocusChangeListener,
|
View.OnFocusChangeListener,
|
||||||
View.OnClickListener,
|
TextView.OnEditorActionListener {
|
||||||
TextWatcher, TextView.OnEditorActionListener {
|
|
||||||
|
|
||||||
var binding: ViewGlobalSearchInputBinding = ViewGlobalSearchInputBinding.inflate(LayoutInflater.from(context), this, true)
|
var binding: ViewGlobalSearchInputBinding = ViewGlobalSearchInputBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
|
||||||
var listener: GlobalSearchInputLayoutListener? = null
|
var listener: GlobalSearchInputLayoutListener? = null
|
||||||
|
|
||||||
private val _query = MutableStateFlow<CharSequence?>(null)
|
private val _query = MutableStateFlow<CharSequence>("")
|
||||||
val query: StateFlow<CharSequence?> = _query
|
val query: StateFlow<CharSequence> = _query
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
binding.searchInput.onFocusChangeListener = this
|
binding.searchInput.onFocusChangeListener = this
|
||||||
binding.searchInput.addTextChangedListener(this)
|
binding.searchInput.addTextChangedListener(::setQuery)
|
||||||
binding.searchInput.setOnEditorActionListener(this)
|
binding.searchInput.setOnEditorActionListener(this)
|
||||||
binding.searchInput.setFilters( arrayOf<InputFilter>(LengthFilter(100)) ) // 100 char search limit
|
binding.searchInput.filters = arrayOf<InputFilter>(LengthFilter(100)) // 100 char search limit
|
||||||
binding.searchCancel.setOnClickListener(this)
|
binding.searchCancel.setOnClickListener { clearSearch(true) }
|
||||||
binding.searchClear.setOnClickListener(this)
|
binding.searchClear.setOnClickListener { clearSearch(false) }
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
|
||||||
super.onDetachedFromWindow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFocusChange(v: View?, hasFocus: Boolean) {
|
override fun onFocusChange(v: View?, hasFocus: Boolean) {
|
||||||
if (v === binding.searchInput) {
|
if (v === binding.searchInput) {
|
||||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).apply {
|
||||||
if (!hasFocus) {
|
if (hasFocus) showSoftInput(v, 0)
|
||||||
imm.hideSoftInputFromWindow(windowToken, 0)
|
else hideSoftInputFromWindow(windowToken, 0)
|
||||||
} else {
|
|
||||||
imm.showSoftInput(v, 0)
|
|
||||||
}
|
}
|
||||||
listener?.onInputFocusChanged(hasFocus)
|
listener?.onInputFocusChanged(hasFocus)
|
||||||
}
|
}
|
||||||
@@ -65,27 +60,16 @@ class GlobalSearchInputLayout @JvmOverloads constructor(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View?) {
|
|
||||||
if (v === binding.searchCancel) {
|
|
||||||
clearSearch(true)
|
|
||||||
} else if (v === binding.searchClear) {
|
|
||||||
clearSearch(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearSearch(clearFocus: Boolean) {
|
fun clearSearch(clearFocus: Boolean) {
|
||||||
binding.searchInput.text = null
|
binding.searchInput.text = null
|
||||||
|
setQuery("")
|
||||||
if (clearFocus) {
|
if (clearFocus) {
|
||||||
binding.searchInput.clearFocus()
|
binding.searchInput.clearFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
private fun setQuery(query: String) {
|
||||||
|
_query.value = query
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(s: Editable?) {
|
|
||||||
_query.value = s?.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GlobalSearchInputLayoutListener {
|
interface GlobalSearchInputLayoutListener {
|
||||||
|
@@ -2,33 +2,25 @@ package org.thoughtcrime.securesms.home.search
|
|||||||
|
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.GroupRecord
|
import org.session.libsession.utilities.GroupRecord
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
|
||||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||||
import org.thoughtcrime.securesms.search.model.SearchResult
|
import org.thoughtcrime.securesms.search.model.SearchResult
|
||||||
|
|
||||||
data class GlobalSearchResult(
|
data class GlobalSearchResult(
|
||||||
val query: String,
|
val query: String,
|
||||||
val contacts: List<Contact>,
|
val contacts: List<Contact> = emptyList(),
|
||||||
val threads: List<GroupRecord>,
|
val threads: List<GroupRecord> = emptyList(),
|
||||||
val messages: List<MessageResult>
|
val messages: List<MessageResult> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
get() = contacts.isEmpty() && threads.isEmpty() && messages.isEmpty()
|
get() = contacts.isEmpty() && threads.isEmpty() && messages.isEmpty()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val EMPTY = GlobalSearchResult("")
|
||||||
val EMPTY = GlobalSearchResult("", emptyList(), emptyList(), emptyList())
|
|
||||||
const val SEARCH_LIMIT = 5
|
|
||||||
|
|
||||||
fun from(searchResult: SearchResult): GlobalSearchResult {
|
|
||||||
val query = searchResult.query
|
|
||||||
val contactList = searchResult.contacts.toList()
|
|
||||||
val threads = searchResult.conversations.toList()
|
|
||||||
val messages = searchResult.messages.toList()
|
|
||||||
searchResult.close()
|
|
||||||
return GlobalSearchResult(query, contactList, threads, messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun SearchResult.toGlobalSearchResult(): GlobalSearchResult = try {
|
||||||
|
GlobalSearchResult(query, contacts.toList(), conversations.toList(), messages.toList())
|
||||||
|
} finally {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
@@ -3,15 +3,22 @@ package org.thoughtcrime.securesms.home.search
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.merge
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.session.libsignal.utilities.SettableFuture
|
import org.session.libsignal.utilities.SettableFuture
|
||||||
import org.thoughtcrime.securesms.search.SearchRepository
|
import org.thoughtcrime.securesms.search.SearchRepository
|
||||||
@@ -19,49 +26,51 @@ import org.thoughtcrime.securesms.search.model.SearchResult
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class GlobalSearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() {
|
class GlobalSearchViewModel @Inject constructor(
|
||||||
|
private val searchRepository: SearchRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
private val scope = viewModelScope + SupervisorJob()
|
||||||
|
private val refreshes = MutableSharedFlow<Unit>()
|
||||||
|
private val _queryText = MutableStateFlow<CharSequence>("")
|
||||||
|
|
||||||
private val executor = viewModelScope + SupervisorJob()
|
val result = _queryText
|
||||||
|
.reEmit(refreshes)
|
||||||
|
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
.mapLatest { query ->
|
||||||
|
if (query.trim().isEmpty()) {
|
||||||
|
// searching for 05 as contactDb#getAllContacts was not returning contacts
|
||||||
|
// without a nickname/name who haven't approved us.
|
||||||
|
GlobalSearchResult(query.toString(), searchRepository.queryContacts("05").first.toList())
|
||||||
|
} else {
|
||||||
|
// User input delay in case we get a new query within a few hundred ms this
|
||||||
|
// coroutine will be cancelled and the expensive query will not be run.
|
||||||
|
delay(300)
|
||||||
|
val settableFuture = SettableFuture<SearchResult>()
|
||||||
|
searchRepository.query(query.toString(), settableFuture::set)
|
||||||
|
try {
|
||||||
|
// search repository doesn't play nicely with suspend functions (yet)
|
||||||
|
settableFuture.get(10_000, TimeUnit.MILLISECONDS).toGlobalSearchResult()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
GlobalSearchResult(query.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val _result: MutableStateFlow<GlobalSearchResult> = MutableStateFlow(GlobalSearchResult.EMPTY)
|
fun setQuery(charSequence: CharSequence) {
|
||||||
|
|
||||||
val result: StateFlow<GlobalSearchResult> = _result
|
|
||||||
|
|
||||||
private val _queryText: MutableStateFlow<CharSequence> = MutableStateFlow("")
|
|
||||||
|
|
||||||
fun postQuery(charSequence: CharSequence?) {
|
|
||||||
charSequence ?: return
|
|
||||||
_queryText.value = charSequence
|
_queryText.value = charSequence
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
fun refresh() {
|
||||||
//
|
viewModelScope.launch {
|
||||||
_queryText
|
refreshes.emit(Unit)
|
||||||
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
}
|
||||||
.mapLatest { query ->
|
|
||||||
// Early exit on empty search query
|
|
||||||
if (query.trim().isEmpty()) {
|
|
||||||
SearchResult.EMPTY
|
|
||||||
} else {
|
|
||||||
// User input delay in case we get a new query within a few hundred ms this
|
|
||||||
// coroutine will be cancelled and the expensive query will not be run.
|
|
||||||
delay(300)
|
|
||||||
|
|
||||||
val settableFuture = SettableFuture<SearchResult>()
|
|
||||||
searchRepository.query(query.toString(), settableFuture::set)
|
|
||||||
try {
|
|
||||||
// search repository doesn't play nicely with suspend functions (yet)
|
|
||||||
settableFuture.get(10_000, TimeUnit.MILLISECONDS)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SearchResult.EMPTY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onEach { result ->
|
|
||||||
// update the latest _result value
|
|
||||||
_result.value = GlobalSearchResult.from(result)
|
|
||||||
}
|
|
||||||
.launchIn(executor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-emit whenever refreshes emits.
|
||||||
|
* */
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun <T> Flow<T>.reEmit(refreshes: Flow<Unit>) = flatMapLatest { query -> merge(flowOf(query), refreshes.map { query }) }
|
||||||
|
@@ -76,7 +76,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
if (TextSecurePreferences.getLocalNumber(context) == null || !TextSecurePreferences.hasSeenWelcomeScreen(context)) {
|
if (TextSecurePreferences.getLocalNumber(context) == null) {
|
||||||
Log.v(TAG, "User not registered yet.")
|
Log.v(TAG, "User not registered yet.")
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
@@ -43,7 +43,7 @@ import com.goterl.lazysodium.utils.KeyPair;
|
|||||||
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
import org.session.libsession.messaging.open_groups.OpenGroup;
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||||
import org.session.libsession.messaging.utilities.SessionId;
|
import org.session.libsession.messaging.utilities.AccountId;
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities;
|
import org.session.libsession.messaging.utilities.SodiumUtilities;
|
||||||
import org.session.libsession.snode.SnodeAPI;
|
import org.session.libsession.snode.SnodeAPI;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
@@ -269,7 +269,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
try {
|
try {
|
||||||
telcoCursor = DatabaseComponent.get(context).mmsSmsDatabase().getUnread(); // TODO: add a notification specific lighter query here
|
telcoCursor = DatabaseComponent.get(context).mmsSmsDatabase().getUnread(); // TODO: add a notification specific lighter query here
|
||||||
|
|
||||||
if ((telcoCursor == null || telcoCursor.isAfterLast()) || !TextSecurePreferences.hasSeenWelcomeScreen(context))
|
if ((telcoCursor == null || telcoCursor.isAfterLast()) || TextSecurePreferences.getLocalNumber(context) == null)
|
||||||
{
|
{
|
||||||
updateBadge(context, 0);
|
updateBadge(context, 0);
|
||||||
cancelActiveNotifications(context);
|
cancelActiveNotifications(context);
|
||||||
@@ -594,7 +594,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
if (openGroup != null && edKeyPair != null) {
|
if (openGroup != null && edKeyPair != null) {
|
||||||
KeyPair blindedKeyPair = SodiumUtilities.blindedKeyPair(openGroup.getPublicKey(), edKeyPair);
|
KeyPair blindedKeyPair = SodiumUtilities.blindedKeyPair(openGroup.getPublicKey(), edKeyPair);
|
||||||
if (blindedKeyPair != null) {
|
if (blindedKeyPair != null) {
|
||||||
return new SessionId(IdPrefix.BLINDED, blindedKeyPair.getPublicKey().getAsBytes()).getHexString();
|
return new AccountId(IdPrefix.BLINDED, blindedKeyPair.getPublicKey().getAsBytes()).getHexString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@@ -118,11 +118,11 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
|
|||||||
*/
|
*/
|
||||||
private String getGroupDisplayName(Recipient recipient, boolean openGroupRecipient) {
|
private String getGroupDisplayName(Recipient recipient, boolean openGroupRecipient) {
|
||||||
SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase();
|
SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase();
|
||||||
String sessionID = recipient.getAddress().serialize();
|
String accountID = recipient.getAddress().serialize();
|
||||||
Contact contact = contactDB.getContactWithSessionID(sessionID);
|
Contact contact = contactDB.getContactWithAccountID(accountID);
|
||||||
if (contact == null) { return sessionID; }
|
if (contact == null) { return accountID; }
|
||||||
String displayName = contact.displayName(openGroupRecipient ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR);
|
String displayName = contact.displayName(openGroupRecipient ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR);
|
||||||
if (displayName == null) { return sessionID; }
|
if (displayName == null) { return accountID; }
|
||||||
return displayName;
|
return displayName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -339,11 +339,11 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||||||
*/
|
*/
|
||||||
private String getGroupDisplayName(Recipient recipient, boolean openGroupRecipient) {
|
private String getGroupDisplayName(Recipient recipient, boolean openGroupRecipient) {
|
||||||
SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase();
|
SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase();
|
||||||
String sessionID = recipient.getAddress().serialize();
|
String accountID = recipient.getAddress().serialize();
|
||||||
Contact contact = contactDB.getContactWithSessionID(sessionID);
|
Contact contact = contactDB.getContactWithAccountID(accountID);
|
||||||
if (contact == null) { return sessionID; }
|
if (contact == null) { return accountID; }
|
||||||
String displayName = contact.displayName(openGroupRecipient ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR);
|
String displayName = contact.displayName(openGroupRecipient ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR);
|
||||||
if (displayName == null) { return sessionID; }
|
if (displayName == null) { return accountID; }
|
||||||
return displayName;
|
return displayName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,57 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.onboarding
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import android.widget.TextView.OnEditorActionListener
|
|
||||||
import android.widget.Toast
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.ActivityDisplayNameBinding
|
|
||||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
|
||||||
import org.thoughtcrime.securesms.util.push
|
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
|
|
||||||
class DisplayNameActivity : BaseActionBarActivity() {
|
|
||||||
private lateinit var binding: ActivityDisplayNameBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setUpActionBarSessionLogo()
|
|
||||||
binding = ActivityDisplayNameBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
with(binding) {
|
|
||||||
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
|
|
||||||
displayNameEditText.setOnEditorActionListener(
|
|
||||||
OnEditorActionListener { _, actionID, event ->
|
|
||||||
if (actionID == EditorInfo.IME_ACTION_SEARCH ||
|
|
||||||
actionID == EditorInfo.IME_ACTION_DONE ||
|
|
||||||
(event.action == KeyEvent.ACTION_DOWN &&
|
|
||||||
event.keyCode == KeyEvent.KEYCODE_ENTER)) {
|
|
||||||
register()
|
|
||||||
return@OnEditorActionListener true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
registerButton.setOnClickListener { register() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun register() {
|
|
||||||
val displayName = binding.displayNameEditText.text.toString().trim()
|
|
||||||
if (displayName.isEmpty()) {
|
|
||||||
return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
if (displayName.toByteArray().size > ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH) {
|
|
||||||
return Toast.makeText(this, R.string.activity_display_name_display_name_too_long_error, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
|
||||||
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
|
||||||
TextSecurePreferences.setProfileName(this, displayName)
|
|
||||||
val intent = Intent(this, PNModeActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,79 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.onboarding
|
|
||||||
|
|
||||||
import android.animation.FloatEvaluator
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Handler
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ScrollView
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.ViewFakeChatBinding
|
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
|
||||||
|
|
||||||
class FakeChatView : ScrollView {
|
|
||||||
private lateinit var binding: ViewFakeChatBinding
|
|
||||||
// region Settings
|
|
||||||
private val spacing = context.resources.getDimension(R.dimen.medium_spacing)
|
|
||||||
private val startDelay: Long = 1000
|
|
||||||
private val delayBetweenMessages: Long = 1500
|
|
||||||
private val animationDuration: Long = 400
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Lifecycle
|
|
||||||
constructor(context: Context) : super(context) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUpViewHierarchy() {
|
|
||||||
binding = ViewFakeChatBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
binding.root.disableClipping()
|
|
||||||
isVerticalScrollBarEnabled = false
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Animation
|
|
||||||
fun startAnimating() {
|
|
||||||
listOf( binding.bubble1, binding.bubble2, binding.bubble3, binding.bubble4, binding.bubble5 ).forEach { it.alpha = 0.0f }
|
|
||||||
fun show(bubble: View) {
|
|
||||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
|
||||||
animation.duration = animationDuration
|
|
||||||
animation.addUpdateListener { animator ->
|
|
||||||
bubble.alpha = animator.animatedValue as Float
|
|
||||||
}
|
|
||||||
animation.start()
|
|
||||||
}
|
|
||||||
Handler().postDelayed({
|
|
||||||
show(binding.bubble1)
|
|
||||||
Handler().postDelayed({
|
|
||||||
show(binding.bubble2)
|
|
||||||
Handler().postDelayed({
|
|
||||||
show(binding.bubble3)
|
|
||||||
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt())
|
|
||||||
Handler().postDelayed({
|
|
||||||
show(binding.bubble4)
|
|
||||||
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt())
|
|
||||||
Handler().postDelayed({
|
|
||||||
show(binding.bubble5)
|
|
||||||
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt() + (binding.bubble3.height + spacing).toInt())
|
|
||||||
}, delayBetweenMessages)
|
|
||||||
}, delayBetweenMessages)
|
|
||||||
}, delayBetweenMessages)
|
|
||||||
}, delayBetweenMessages)
|
|
||||||
}, startDelay)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@@ -1,230 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.onboarding
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.InputType
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
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
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
|
|
||||||
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
|
|
||||||
import org.session.libsession.snode.SnodeModule
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
|
||||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
import org.session.libsignal.utilities.KeyHelper
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
|
||||||
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
|
|
||||||
private val adapter = LinkDeviceActivityAdapter(this)
|
|
||||||
private var restoreJob: Job? = null
|
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (restoreJob?.isActive == true) return // Don't allow going back with a pending job
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Lifecycle
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setUpActionBarSessionLogo()
|
|
||||||
TextSecurePreferences.apply {
|
|
||||||
setHasViewedSeed(this@LinkDeviceActivity, true)
|
|
||||||
setConfigurationMessageSynced(this@LinkDeviceActivity, false)
|
|
||||||
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
|
||||||
setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
|
|
||||||
}
|
|
||||||
binding = ActivityLinkDeviceBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
binding.viewPager.adapter = adapter
|
|
||||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Interaction
|
|
||||||
override fun handleQRCodeScanned(mnemonic: String) {
|
|
||||||
try {
|
|
||||||
val seed = Hex.fromStringCondensed(mnemonic)
|
|
||||||
continueWithSeed(seed)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Loki","Error getting seed from QR code", e)
|
|
||||||
Toast.makeText(this, "An error occurred.", Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun continueWithMnemonic(mnemonic: String) {
|
|
||||||
val loadFileContents: (String) -> String = { fileName ->
|
|
||||||
MnemonicUtilities.loadFileContents(this, fileName)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val hexEncodedSeed = MnemonicCodec(loadFileContents).decode(mnemonic)
|
|
||||||
val seed = Hex.fromStringCondensed(hexEncodedSeed)
|
|
||||||
continueWithSeed(seed)
|
|
||||||
} catch (error: Exception) {
|
|
||||||
val message = if (error is MnemonicCodec.DecodingError) {
|
|
||||||
error.description
|
|
||||||
} else {
|
|
||||||
"An error occurred."
|
|
||||||
}
|
|
||||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun continueWithSeed(seed: ByteArray) {
|
|
||||||
|
|
||||||
// only have one sync job running at a time (prevent QR from trying to spawn a new job)
|
|
||||||
if (restoreJob?.isActive == true) return
|
|
||||||
|
|
||||||
restoreJob = lifecycleScope.launch {
|
|
||||||
// This is here to resolve a case where the app restarts before a user completes onboarding
|
|
||||||
// which can result in an invalid database state
|
|
||||||
database.clearAllLastMessageHashes()
|
|
||||||
database.clearReceivedMessageHashValues()
|
|
||||||
|
|
||||||
// RestoreActivity handles seed this way
|
|
||||||
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)
|
|
||||||
TextSecurePreferences.setLocalNumber(this@LinkDeviceActivity, userHexEncodedPublicKey)
|
|
||||||
TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
|
||||||
TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true)
|
|
||||||
|
|
||||||
binding.loader.isVisible = true
|
|
||||||
val snackBar = Snackbar.make(binding.containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE)
|
|
||||||
.setAction(R.string.registration_activity__skip) { register(true) }
|
|
||||||
|
|
||||||
val skipJob = launch {
|
|
||||||
delay(15_000L)
|
|
||||||
snackBar.show()
|
|
||||||
}
|
|
||||||
// start polling and wait for updated message
|
|
||||||
ApplicationContext.getInstance(this@LinkDeviceActivity).apply {
|
|
||||||
startPollingIfNeeded()
|
|
||||||
}
|
|
||||||
TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect {
|
|
||||||
// handle we've synced
|
|
||||||
snackBar.dismiss()
|
|
||||||
skipJob.cancel()
|
|
||||||
register(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.loader.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun register(skipped: Boolean) {
|
|
||||||
restoreJob?.cancel()
|
|
||||||
binding.loader.isVisible = false
|
|
||||||
TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis())
|
|
||||||
val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Adapter
|
|
||||||
private class LinkDeviceActivityAdapter(private val activity: LinkDeviceActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
|
||||||
val recoveryPhraseFragment = RecoveryPhraseFragment()
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(index: Int): Fragment {
|
|
||||||
return when (index) {
|
|
||||||
0 -> recoveryPhraseFragment
|
|
||||||
1 -> {
|
|
||||||
val result = ScanQRCodeWrapperFragment()
|
|
||||||
result.delegate = activity
|
|
||||||
result.message = activity.getString(R.string.activity_link_device_qr_message)
|
|
||||||
result
|
|
||||||
}
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPageTitle(index: Int): CharSequence {
|
|
||||||
return when (index) {
|
|
||||||
0 -> activity.getString(R.string.activity_link_device_recovery_phrase)
|
|
||||||
1 -> activity.getString(R.string.activity_link_device_scan_qr_code)
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Recovery Phrase Fragment
|
|
||||||
class RecoveryPhraseFragment : Fragment() {
|
|
||||||
private lateinit var binding: FragmentRecoveryPhraseBinding
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
binding = FragmentRecoveryPhraseBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
with(binding) {
|
|
||||||
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
|
||||||
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
|
||||||
mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
|
|
||||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
|
||||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
||||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
|
||||||
handleContinueButtonTapped()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continueButton.setOnClickListener { handleContinueButtonTapped() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleContinueButtonTapped() {
|
|
||||||
val mnemonic = binding.mnemonicEditText.text?.trim().toString()
|
|
||||||
(requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
@@ -0,0 +1,33 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.ui.AlertDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.DialogButtonModel
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OnboardingBackPressAlertDialog(
|
||||||
|
dismissDialog: () -> Unit,
|
||||||
|
@StringRes textId: Int = R.string.you_cannot_go_back_further_in_order_to_stop_loading_your_account_session_needs_to_quit,
|
||||||
|
quit: () -> Unit
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = dismissDialog,
|
||||||
|
title = stringResource(R.string.warning),
|
||||||
|
text = stringResource(textId),
|
||||||
|
buttons = listOf(
|
||||||
|
DialogButtonModel(
|
||||||
|
GetString(stringResource(R.string.quit)),
|
||||||
|
color = LocalColors.current.danger,
|
||||||
|
onClick = quit
|
||||||
|
),
|
||||||
|
DialogButtonModel(
|
||||||
|
GetString(stringResource(R.string.cancel))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
@@ -1,179 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.onboarding
|
|
||||||
|
|
||||||
import android.animation.ArgbEvaluator
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.drawable.TransitionDrawable
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.ActivityPnModeBinding
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsession.utilities.ThemeUtil
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity
|
|
||||||
import org.thoughtcrime.securesms.notifications.PushManager
|
|
||||||
import org.thoughtcrime.securesms.notifications.PushRegistry
|
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
|
||||||
import org.thoughtcrime.securesms.util.PNModeView
|
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
|
||||||
import org.thoughtcrime.securesms.util.getColorWithID
|
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
|
||||||
import org.thoughtcrime.securesms.util.show
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class PNModeActivity : BaseActionBarActivity() {
|
|
||||||
|
|
||||||
@Inject lateinit var pushRegistry: PushRegistry
|
|
||||||
|
|
||||||
private lateinit var binding: ActivityPnModeBinding
|
|
||||||
private var selectedOptionView: PNModeView? = null
|
|
||||||
|
|
||||||
// region Lifecycle
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setUpActionBarSessionLogo(true)
|
|
||||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
|
||||||
binding = ActivityPnModeBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
with(binding) {
|
|
||||||
contentView.disableClipping()
|
|
||||||
fcmOptionView.setOnClickListener { toggleFCM() }
|
|
||||||
fcmOptionView.mainColor = ThemeUtil.getThemedColor(root.context, R.attr.colorPrimary)
|
|
||||||
fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
|
||||||
backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() }
|
|
||||||
backgroundPollingOptionView.mainColor = ThemeUtil.getThemedColor(root.context, R.attr.colorPrimary)
|
|
||||||
backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
|
||||||
registerButton.setOnClickListener { register() }
|
|
||||||
}
|
|
||||||
toggleFCM()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.menu_pn_mode, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Animation
|
|
||||||
private fun performTransition(@DrawableRes transitionID: Int, subject: View) {
|
|
||||||
val drawable = resources.getDrawable(transitionID, theme) as TransitionDrawable
|
|
||||||
subject.background = drawable
|
|
||||||
drawable.startTransition(250)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Interaction
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when(item.itemId) {
|
|
||||||
R.id.learnMoreButton -> learnMore()
|
|
||||||
else -> { /* Do nothing */ }
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun learnMore() {
|
|
||||||
try {
|
|
||||||
val url = "https://getsession.org/faq/#privacy"
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleFCM() = with(binding) {
|
|
||||||
val accentColor = getAccentColor()
|
|
||||||
when (selectedOptionView) {
|
|
||||||
null -> {
|
|
||||||
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
|
|
||||||
GlowViewUtilities.animateShadowColorChange(fcmOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor)
|
|
||||||
animateStrokeColorChange(fcmOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor)
|
|
||||||
selectedOptionView = fcmOptionView
|
|
||||||
}
|
|
||||||
fcmOptionView -> {
|
|
||||||
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
|
|
||||||
GlowViewUtilities.animateShadowColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme))
|
|
||||||
animateStrokeColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme))
|
|
||||||
selectedOptionView = null
|
|
||||||
}
|
|
||||||
backgroundPollingOptionView -> {
|
|
||||||
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
|
|
||||||
GlowViewUtilities.animateShadowColorChange(fcmOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor)
|
|
||||||
animateStrokeColorChange(fcmOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor)
|
|
||||||
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
|
|
||||||
GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme))
|
|
||||||
animateStrokeColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme))
|
|
||||||
selectedOptionView = fcmOptionView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleBackgroundPolling() = with(binding) {
|
|
||||||
val accentColor = getAccentColor()
|
|
||||||
when (selectedOptionView) {
|
|
||||||
null -> {
|
|
||||||
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
|
|
||||||
GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor)
|
|
||||||
animateStrokeColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor)
|
|
||||||
selectedOptionView = backgroundPollingOptionView
|
|
||||||
}
|
|
||||||
backgroundPollingOptionView -> {
|
|
||||||
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
|
|
||||||
GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme))
|
|
||||||
animateStrokeColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme))
|
|
||||||
selectedOptionView = null
|
|
||||||
}
|
|
||||||
fcmOptionView -> {
|
|
||||||
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
|
|
||||||
GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor)
|
|
||||||
animateStrokeColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor)
|
|
||||||
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
|
|
||||||
GlowViewUtilities.animateShadowColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme))
|
|
||||||
animateStrokeColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme))
|
|
||||||
selectedOptionView = backgroundPollingOptionView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateStrokeColorChange(bubble: PNModeView, @ColorInt startColor: Int, @ColorInt endColor: Int) {
|
|
||||||
val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor)
|
|
||||||
animation.duration = 250
|
|
||||||
animation.addUpdateListener { animator ->
|
|
||||||
val color = animator.animatedValue as Int
|
|
||||||
bubble.strokeColor = color
|
|
||||||
}
|
|
||||||
animation.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun register() {
|
|
||||||
if (selectedOptionView == null) {
|
|
||||||
showSessionDialog {
|
|
||||||
title(R.string.activity_pn_mode_no_option_picked_dialog_title)
|
|
||||||
button(R.string.ok)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
TextSecurePreferences.setPushEnabled(this, (selectedOptionView == binding.fcmOptionView))
|
|
||||||
val application = ApplicationContext.getInstance(this)
|
|
||||||
application.startPollingIfNeeded()
|
|
||||||
pushRegistry.refresh(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
|
|
||||||
}
|
|
@@ -1,165 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.onboarding
|
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.text.style.ClickableSpan
|
|
||||||
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
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
|
||||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
|
||||||
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() {
|
|
||||||
|
|
||||||
private val temporarySeedKey = "TEMPORARY_SEED_KEY"
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var configFactory: ConfigFactory
|
|
||||||
|
|
||||||
private lateinit var binding: ActivityRegisterBinding
|
|
||||||
internal val database: LokiAPIDatabaseProtocol
|
|
||||||
get() = SnodeModule.shared.storage
|
|
||||||
private var seed: ByteArray? = null
|
|
||||||
private var ed25519KeyPair: KeyPair? = null
|
|
||||||
private var x25519KeyPair: ECKeyPair? = null
|
|
||||||
set(value) { field = value; updatePublicKeyTextView() }
|
|
||||||
|
|
||||||
// region Lifecycle
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ActivityRegisterBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
setUpActionBarSessionLogo()
|
|
||||||
TextSecurePreferences.apply {
|
|
||||||
setHasViewedSeed(this@RegisterActivity, false)
|
|
||||||
setConfigurationMessageSynced(this@RegisterActivity, true)
|
|
||||||
setRestorationTime(this@RegisterActivity, 0)
|
|
||||||
setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
binding.registerButton.setOnClickListener { register() }
|
|
||||||
binding.copyButton.setOnClickListener { copyPublicKey() }
|
|
||||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
|
|
||||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(object : ClickableSpan() {
|
|
||||||
|
|
||||||
override fun onClick(widget: View) {
|
|
||||||
openURL("https://getsession.org/terms-of-service/")
|
|
||||||
}
|
|
||||||
}, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(object : ClickableSpan() {
|
|
||||||
|
|
||||||
override fun onClick(widget: View) {
|
|
||||||
openURL("https://getsession.org/privacy-policy/")
|
|
||||||
}
|
|
||||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
binding.termsTextView.text = termsExplanation
|
|
||||||
updateKeyPair(savedInstanceState?.getByteArray(temporarySeedKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
seed?.let { tempSeed ->
|
|
||||||
outState.putByteArray(temporarySeedKey, tempSeed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Updating
|
|
||||||
private fun updateKeyPair(temporaryKey: ByteArray?) {
|
|
||||||
val keyPairGenerationResult = temporaryKey?.let(KeyPairUtilities::generate) ?: KeyPairUtilities.generate()
|
|
||||||
seed = keyPairGenerationResult.seed
|
|
||||||
ed25519KeyPair = keyPairGenerationResult.ed25519KeyPair
|
|
||||||
x25519KeyPair = keyPairGenerationResult.x25519KeyPair
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePublicKeyTextView() {
|
|
||||||
val hexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey
|
|
||||||
val characterCount = hexEncodedPublicKey.count()
|
|
||||||
var count = 0
|
|
||||||
val limit = 32
|
|
||||||
fun animate() {
|
|
||||||
val numberOfIndexesToShuffle = 32 - count
|
|
||||||
val indexesToShuffle = (0 until characterCount).shuffled().subList(0, numberOfIndexesToShuffle)
|
|
||||||
var mangledHexEncodedPublicKey = hexEncodedPublicKey
|
|
||||||
for (index in indexesToShuffle) {
|
|
||||||
try {
|
|
||||||
mangledHexEncodedPublicKey = mangledHexEncodedPublicKey.substring(0, index) + "0123456789abcdef__".random() + mangledHexEncodedPublicKey.substring(index + 1, mangledHexEncodedPublicKey.count())
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count += 1
|
|
||||||
if (count < limit) {
|
|
||||||
binding.publicKeyTextView.text = mangledHexEncodedPublicKey
|
|
||||||
Handler().postDelayed({
|
|
||||||
animate()
|
|
||||||
}, 32)
|
|
||||||
} else {
|
|
||||||
binding.publicKeyTextView.text = hexEncodedPublicKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
animate()
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Interaction
|
|
||||||
private fun register() {
|
|
||||||
// This is here to resolve a case where the app restarts before a user completes onboarding
|
|
||||||
// which can result in an invalid database state
|
|
||||||
database.clearAllLastMessageHashes()
|
|
||||||
database.clearReceivedMessageHashValues()
|
|
||||||
KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!)
|
|
||||||
configFactory.keyPairChanged()
|
|
||||||
val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey
|
|
||||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
|
||||||
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
|
|
||||||
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
|
|
||||||
TextSecurePreferences.setRestorationTime(this, 0)
|
|
||||||
TextSecurePreferences.setHasViewedSeed(this, false)
|
|
||||||
val intent = Intent(this, DisplayNameActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyPublicKey() {
|
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clip = ClipData.newPlainText("Session ID", x25519KeyPair!!.hexEncodedPublicKey)
|
|
||||||
clipboard.setPrimaryClip(clip)
|
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openURL(url: String) {
|
|
||||||
try {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@@ -1,95 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.onboarding
|
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.style.ForegroundColorSpan
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.Toast
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.ActivitySeedBinding
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
|
||||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
|
||||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
|
||||||
|
|
||||||
class SeedActivity : BaseActionBarActivity() {
|
|
||||||
|
|
||||||
private lateinit var binding: ActivitySeedBinding
|
|
||||||
|
|
||||||
private val seed by lazy {
|
|
||||||
var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED)
|
|
||||||
if (hexEncodedSeed == null) {
|
|
||||||
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
|
|
||||||
}
|
|
||||||
val loadFileContents: (String) -> String = { fileName ->
|
|
||||||
MnemonicUtilities.loadFileContents(this, fileName)
|
|
||||||
}
|
|
||||||
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Lifecycle
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ActivitySeedBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_seed_title)
|
|
||||||
val seedReminderViewTitle = SpannableString("You're almost finished! 90%") // Intentionally not yet translated
|
|
||||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(getAccentColor()), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
with(binding) {
|
|
||||||
seedReminderView.title = seedReminderViewTitle
|
|
||||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2)
|
|
||||||
seedReminderView.setProgress(90, false)
|
|
||||||
seedReminderView.hideContinueButton()
|
|
||||||
var redactedSeed = seed
|
|
||||||
var index = 0
|
|
||||||
for (character in seed) {
|
|
||||||
if (character.isLetter()) {
|
|
||||||
redactedSeed = redactedSeed.replaceRange(index, index + 1, "▆")
|
|
||||||
}
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
seedTextView.setTextColor(getAccentColor())
|
|
||||||
seedTextView.text = redactedSeed
|
|
||||||
seedTextView.setOnLongClickListener { revealSeed(); true }
|
|
||||||
revealButton.setOnLongClickListener { revealSeed(); true }
|
|
||||||
copyButton.setOnClickListener { copySeed() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Updating
|
|
||||||
private fun revealSeed() {
|
|
||||||
val seedReminderViewTitle = SpannableString("Account secured! 100%") // Intentionally not yet translated
|
|
||||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(getAccentColor()), 17, 21, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
with(binding) {
|
|
||||||
seedReminderView.title = seedReminderViewTitle
|
|
||||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3)
|
|
||||||
seedReminderView.setProgress(100, true)
|
|
||||||
val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams
|
|
||||||
seedTextViewLayoutParams.height = seedTextView.height
|
|
||||||
seedTextView.layoutParams = seedTextViewLayoutParams
|
|
||||||
seedTextView.setTextColor(getColorFromAttr(android.R.attr.textColorPrimary))
|
|
||||||
seedTextView.text = seed
|
|
||||||
}
|
|
||||||
TextSecurePreferences.setHasViewedSeed(this, true)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Interaction
|
|
||||||
private fun copySeed() {
|
|
||||||
revealSeed()
|
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clip = ClipData.newPlainText("Seed", seed)
|
|
||||||
clipboard.setPrimaryClip(clip)
|
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@@ -1,59 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.onboarding
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import network.loki.messenger.databinding.ViewSeedReminderBinding
|
|
||||||
|
|
||||||
class SeedReminderView : FrameLayout {
|
|
||||||
private lateinit var binding: ViewSeedReminderBinding
|
|
||||||
|
|
||||||
var title: CharSequence
|
|
||||||
get() = binding.titleTextView.text
|
|
||||||
set(value) { binding.titleTextView.text = value }
|
|
||||||
var subtitle: CharSequence
|
|
||||||
get() = binding.subtitleTextView.text
|
|
||||||
set(value) { binding.subtitleTextView.text = value }
|
|
||||||
var delegate: SeedReminderViewDelegate? = null
|
|
||||||
|
|
||||||
constructor(context: Context) : super(context) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
|
||||||
setUpViewHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUpViewHierarchy() {
|
|
||||||
binding = ViewSeedReminderBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
binding.button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setProgress(progress: Int, isAnimated: Boolean) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
binding.progressBar.setProgress(progress, isAnimated)
|
|
||||||
} else {
|
|
||||||
binding.progressBar.progress = progress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideContinueButton() {
|
|
||||||
binding.button.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SeedReminderViewDelegate {
|
|
||||||
|
|
||||||
fun handleSeedReminderViewContinueButtonTapped()
|
|
||||||
}
|
|
@@ -0,0 +1,240 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.landing
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.ui.AlertDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.DialogButtonModel
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.SessionColorsParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.color.Colors
|
||||||
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.components.BorderlessHtmlButton
|
||||||
|
import org.thoughtcrime.securesms.ui.components.PrimaryFillButton
|
||||||
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.h4
|
||||||
|
import org.thoughtcrime.securesms.ui.large
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PreviewLandingScreen(
|
||||||
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
|
||||||
|
) {
|
||||||
|
PreviewTheme(colors) {
|
||||||
|
LandingScreen({}, {}, {}, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun LandingScreen(
|
||||||
|
createAccount: () -> Unit,
|
||||||
|
loadAccount: () -> Unit,
|
||||||
|
openTerms: () -> Unit,
|
||||||
|
openPrivacyPolicy: () -> Unit,
|
||||||
|
) {
|
||||||
|
var count by remember { mutableStateOf(0) }
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
var isUrlDialogVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (isUrlDialogVisible) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { isUrlDialogVisible = false },
|
||||||
|
title = stringResource(R.string.urlOpen),
|
||||||
|
text = stringResource(R.string.urlOpenBrowser),
|
||||||
|
buttons = listOf(
|
||||||
|
DialogButtonModel(
|
||||||
|
text = GetString(R.string.activity_landing_terms_of_service),
|
||||||
|
contentDescription = GetString(R.string.AccessibilityId_terms_of_service_button),
|
||||||
|
onClick = openTerms
|
||||||
|
),
|
||||||
|
DialogButtonModel(
|
||||||
|
text = GetString(R.string.activity_landing_privacy_policy),
|
||||||
|
contentDescription = GetString(R.string.AccessibilityId_privacy_policy_button),
|
||||||
|
onClick = openPrivacyPolicy
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(500.milliseconds)
|
||||||
|
while(count < MESSAGES.size) {
|
||||||
|
count += 1
|
||||||
|
listState.animateScrollToItem(0.coerceAtLeast((count - 1)))
|
||||||
|
delay(1500L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = LocalDimensions.current.onboardingMargin)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.onboardingBubblePrivacyInYourPocket),
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
style = h4,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.itemSpacing))
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.heightIn(min = LocalDimensions.current.minScrollableViewHeight)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(3f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallItemSpacing)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
MESSAGES.take(count),
|
||||||
|
key = { it.stringId }
|
||||||
|
) { item ->
|
||||||
|
AnimateMessageText(
|
||||||
|
stringResource(item.stringId),
|
||||||
|
item.isOutgoing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(horizontal = LocalDimensions.current.largeMargin)) {
|
||||||
|
PrimaryFillButton(
|
||||||
|
text = stringResource(R.string.onboardingAccountCreate),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.contentDescription(R.string.AccessibilityId_create_account_button),
|
||||||
|
onClick = createAccount
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.smallItemSpacing))
|
||||||
|
PrimaryOutlineButton(
|
||||||
|
stringResource(R.string.onboardingAccountExists),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.contentDescription(R.string.AccessibilityId_restore_account_button),
|
||||||
|
onClick = loadAccount
|
||||||
|
)
|
||||||
|
BorderlessHtmlButton(
|
||||||
|
textId = R.string.onboardingTosPrivacy,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.contentDescription(R.string.AccessibilityId_open_url),
|
||||||
|
onClick = { isUrlDialogVisible = true }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.xxsItemSpacing))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AnimateMessageText(text: String, isOutgoing: Boolean, modifier: Modifier = Modifier) {
|
||||||
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(Unit) { visible = true }
|
||||||
|
|
||||||
|
Box {
|
||||||
|
// TODO [SES-2077] Use LazyList itemAnimation when we update to compose 1.7 or so.
|
||||||
|
MessageText(text, isOutgoing, Modifier.alpha(0f))
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn(animationSpec = tween(durationMillis = 300)) +
|
||||||
|
slideInVertically(animationSpec = tween(durationMillis = 300)) { it }
|
||||||
|
) {
|
||||||
|
MessageText(text, isOutgoing, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageText(text: String, isOutgoing: Boolean, modifier: Modifier) {
|
||||||
|
Box(modifier = modifier then Modifier.fillMaxWidth()) {
|
||||||
|
MessageText(
|
||||||
|
text,
|
||||||
|
color = if (isOutgoing) LocalColors.current.backgroundBubbleSent else LocalColors.current.backgroundBubbleReceived,
|
||||||
|
textColor = if (isOutgoing) LocalColors.current.textBubbleSent else LocalColors.current.textBubbleReceived,
|
||||||
|
modifier = Modifier.align(if (isOutgoing) Alignment.TopEnd else Alignment.TopStart)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageText(
|
||||||
|
text: String,
|
||||||
|
color: Color,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
textColor: Color = Color.Unspecified
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(0.666f),
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
backgroundColor = color,
|
||||||
|
elevation = 0.dp
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style = large,
|
||||||
|
color = textColor,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
horizontal = LocalDimensions.current.smallItemSpacing,
|
||||||
|
vertical = LocalDimensions.current.xsItemSpacing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class TextData(
|
||||||
|
@StringRes val stringId: Int,
|
||||||
|
val isOutgoing: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
private val MESSAGES = listOf(
|
||||||
|
TextData(R.string.onboardingBubbleWelcomeToSession),
|
||||||
|
TextData(R.string.onboardingBubbleSessionIsEngineered, isOutgoing = true),
|
||||||
|
TextData(R.string.onboardingBubbleNoPhoneNumber),
|
||||||
|
TextData(R.string.onboardingBubbleCreatingAnAccountIsEasy, isOutgoing = true)
|
||||||
|
)
|
@@ -1,16 +1,25 @@
|
|||||||
package org.thoughtcrime.securesms.onboarding
|
package org.thoughtcrime.securesms.onboarding.landing
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import network.loki.messenger.databinding.ActivityLandingBinding
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||||
|
import org.thoughtcrime.securesms.onboarding.loadaccount.LoadAccountActivity
|
||||||
|
import org.thoughtcrime.securesms.onboarding.pickname.startPickDisplayNameActivity
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
|
import org.thoughtcrime.securesms.util.start
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LandingActivity : BaseActionBarActivity() {
|
@AndroidEntryPoint
|
||||||
|
class LandingActivity: BaseActionBarActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
internal lateinit var prefs: TextSecurePreferences
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -19,28 +28,24 @@ class LandingActivity : BaseActionBarActivity() {
|
|||||||
// Session then close this activity to resume the last activity from the previous instance.
|
// Session then close this activity to resume the last activity from the previous instance.
|
||||||
if (!isTaskRoot) { finish(); return }
|
if (!isTaskRoot) { finish(); return }
|
||||||
|
|
||||||
val binding = ActivityLandingBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
setUpActionBarSessionLogo(true)
|
setUpActionBarSessionLogo(true)
|
||||||
with(binding) {
|
|
||||||
fakeChatView.startAnimating()
|
setComposeContent {
|
||||||
registerButton.setOnClickListener { register() }
|
LandingScreen(
|
||||||
restoreButton.setOnClickListener { link() }
|
createAccount = { startPickDisplayNameActivity() },
|
||||||
linkButton.setOnClickListener { link() }
|
loadAccount = { start<LoadAccountActivity>() },
|
||||||
|
openTerms = { open("https://getsession.org/terms-of-service") },
|
||||||
|
openPrivacyPolicy = { open("https://getsession.org/privacy-policy") }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IdentityKeyUtil.generateIdentityKeyPair(this)
|
IdentityKeyUtil.generateIdentityKeyPair(this)
|
||||||
TextSecurePreferences.setPasswordDisabled(this, true)
|
TextSecurePreferences.setPasswordDisabled(this, true)
|
||||||
// AC: This is a temporary workaround to trick the old code that the screen is unlocked.
|
// AC: This is a temporary workaround to trick the old code that the screen is unlocked.
|
||||||
KeyCachingService.setMasterSecret(applicationContext, Object())
|
KeyCachingService.setMasterSecret(applicationContext, Object())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun register() {
|
private fun open(url: String) {
|
||||||
val intent = Intent(this, RegisterActivity::class.java)
|
Intent(Intent.ACTION_VIEW, Uri.parse(url)).let(::startActivity)
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun link() {
|
|
||||||
val intent = Intent(this, LinkDeviceActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,118 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.loadaccount
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.base
|
||||||
|
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||||
|
import org.thoughtcrime.securesms.ui.h4
|
||||||
|
|
||||||
|
private val TITLES = listOf(R.string.sessionRecoveryPassword, R.string.qrScan)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun LoadAccountScreen(
|
||||||
|
state: State,
|
||||||
|
qrErrors: Flow<String>,
|
||||||
|
onChange: (String) -> Unit = {},
|
||||||
|
onContinue: () -> Unit = {},
|
||||||
|
onScan: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val pagerState = rememberPagerState { TITLES.size }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
SessionTabRow(pagerState, TITLES)
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) { page ->
|
||||||
|
when (TITLES[page]) {
|
||||||
|
R.string.sessionRecoveryPassword -> RecoveryPassword(state, onChange, onContinue)
|
||||||
|
R.string.qrScan -> MaybeScanQrCode(qrErrors, onScan = onScan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PreviewRecoveryPassword() {
|
||||||
|
PreviewTheme {
|
||||||
|
RecoveryPassword(state = State())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RecoveryPassword(state: State, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.smallItemSpacing))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = LocalDimensions.current.largeMargin)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.sessionRecoveryPassword),
|
||||||
|
style = h4
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(LocalDimensions.current.xxsItemSpacing))
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
|
painter = painterResource(id = R.drawable.ic_shield_outline),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(LocalDimensions.current.smallItemSpacing))
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.activity_link_enter_your_recovery_password_to_load_your_account_if_you_haven_t_saved_it_you_can_find_it_in_your_app_settings),
|
||||||
|
style = base
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(LocalDimensions.current.itemSpacing))
|
||||||
|
SessionOutlinedTextField(
|
||||||
|
text = state.recoveryPhrase,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentDescription = stringResource(R.string.AccessibilityId_recovery_phrase_input),
|
||||||
|
placeholder = stringResource(R.string.recoveryPasswordEnter),
|
||||||
|
onChange = onChange,
|
||||||
|
onContinue = onContinue,
|
||||||
|
error = state.error,
|
||||||
|
isTextErrorColor = state.isTextErrorColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.smallItemSpacing))
|
||||||
|
Spacer(Modifier.weight(2f))
|
||||||
|
|
||||||
|
ContinuePrimaryOutlineButton(modifier = Modifier.align(Alignment.CenterHorizontally), onContinue)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,48 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.loadaccount
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
|
import org.thoughtcrime.securesms.onboarding.manager.LoadAccountManager
|
||||||
|
import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity
|
||||||
|
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||||
|
import org.thoughtcrime.securesms.util.start
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class LoadAccountActivity : BaseActionBarActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
internal lateinit var prefs: TextSecurePreferences
|
||||||
|
@Inject
|
||||||
|
internal lateinit var loadAccountManager: LoadAccountManager
|
||||||
|
|
||||||
|
private val viewModel: LoadAccountViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
supportActionBar?.setTitle(R.string.activity_link_load_account)
|
||||||
|
prefs.setConfigurationMessageSynced(false)
|
||||||
|
prefs.setRestorationTime(System.currentTimeMillis())
|
||||||
|
prefs.setLastProfileUpdateTime(0)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.events.collect {
|
||||||
|
loadAccountManager.load(it.mnemonic)
|
||||||
|
start<MessageNotificationsActivity>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setComposeContent {
|
||||||
|
val state by viewModel.stateFlow.collectAsState()
|
||||||
|
LoadAccountScreen(state, viewModel.qrErrors, viewModel::onChange, viewModel::onContinue, viewModel::onScanQrCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,90 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.loadaccount
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsignal.crypto.MnemonicCodec
|
||||||
|
import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InputTooShort
|
||||||
|
import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InvalidWord
|
||||||
|
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LoadAccountEvent(val mnemonic: ByteArray)
|
||||||
|
|
||||||
|
internal data class State(
|
||||||
|
val recoveryPhrase: String = "",
|
||||||
|
val isTextErrorColor: Boolean = false,
|
||||||
|
val error: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
internal class LoadAccountViewModel @Inject constructor(
|
||||||
|
private val application: Application
|
||||||
|
): AndroidViewModel(application) {
|
||||||
|
private val state = MutableStateFlow(State())
|
||||||
|
val stateFlow = state.asStateFlow()
|
||||||
|
|
||||||
|
private val _events = MutableSharedFlow<LoadAccountEvent>()
|
||||||
|
val events = _events.asSharedFlow()
|
||||||
|
|
||||||
|
private val _qrErrors = MutableSharedFlow<Throwable>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
val qrErrors = _qrErrors.asSharedFlow()
|
||||||
|
.mapNotNull { application.getString(R.string.qrNotRecoveryPassword) }
|
||||||
|
|
||||||
|
private val codec by lazy { MnemonicCodec { MnemonicUtilities.loadFileContents(getApplication(), it) } }
|
||||||
|
|
||||||
|
fun onContinue() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
codec.sanitizeAndDecodeAsByteArray(state.value.recoveryPhrase).let(::onSuccess)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onFailure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onScanQrCode(string: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
codec.decodeMnemonicOrHexAsByteArray(string).let(::onSuccess)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onQrCodeScanFailure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onChange(recoveryPhrase: String) {
|
||||||
|
state.update { it.copy(recoveryPhrase = recoveryPhrase, isTextErrorColor = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSuccess(seed: ByteArray) {
|
||||||
|
viewModelScope.launch { _events.emit(LoadAccountEvent(seed)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFailure(error: Throwable) {
|
||||||
|
state.update {
|
||||||
|
it.copy(
|
||||||
|
isTextErrorColor = true,
|
||||||
|
error = when (error) {
|
||||||
|
is InvalidWord -> R.string.recoveryPasswordErrorMessageIncorrect
|
||||||
|
is InputTooShort -> R.string.recoveryPasswordErrorMessageShort
|
||||||
|
else -> R.string.recoveryPasswordErrorMessageGeneric
|
||||||
|
}.let(application::getString)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onQrCodeScanFailure(error: Throwable) {
|
||||||
|
viewModelScope.launch { _qrErrors.emit(error) }
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,37 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.loading
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.ProgressArc
|
||||||
|
import org.thoughtcrime.securesms.ui.base
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.h7
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun LoadingScreen(progress: Float) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
ProgressArc(
|
||||||
|
progress,
|
||||||
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_loading_animation)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.waitOneMoment),
|
||||||
|
style = h7
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsItemSpacing))
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.loadAccountProgressMessage),
|
||||||
|
style = base
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(2f))
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,60 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.loading
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
|
import org.thoughtcrime.securesms.home.startHomeActivity
|
||||||
|
import org.thoughtcrime.securesms.onboarding.pickname.startPickDisplayNameActivity
|
||||||
|
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||||
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class LoadingActivity: BaseActionBarActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
internal lateinit var configFactory: ConfigFactory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
internal lateinit var prefs: TextSecurePreferences
|
||||||
|
|
||||||
|
private val viewModel: LoadingViewModel by viewModels()
|
||||||
|
|
||||||
|
private fun register(loadFailed: Boolean) {
|
||||||
|
prefs.setLastConfigurationSyncTime(System.currentTimeMillis())
|
||||||
|
|
||||||
|
when {
|
||||||
|
loadFailed -> startPickDisplayNameActivity(loadFailed = true)
|
||||||
|
else -> startHomeActivity(isNewAccount = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setUpActionBarSessionLogo()
|
||||||
|
|
||||||
|
setComposeContent {
|
||||||
|
val progress by viewModel.progress.collectAsState()
|
||||||
|
LoadingScreen(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.events.collect {
|
||||||
|
when (it) {
|
||||||
|
Event.TIMEOUT -> register(loadFailed = true)
|
||||||
|
Event.SUCCESS -> register(loadFailed = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,121 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.loading
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.buffer
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.timeout
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
LOADING,
|
||||||
|
SUCCESS,
|
||||||
|
FAIL
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ANIMATE_TO_DONE_TIME = 500.milliseconds
|
||||||
|
private val IDLE_DONE_TIME = 1.seconds
|
||||||
|
private val TIMEOUT_TIME = 15.seconds
|
||||||
|
|
||||||
|
private val REFRESH_TIME = 50.milliseconds
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||||
|
@HiltViewModel
|
||||||
|
internal class LoadingViewModel @Inject constructor(
|
||||||
|
val prefs: TextSecurePreferences
|
||||||
|
): ViewModel() {
|
||||||
|
|
||||||
|
private val state = MutableStateFlow(State.LOADING)
|
||||||
|
|
||||||
|
private val _progress = MutableStateFlow(0f)
|
||||||
|
val progress = _progress.asStateFlow()
|
||||||
|
|
||||||
|
private val _events = MutableSharedFlow<Event>()
|
||||||
|
val events = _events.asSharedFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
state.flatMapLatest {
|
||||||
|
when (it) {
|
||||||
|
State.LOADING -> progress(0f, 1f, TIMEOUT_TIME)
|
||||||
|
else -> progress(progress.value, 1f, ANIMATE_TO_DONE_TIME)
|
||||||
|
}
|
||||||
|
}.buffer(0, BufferOverflow.DROP_OLDEST)
|
||||||
|
.collectLatest { _progress.value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
TextSecurePreferences.events
|
||||||
|
.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }
|
||||||
|
.onStart { emit(TextSecurePreferences.CONFIGURATION_SYNCED) }
|
||||||
|
.filter { prefs.getConfigurationMessageSynced() }
|
||||||
|
.timeout(TIMEOUT_TIME)
|
||||||
|
.collectLatest { onSuccess() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onFail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onSuccess() {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
state.value = State.SUCCESS
|
||||||
|
delay(IDLE_DONE_TIME)
|
||||||
|
_events.emit(Event.SUCCESS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onFail() {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
state.value = State.FAIL
|
||||||
|
delay(IDLE_DONE_TIME)
|
||||||
|
_events.emit(Event.TIMEOUT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface Event {
|
||||||
|
object SUCCESS: Event
|
||||||
|
object TIMEOUT: Event
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun progress(
|
||||||
|
init: Float,
|
||||||
|
target: Float,
|
||||||
|
time: Duration,
|
||||||
|
refreshRate: Duration = REFRESH_TIME
|
||||||
|
): Flow<Float> = flow {
|
||||||
|
val startMs = System.currentTimeMillis()
|
||||||
|
val timeMs = time.inWholeMilliseconds
|
||||||
|
val finishMs = startMs + timeMs
|
||||||
|
val range = target - init
|
||||||
|
|
||||||
|
generateSequence { System.currentTimeMillis() }.takeWhile { it < finishMs }.forEach {
|
||||||
|
emit((it - startMs) * range / timeMs + init)
|
||||||
|
delay(refreshRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(target)
|
||||||
|
}
|
@@ -0,0 +1,45 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.manager
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import org.session.libsession.snode.SnodeModule
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||||
|
import org.session.libsignal.utilities.KeyHelper
|
||||||
|
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||||
|
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class CreateAccountManager @Inject constructor(
|
||||||
|
private val application: Application,
|
||||||
|
private val prefs: TextSecurePreferences,
|
||||||
|
private val configFactory: ConfigFactory,
|
||||||
|
) {
|
||||||
|
private val database: LokiAPIDatabaseProtocol
|
||||||
|
get() = SnodeModule.shared.storage
|
||||||
|
|
||||||
|
fun createAccount(displayName: String) {
|
||||||
|
prefs.setProfileName(displayName)
|
||||||
|
configFactory.user?.setName(displayName)
|
||||||
|
|
||||||
|
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||||
|
// which can result in an invalid database state
|
||||||
|
database.clearAllLastMessageHashes()
|
||||||
|
database.clearReceivedMessageHashValues()
|
||||||
|
|
||||||
|
val keyPairGenerationResult = KeyPairUtilities.generate()
|
||||||
|
val seed = keyPairGenerationResult.seed
|
||||||
|
val ed25519KeyPair = keyPairGenerationResult.ed25519KeyPair
|
||||||
|
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
|
||||||
|
|
||||||
|
KeyPairUtilities.store(application, seed, ed25519KeyPair, x25519KeyPair)
|
||||||
|
configFactory.keyPairChanged()
|
||||||
|
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
|
||||||
|
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||||
|
prefs.setLocalRegistrationId(registrationID)
|
||||||
|
prefs.setLocalNumber(userHexEncodedPublicKey)
|
||||||
|
prefs.setRestorationTime(0)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,58 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.manager
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.session.libsession.snode.SnodeModule
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||||
|
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class LoadAccountManager @Inject constructor(
|
||||||
|
@dagger.hilt.android.qualifiers.ApplicationContext private val context: Context,
|
||||||
|
private val configFactory: ConfigFactory,
|
||||||
|
private val prefs: TextSecurePreferences
|
||||||
|
) {
|
||||||
|
private val database: LokiAPIDatabaseProtocol
|
||||||
|
get() = SnodeModule.shared.storage
|
||||||
|
|
||||||
|
private var restoreJob: Job? = null
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
fun load(seed: ByteArray) {
|
||||||
|
// only have one sync job running at a time (prevent QR from trying to spawn a new job)
|
||||||
|
if (restoreJob?.isActive == true) return
|
||||||
|
|
||||||
|
restoreJob = scope.launch {
|
||||||
|
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||||
|
// which can result in an invalid database state
|
||||||
|
database.clearAllLastMessageHashes()
|
||||||
|
database.clearReceivedMessageHashValues()
|
||||||
|
|
||||||
|
// RestoreActivity handles seed this way
|
||||||
|
val keyPairGenerationResult = KeyPairUtilities.generate(seed)
|
||||||
|
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
|
||||||
|
KeyPairUtilities.store(context, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair)
|
||||||
|
configFactory.keyPairChanged()
|
||||||
|
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
|
||||||
|
val registrationID = org.session.libsignal.utilities.KeyHelper.generateRegistrationId(false)
|
||||||
|
prefs.apply {
|
||||||
|
setLocalRegistrationId(registrationID)
|
||||||
|
setLocalNumber(userHexEncodedPublicKey)
|
||||||
|
setRestorationTime(System.currentTimeMillis())
|
||||||
|
setHasViewedSeed(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationContext.getInstance(context).retrieveUserProfile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,149 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.messagenotifications
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.onboarding.OnboardingBackPressAlertDialog
|
||||||
|
import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsViewModel.UiState
|
||||||
|
import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.AlertDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.DialogButtonModel
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.SessionColorsParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.base
|
||||||
|
import org.thoughtcrime.securesms.ui.color.Colors
|
||||||
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.components.CircularProgressIndicator
|
||||||
|
import org.thoughtcrime.securesms.ui.components.RadioButton
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.h4
|
||||||
|
import org.thoughtcrime.securesms.ui.h8
|
||||||
|
import org.thoughtcrime.securesms.ui.h9
|
||||||
|
import org.thoughtcrime.securesms.ui.small
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MessageNotificationsScreen(
|
||||||
|
state: UiState = UiState(),
|
||||||
|
setEnabled: (Boolean) -> Unit = {},
|
||||||
|
onContinue: () -> Unit = {},
|
||||||
|
quit: () -> Unit = {},
|
||||||
|
dismissDialog: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
if (state.clearData) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(LocalColors.current.primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit = quit)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(horizontal = LocalDimensions.current.onboardingMargin)) {
|
||||||
|
Text(stringResource(R.string.notificationsMessage), style = h4)
|
||||||
|
Spacer(Modifier.height(LocalDimensions.current.xsMargin))
|
||||||
|
Text(stringResource(R.string.onboardingMessageNotificationExplaination), style = base)
|
||||||
|
Spacer(Modifier.height(LocalDimensions.current.itemSpacing))
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationRadioButton(
|
||||||
|
R.string.activity_pn_mode_fast_mode,
|
||||||
|
R.string.activity_pn_mode_fast_mode_explanation,
|
||||||
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_fast_mode_notifications_button),
|
||||||
|
tag = R.string.activity_pn_mode_recommended_option_tag,
|
||||||
|
checked = state.pushEnabled,
|
||||||
|
onClick = { setEnabled(true) }
|
||||||
|
)
|
||||||
|
|
||||||
|
// spacing between buttons is provided by ripple/downstate of NotificationRadioButton
|
||||||
|
|
||||||
|
NotificationRadioButton(
|
||||||
|
R.string.activity_pn_mode_slow_mode,
|
||||||
|
R.string.activity_pn_mode_slow_mode_explanation,
|
||||||
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_slow_mode_notifications_button),
|
||||||
|
checked = state.pushDisabled,
|
||||||
|
onClick = { setEnabled(false) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
ContinuePrimaryOutlineButton(Modifier.align(Alignment.CenterHorizontally), onContinue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NotificationRadioButton(
|
||||||
|
@StringRes title: Int,
|
||||||
|
@StringRes explanation: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
@StringRes tag: Int? = null,
|
||||||
|
checked: Boolean = false,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
checked = checked,
|
||||||
|
contentPadding = PaddingValues(horizontal = LocalDimensions.current.margin, vertical = 7.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.border(
|
||||||
|
LocalDimensions.current.borderStroke,
|
||||||
|
LocalColors.current.borders,
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.padding(horizontal = 15.dp)
|
||||||
|
.padding(top = 10.dp, bottom = 11.dp)) {
|
||||||
|
Text(stringResource(title), style = h8)
|
||||||
|
|
||||||
|
Text(stringResource(explanation), style = small, modifier = Modifier.padding(top = 7.dp))
|
||||||
|
tag?.let {
|
||||||
|
Text(
|
||||||
|
stringResource(it),
|
||||||
|
modifier = Modifier.padding(top = 6.dp),
|
||||||
|
color = LocalColors.current.primary,
|
||||||
|
style = h9
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun MessageNotificationsScreenPreview(
|
||||||
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
|
||||||
|
) {
|
||||||
|
PreviewTheme(colors) {
|
||||||
|
MessageNotificationsScreen()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,81 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.messagenotifications
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
|
import org.thoughtcrime.securesms.home.startHomeActivity
|
||||||
|
import org.thoughtcrime.securesms.onboarding.loading.LoadingActivity
|
||||||
|
import org.thoughtcrime.securesms.onboarding.manager.LoadAccountManager
|
||||||
|
import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity.Companion.EXTRA_PROFILE_NAME
|
||||||
|
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||||
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
|
import org.thoughtcrime.securesms.util.start
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class MessageNotificationsActivity : BaseActionBarActivity() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_PROFILE_NAME = "EXTRA_PROFILE_NAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
internal lateinit var viewModelFactory: MessageNotificationsViewModel.AssistedFactory
|
||||||
|
|
||||||
|
@Inject lateinit var prefs: TextSecurePreferences
|
||||||
|
@Inject lateinit var loadAccountManager: LoadAccountManager
|
||||||
|
|
||||||
|
val profileName by lazy { intent.getStringExtra(EXTRA_PROFILE_NAME) }
|
||||||
|
|
||||||
|
private val viewModel: MessageNotificationsViewModel by viewModels {
|
||||||
|
viewModelFactory.create(profileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setUpActionBarSessionLogo()
|
||||||
|
|
||||||
|
setComposeContent { MessageNotificationsScreen() }
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.events.collect {
|
||||||
|
when (it) {
|
||||||
|
Event.Loading -> start<LoadingActivity>()
|
||||||
|
Event.OnboardingComplete -> startHomeActivity(isNewAccount = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (viewModel.onBackPressed()) return
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageNotificationsScreen() {
|
||||||
|
val uiState by viewModel.uiStates.collectAsState()
|
||||||
|
MessageNotificationsScreen(
|
||||||
|
uiState,
|
||||||
|
setEnabled = viewModel::setEnabled,
|
||||||
|
onContinue = viewModel::onContinue,
|
||||||
|
quit = viewModel::quit,
|
||||||
|
dismissDialog = viewModel::dismissDialog
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.startMessageNotificationsActivity(profileName: String) {
|
||||||
|
start<MessageNotificationsActivity> { putExtra(EXTRA_PROFILE_NAME, profileName) }
|
||||||
|
}
|
@@ -0,0 +1,120 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.messagenotifications
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||||
|
import org.thoughtcrime.securesms.onboarding.manager.CreateAccountManager
|
||||||
|
|
||||||
|
internal class MessageNotificationsViewModel(
|
||||||
|
private val state: State,
|
||||||
|
private val application: Application,
|
||||||
|
private val prefs: TextSecurePreferences,
|
||||||
|
private val pushRegistry: PushRegistry,
|
||||||
|
private val createAccountManager: CreateAccountManager
|
||||||
|
): AndroidViewModel(application) {
|
||||||
|
private val _uiStates = MutableStateFlow(UiState())
|
||||||
|
val uiStates = _uiStates.asStateFlow()
|
||||||
|
|
||||||
|
private val _events = MutableSharedFlow<Event>()
|
||||||
|
val events = _events.asSharedFlow()
|
||||||
|
|
||||||
|
fun setEnabled(enabled: Boolean) {
|
||||||
|
_uiStates.update { UiState(pushEnabled = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onContinue() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
if (state is State.CreateAccount) createAccountManager.createAccount(state.displayName)
|
||||||
|
|
||||||
|
prefs.setPushEnabled(uiStates.value.pushEnabled)
|
||||||
|
pushRegistry.refresh(true)
|
||||||
|
|
||||||
|
_events.emit(
|
||||||
|
when (state) {
|
||||||
|
is State.CreateAccount -> Event.OnboardingComplete
|
||||||
|
else -> Event.Loading
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return [true] if the back press was handled.
|
||||||
|
*/
|
||||||
|
fun onBackPressed(): Boolean = when (state) {
|
||||||
|
is State.CreateAccount -> false
|
||||||
|
is State.LoadAccount -> {
|
||||||
|
_uiStates.update { it.copy(showDialog = true) }
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissDialog() {
|
||||||
|
_uiStates.update { it.copy(showDialog = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun quit() {
|
||||||
|
_uiStates.update { it.copy(clearData = true) }
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
ApplicationContext.getInstance(application).clearAllData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UiState(
|
||||||
|
val pushEnabled: Boolean = true,
|
||||||
|
val showDialog: Boolean = false,
|
||||||
|
val clearData: Boolean = false
|
||||||
|
) {
|
||||||
|
val pushDisabled get() = !pushEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface State {
|
||||||
|
class CreateAccount(val displayName: String): State
|
||||||
|
object LoadAccount: State
|
||||||
|
}
|
||||||
|
|
||||||
|
@dagger.assisted.AssistedFactory
|
||||||
|
interface AssistedFactory {
|
||||||
|
fun create(profileName: String?): Factory
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
class Factory @AssistedInject constructor(
|
||||||
|
@Assisted private val profileName: String?,
|
||||||
|
private val application: Application,
|
||||||
|
private val prefs: TextSecurePreferences,
|
||||||
|
private val pushRegistry: PushRegistry,
|
||||||
|
private val createAccountManager: CreateAccountManager,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return MessageNotificationsViewModel(
|
||||||
|
state = profileName?.let(State::CreateAccount) ?: State.LoadAccount,
|
||||||
|
application = application,
|
||||||
|
prefs = prefs,
|
||||||
|
pushRegistry = pushRegistry,
|
||||||
|
createAccountManager = createAccountManager
|
||||||
|
) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Event {
|
||||||
|
OnboardingComplete, Loading
|
||||||
|
}
|
@@ -0,0 +1,84 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.pickname
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.onboarding.OnboardingBackPressAlertDialog
|
||||||
|
import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.base
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
||||||
|
import org.thoughtcrime.securesms.ui.h4
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PreviewPickDisplayName() {
|
||||||
|
PreviewTheme {
|
||||||
|
PickDisplayName(State())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun PickDisplayName(
|
||||||
|
state: State,
|
||||||
|
onChange: (String) -> Unit = {},
|
||||||
|
onContinue: () -> Unit = {},
|
||||||
|
dismissDialog: () -> Unit = {},
|
||||||
|
quit: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (state.showDialog) OnboardingBackPressAlertDialog(
|
||||||
|
dismissDialog,
|
||||||
|
R.string.you_cannot_go_back_further_cancel_account_creation,
|
||||||
|
quit
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.smallItemSpacing))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = LocalDimensions.current.largeMargin)
|
||||||
|
) {
|
||||||
|
Text(stringResource(state.title), style = h4)
|
||||||
|
Spacer(Modifier.height(LocalDimensions.current.smallItemSpacing))
|
||||||
|
Text(
|
||||||
|
stringResource(state.description),
|
||||||
|
style = base,
|
||||||
|
modifier = Modifier.padding(bottom = LocalDimensions.current.xsItemSpacing))
|
||||||
|
Spacer(Modifier.height(LocalDimensions.current.itemSpacing))
|
||||||
|
SessionOutlinedTextField(
|
||||||
|
text = state.displayName,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentDescription = stringResource(R.string.AccessibilityId_enter_display_name),
|
||||||
|
placeholder = stringResource(R.string.displayNameEnter),
|
||||||
|
onChange = onChange,
|
||||||
|
onContinue = onContinue,
|
||||||
|
error = state.error?.let { stringResource(it) },
|
||||||
|
isTextErrorColor = state.isTextErrorColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.smallItemSpacing))
|
||||||
|
Spacer(Modifier.weight(2f))
|
||||||
|
|
||||||
|
ContinuePrimaryOutlineButton(modifier = Modifier.align(Alignment.CenterHorizontally), onContinue)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,79 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.pickname
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
|
import org.thoughtcrime.securesms.home.startHomeActivity
|
||||||
|
import org.thoughtcrime.securesms.onboarding.messagenotifications.startMessageNotificationsActivity
|
||||||
|
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||||
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val EXTRA_LOAD_FAILED = "extra_load_failed"
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class PickDisplayNameActivity : BaseActionBarActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
internal lateinit var viewModelFactory: PickDisplayNameViewModel.AssistedFactory
|
||||||
|
@Inject
|
||||||
|
internal lateinit var prefs: TextSecurePreferences
|
||||||
|
|
||||||
|
private val loadFailed get() = intent.getBooleanExtra(EXTRA_LOAD_FAILED, false)
|
||||||
|
|
||||||
|
private val viewModel: PickDisplayNameViewModel by viewModels {
|
||||||
|
viewModelFactory.create(loadFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setUpActionBarSessionLogo()
|
||||||
|
|
||||||
|
setComposeContent { DisplayNameScreen(viewModel) }
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
viewModel.events.collect {
|
||||||
|
when (it) {
|
||||||
|
is Event.CreateAccount -> startMessageNotificationsActivity(it.profileName)
|
||||||
|
Event.LoadAccountComplete -> startHomeActivity(isNewAccount = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DisplayNameScreen(viewModel: PickDisplayNameViewModel) {
|
||||||
|
PickDisplayName(
|
||||||
|
viewModel.states.collectAsState().value,
|
||||||
|
viewModel::onChange,
|
||||||
|
viewModel::onContinue,
|
||||||
|
viewModel::dismissDialog,
|
||||||
|
quit = { viewModel.dismissDialog(); finish() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (viewModel.onBackPressed()) return
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.startPickDisplayNameActivity(loadFailed: Boolean = false, flags: Int = 0) {
|
||||||
|
Intent(this, PickDisplayNameActivity::class.java)
|
||||||
|
.apply { putExtra(EXTRA_LOAD_FAILED, loadFailed) }
|
||||||
|
.also { it.flags = flags }
|
||||||
|
.also(::startActivity)
|
||||||
|
}
|
@@ -0,0 +1,116 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.pickname
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
|
import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsViewModel
|
||||||
|
|
||||||
|
internal class PickDisplayNameViewModel(
|
||||||
|
private val loadFailed: Boolean,
|
||||||
|
private val prefs: TextSecurePreferences,
|
||||||
|
private val configFactory: ConfigFactory
|
||||||
|
): ViewModel() {
|
||||||
|
private val isCreateAccount = !loadFailed
|
||||||
|
|
||||||
|
private val _states = MutableStateFlow(if (loadFailed) pickNewNameState() else State())
|
||||||
|
val states = _states.asStateFlow()
|
||||||
|
|
||||||
|
private val _events = MutableSharedFlow<Event>()
|
||||||
|
val events = _events.asSharedFlow()
|
||||||
|
|
||||||
|
fun onContinue() {
|
||||||
|
_states.update { it.copy(displayName = it.displayName.trim()) }
|
||||||
|
|
||||||
|
val displayName = _states.value.displayName
|
||||||
|
|
||||||
|
when {
|
||||||
|
displayName.isEmpty() -> { _states.update { it.copy(isTextErrorColor = true, error = R.string.displayNameErrorDescription) } }
|
||||||
|
displayName.toByteArray().size > NAME_PADDED_LENGTH -> { _states.update { it.copy(isTextErrorColor = true, error = R.string.displayNameErrorDescriptionShorter) } }
|
||||||
|
else -> {
|
||||||
|
// success - clear the error as we can still see it during the transition to the
|
||||||
|
// next screen.
|
||||||
|
_states.update { it.copy(isTextErrorColor = false, error = null) }
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
if (loadFailed) {
|
||||||
|
prefs.setProfileName(displayName)
|
||||||
|
configFactory.user?.setName(displayName)
|
||||||
|
|
||||||
|
_events.emit(Event.LoadAccountComplete)
|
||||||
|
} else _events.emit(Event.CreateAccount(displayName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onChange(value: String) {
|
||||||
|
_states.update { state ->
|
||||||
|
state.copy(
|
||||||
|
displayName = value,
|
||||||
|
isTextErrorColor = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return [true] if the back press was handled.
|
||||||
|
*/
|
||||||
|
fun onBackPressed(): Boolean = isCreateAccount.also {
|
||||||
|
if (it) _states.update { it.copy(showDialog = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissDialog() {
|
||||||
|
_states.update { it.copy(showDialog = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@dagger.assisted.AssistedFactory
|
||||||
|
interface AssistedFactory {
|
||||||
|
fun create(loadFailed: Boolean): Factory
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
class Factory @AssistedInject constructor(
|
||||||
|
@Assisted private val loadFailed: Boolean,
|
||||||
|
private val prefs: TextSecurePreferences,
|
||||||
|
private val configFactory: ConfigFactory
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return PickDisplayNameViewModel(loadFailed, prefs, configFactory) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
@StringRes val title: Int = R.string.displayNamePick,
|
||||||
|
@StringRes val description: Int = R.string.displayNameDescription,
|
||||||
|
val showDialog: Boolean = false,
|
||||||
|
val isTextErrorColor: Boolean = false,
|
||||||
|
@StringRes val error: Int? = null,
|
||||||
|
val displayName: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
fun pickNewNameState() = State(
|
||||||
|
title = R.string.displayNameNew,
|
||||||
|
description = R.string.displayNameErrorNew
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface Event {
|
||||||
|
class CreateAccount(val profileName: String): Event
|
||||||
|
object LoadAccountComplete: Event
|
||||||
|
}
|
@@ -0,0 +1,24 @@
|
|||||||
|
package org.thoughtcrime.securesms.onboarding.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContinuePrimaryOutlineButton(modifier: Modifier, onContinue: () -> Unit) {
|
||||||
|
PrimaryOutlineButton(
|
||||||
|
stringResource(R.string.continue_2),
|
||||||
|
modifier = modifier
|
||||||
|
.contentDescription(R.string.AccessibilityId_continue)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = LocalDimensions.current.largeMargin)
|
||||||
|
.padding(bottom = LocalDimensions.current.xxsMargin),
|
||||||
|
onClick = onContinue,
|
||||||
|
)
|
||||||
|
}
|
@@ -125,7 +125,7 @@ class ClearAllDataDialog : DialogFragment() {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ApplicationContext.getInstance(context).clearAllData(false).let { success ->
|
ApplicationContext.getInstance(context).clearAllData().let { success ->
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
if (success) {
|
if (success) {
|
||||||
dismiss()
|
dismiss()
|
||||||
@@ -162,7 +162,7 @@ class ClearAllDataDialog : DialogFragment() {
|
|||||||
}
|
}
|
||||||
else if (deletionResultMap.values.all { it }) {
|
else if (deletionResultMap.values.all { it }) {
|
||||||
// ..otherwise if the network data deletion was successful proceed to delete the local data as well.
|
// ..otherwise if the network data deletion was successful proceed to delete the local data as well.
|
||||||
ApplicationContext.getInstance(context).clearAllData(false)
|
ApplicationContext.getInstance(context).clearAllData()
|
||||||
withContext(Main) { dismiss() }
|
withContext(Main) { dismiss() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,6 @@ import android.os.Bundle
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationSettingsActivity : PassphraseRequiredActionBarActivity() {
|
class NotificationSettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||||
|
@@ -1,141 +1,115 @@
|
|||||||
package org.thoughtcrime.securesms.preferences
|
package org.thoughtcrime.securesms.preferences
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import android.view.LayoutInflater
|
import androidx.compose.foundation.background
|
||||||
import android.view.View
|
import androidx.compose.foundation.layout.Column
|
||||||
import android.view.ViewGroup
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import android.widget.Toast
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.fragment.app.FragmentPagerAdapter
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityQrCodeBinding
|
|
||||||
import network.loki.messenger.databinding.FragmentViewMyQrCodeBinding
|
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.PublicKeyValidation
|
import org.session.libsignal.utilities.PublicKeyValidation
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.database.threadDatabase
|
||||||
import org.thoughtcrime.securesms.util.FileProviderUtil
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
import org.thoughtcrime.securesms.util.QRCodeUtilities
|
import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
import org.thoughtcrime.securesms.ui.components.QrImage
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||||
import java.io.File
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
import java.io.FileOutputStream
|
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||||
|
import org.thoughtcrime.securesms.ui.small
|
||||||
|
import org.thoughtcrime.securesms.util.start
|
||||||
|
|
||||||
class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
private val TITLES = listOf(R.string.view, R.string.scan)
|
||||||
private lateinit var binding: ActivityQrCodeBinding
|
|
||||||
private val adapter = QRCodeActivityAdapter(this)
|
class QRCodeActivity : PassphraseRequiredActionBarActivity() {
|
||||||
|
|
||||||
|
private val errors = MutableSharedFlow<String>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
|
||||||
// region Lifecycle
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||||
super.onCreate(savedInstanceState, isReady)
|
super.onCreate(savedInstanceState, isReady)
|
||||||
binding = ActivityQrCodeBinding.inflate(layoutInflater)
|
|
||||||
// Set content view
|
|
||||||
setContentView(binding.root)
|
|
||||||
// Set title
|
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title)
|
supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title)
|
||||||
// Set up view pager
|
|
||||||
binding.viewPager.adapter = adapter
|
|
||||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Interaction
|
setComposeContent {
|
||||||
override fun handleQRCodeScanned(hexEncodedPublicKey: String) {
|
Tabs(
|
||||||
createPrivateChatIfPossible(hexEncodedPublicKey)
|
TextSecurePreferences.getLocalNumber(this)!!,
|
||||||
|
errors.asSharedFlow(),
|
||||||
|
onScan = ::onScan
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createPrivateChatIfPossible(hexEncodedPublicKey: String) {
|
fun onScan(string: String) {
|
||||||
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) { return Toast.makeText(this, R.string.invalid_session_id, Toast.LENGTH_SHORT).show() }
|
if (!PublicKeyValidation.isValid(string)) {
|
||||||
val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false)
|
errors.tryEmit(getString(R.string.this_qr_code_does_not_contain_an_account_id))
|
||||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
} else if (!isFinishing) {
|
||||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
val recipient = Recipient.from(this, Address.fromSerialized(string), false)
|
||||||
intent.setDataAndType(getIntent().data, getIntent().type)
|
start<ConversationActivityV2> {
|
||||||
val existingThread = DatabaseComponent.get(this).threadDatabase().getThreadIdIfExistsFor(recipient)
|
putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||||
intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
|
setDataAndType(intent.data, intent.type)
|
||||||
startActivity(intent)
|
val existingThread = threadDatabase().getThreadIdIfExistsFor(recipient)
|
||||||
finish()
|
putExtra(ConversationActivityV2.THREAD_ID, existingThread)
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Adapter
|
|
||||||
private class QRCodeActivityAdapter(val activity: QRCodeActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(index: Int): Fragment {
|
|
||||||
return when (index) {
|
|
||||||
0 -> ViewMyQRCodeFragment()
|
|
||||||
1 -> {
|
|
||||||
val result = ScanQRCodeWrapperFragment()
|
|
||||||
result.delegate = activity
|
|
||||||
result.message = activity.resources.getString(R.string.activity_qr_code_view_scan_qr_code_explanation)
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
else -> throw IllegalStateException()
|
finish()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPageTitle(index: Int): CharSequence? {
|
|
||||||
return when (index) {
|
|
||||||
0 -> activity.resources.getString(R.string.activity_qr_code_view_my_qr_code_tab_title)
|
|
||||||
1 -> activity.resources.getString(R.string.activity_qr_code_view_scan_qr_code_tab_title)
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region View My QR Code Fragment
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
class ViewMyQRCodeFragment : Fragment() {
|
@Composable
|
||||||
private lateinit var binding: FragmentViewMyQrCodeBinding
|
private fun Tabs(accountId: String, errors: Flow<String>, onScan: (String) -> Unit) {
|
||||||
|
val pagerState = rememberPagerState { TITLES.size }
|
||||||
|
|
||||||
private val hexEncodedPublicKey: String
|
Column {
|
||||||
get() {
|
SessionTabRow(pagerState, TITLES)
|
||||||
return TextSecurePreferences.getLocalNumber(requireContext())!!
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) { page ->
|
||||||
|
when (TITLES[page]) {
|
||||||
|
R.string.view -> QrPage(accountId)
|
||||||
|
R.string.scan -> MaybeScanQrCode(errors, onScan = onScan)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
binding = FragmentViewMyQrCodeBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
val size = toPx(280, resources)
|
|
||||||
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
|
|
||||||
binding.qrCodeImageView.setImageBitmap(qrCode)
|
|
||||||
// val explanation = SpannableStringBuilder("This is your unique public QR code. Other users can scan this to start a conversation with you.")
|
|
||||||
// explanation.setSpan(StyleSpan(Typeface.BOLD), 8, 34, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
binding.explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation)
|
|
||||||
binding.shareButton.setOnClickListener { shareQRCode() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shareQRCode() {
|
|
||||||
val directory = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
|
||||||
val fileName = "$hexEncodedPublicKey.png"
|
|
||||||
val file = File(directory, fileName)
|
|
||||||
file.createNewFile()
|
|
||||||
val fos = FileOutputStream(file)
|
|
||||||
val size = toPx(280, resources)
|
|
||||||
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
|
|
||||||
qrCode.compress(Bitmap.CompressFormat.PNG, 100, fos)
|
|
||||||
fos.flush()
|
|
||||||
fos.close()
|
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, FileProviderUtil.getUriFor(requireActivity(), file))
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
intent.type = "image/png"
|
|
||||||
startActivity(Intent.createChooser(intent, resources.getString(R.string.fragment_view_my_qr_code_share_title)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
|
||||||
|
@Composable
|
||||||
|
fun QrPage(string: String) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(LocalColors.current.backgroundSecondary)
|
||||||
|
.padding(horizontal = LocalDimensions.current.margin)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
QrImage(
|
||||||
|
string = string,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = LocalDimensions.current.margin, bottom = LocalDimensions.current.xxsMargin)
|
||||||
|
.contentDescription(R.string.AccessibilityId_qr_code),
|
||||||
|
icon = R.drawable.session
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.this_is_your_account_id_other_users_can_scan_it_to_start_a_conversation_with_you),
|
||||||
|
color = LocalColors.current.textSecondary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = small
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -10,7 +10,6 @@ import androidx.recyclerview.widget.ListAdapter
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ItemSelectableBinding
|
import network.loki.messenger.databinding.ItemSelectableBinding
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.ui.GetString
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
@@ -68,7 +67,6 @@ class RadioOptionAdapter<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class RadioOption<out T>(
|
data class RadioOption<out T>(
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.preferences
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
|
||||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
|
||||||
import org.thoughtcrime.securesms.createSessionDialog
|
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
|
||||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
|
||||||
|
|
||||||
class SeedDialog: DialogFragment() {
|
|
||||||
private val seed by lazy {
|
|
||||||
val hexEncodedSeed = IdentityKeyUtil.retrieve(requireContext(), IdentityKeyUtil.LOKI_SEED)
|
|
||||||
?: IdentityKeyUtil.getIdentityKeyPair(requireContext()).hexEncodedPrivateKey // Legacy account
|
|
||||||
|
|
||||||
MnemonicCodec { fileName -> MnemonicUtilities.loadFileContents(requireContext(), fileName) }
|
|
||||||
.encode(hexEncodedSeed, MnemonicCodec.Language.Configuration.english)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
|
||||||
title(R.string.dialog_seed_title)
|
|
||||||
text(R.string.dialog_seed_explanation)
|
|
||||||
text(seed, R.style.SessionIDTextView)
|
|
||||||
button(R.string.copy, R.string.AccessibilityId_copy_recovery_phrase) { copySeed() }
|
|
||||||
button(R.string.close) { dismiss() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copySeed() {
|
|
||||||
val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clip = ClipData.newPlainText("Seed", seed)
|
|
||||||
clipboard.setPrimaryClip(clip)
|
|
||||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,15 +2,13 @@ package org.thoughtcrime.securesms.preferences
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ClipData
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.AsyncTask
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
import android.view.ActionMode
|
import android.view.ActionMode
|
||||||
@@ -20,26 +18,49 @@ import android.view.View
|
|||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.io.File
|
import kotlinx.coroutines.Dispatchers
|
||||||
import java.security.SecureRandom
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.BuildConfig
|
import network.loki.messenger.BuildConfig
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivitySettingsBinding
|
import network.loki.messenger.databinding.ActivitySettingsBinding
|
||||||
import network.loki.messenger.libsession_util.util.UserPic
|
import network.loki.messenger.libsession_util.util.UserPic
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.ui.alwaysUi
|
import nl.komponents.kovenant.ui.alwaysUi
|
||||||
import nl.komponents.kovenant.ui.failUi
|
import nl.komponents.kovenant.ui.failUi
|
||||||
import nl.komponents.kovenant.ui.successUi
|
import nl.komponents.kovenant.ui.successUi
|
||||||
import org.session.libsession.avatars.AvatarHelper
|
import org.session.libsession.avatars.AvatarHelper
|
||||||
import org.session.libsession.avatars.ProfileContactPhoto
|
import org.session.libsession.avatars.ProfileContactPhoto
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.*
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.ProfileKeyUtil
|
||||||
|
import org.session.libsession.utilities.ProfilePictureUtilities
|
||||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsession.utilities.truncateIdForDisplay
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||||
@@ -47,19 +68,30 @@ import org.thoughtcrime.securesms.components.ProfilePictureView
|
|||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.home.PathActivity
|
import org.thoughtcrime.securesms.home.PathActivity
|
||||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity
|
import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
||||||
|
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.Cell
|
||||||
|
import org.thoughtcrime.securesms.ui.Divider
|
||||||
|
import org.thoughtcrime.securesms.ui.LargeItemButton
|
||||||
|
import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.color.destructiveButtonColors
|
||||||
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.setThemedContent
|
||||||
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
import org.thoughtcrime.securesms.util.NetworkUtils
|
import org.thoughtcrime.securesms.util.NetworkUtils
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
|
import java.io.File
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||||
@@ -67,20 +99,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var configFactory: ConfigFactory
|
lateinit var configFactory: ConfigFactory
|
||||||
|
@Inject
|
||||||
|
lateinit var prefs: TextSecurePreferences
|
||||||
|
|
||||||
private lateinit var binding: ActivitySettingsBinding
|
private lateinit var binding: ActivitySettingsBinding
|
||||||
private var displayNameEditActionMode: ActionMode? = null
|
private var displayNameEditActionMode: ActionMode? = null
|
||||||
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
|
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
|
||||||
private lateinit var glide: GlideRequests
|
|
||||||
private var tempFile: File? = null
|
private var tempFile: File? = null
|
||||||
|
|
||||||
private val hexEncodedPublicKey: String
|
private val hexEncodedPublicKey: String get() = TextSecurePreferences.getLocalNumber(this)!!
|
||||||
get() {
|
|
||||||
return TextSecurePreferences.getLocalNumber(this)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val updatedProfileResultCode = 1234
|
|
||||||
private const val SCROLL_STATE = "SCROLL_STATE"
|
private const val SCROLL_STATE = "SCROLL_STATE"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,31 +118,24 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
super.onCreate(savedInstanceState, isReady)
|
super.onCreate(savedInstanceState, isReady)
|
||||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
val displayName = getDisplayName()
|
}
|
||||||
glide = GlideApp.with(this)
|
|
||||||
with(binding) {
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
|
||||||
|
binding.run {
|
||||||
setupProfilePictureView(profilePictureView)
|
setupProfilePictureView(profilePictureView)
|
||||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
||||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||||
btnGroupNameDisplay.text = displayName
|
btnGroupNameDisplay.text = getDisplayName()
|
||||||
publicKeyTextView.text = hexEncodedPublicKey
|
publicKeyTextView.text = hexEncodedPublicKey
|
||||||
copyButton.setOnClickListener { copyPublicKey() }
|
|
||||||
shareButton.setOnClickListener { sharePublicKey() }
|
|
||||||
pathButton.setOnClickListener { showPath() }
|
|
||||||
pathContainer.disableClipping()
|
|
||||||
privacyButton.setOnClickListener { showPrivacySettings() }
|
|
||||||
notificationsButton.setOnClickListener { showNotificationSettings() }
|
|
||||||
messageRequestsButton.setOnClickListener { showMessageRequests() }
|
|
||||||
chatsButton.setOnClickListener { showChatSettings() }
|
|
||||||
appearanceButton.setOnClickListener { showAppearanceSettings() }
|
|
||||||
inviteFriendButton.setOnClickListener { sendInvitation() }
|
|
||||||
helpButton.setOnClickListener { showHelp() }
|
|
||||||
seedButton.setOnClickListener { showSeed() }
|
|
||||||
clearAllDataButton.setOnClickListener { clearAllData() }
|
|
||||||
|
|
||||||
val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6)
|
val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6)
|
||||||
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars)")
|
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.composeView.setThemedContent {
|
||||||
|
Buttons()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisplayName(): String =
|
private fun getDisplayName(): String =
|
||||||
@@ -143,13 +165,16 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.settings_general, menu)
|
menuInflater.inflate(R.menu.settings_general, menu)
|
||||||
|
if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
menu.findItem(R.id.action_qr_code)?.contentDescription = resources.getString(R.string.AccessibilityId_view_qr_code)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_qr_code -> {
|
R.id.action_qr_code -> {
|
||||||
showQRCode()
|
push<QRCodeActivity>()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
@@ -159,30 +184,22 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
@Deprecated("Deprecated in Java")
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (resultCode != Activity.RESULT_OK) return
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
AvatarSelection.REQUEST_CODE_AVATAR -> {
|
AvatarSelection.REQUEST_CODE_AVATAR -> {
|
||||||
if (resultCode != Activity.RESULT_OK) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
|
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
|
||||||
var inputFile: Uri? = data?.data
|
val inputFile: Uri? = data?.data ?: tempFile?.let(Uri::fromFile)
|
||||||
if (inputFile == null && tempFile != null) {
|
|
||||||
inputFile = Uri.fromFile(tempFile)
|
|
||||||
}
|
|
||||||
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
|
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
|
||||||
}
|
}
|
||||||
AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
|
AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
|
||||||
if (resultCode != Activity.RESULT_OK) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
return
|
|
||||||
}
|
|
||||||
AsyncTask.execute {
|
|
||||||
try {
|
try {
|
||||||
val profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
|
val profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
|
||||||
Handler(Looper.getMainLooper()).post {
|
launch(Dispatchers.Main) {
|
||||||
updateProfilePicture(profilePictureToBeUploaded)
|
updateProfilePicture(profilePictureToBeUploaded)
|
||||||
}
|
}
|
||||||
} catch (e: BitmapDecodingException) {
|
} catch (e: BitmapDecodingException) {
|
||||||
e.printStackTrace()
|
Log.e(TAG, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,10 +214,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
private fun handleDisplayNameEditActionModeChanged() {
|
private fun handleDisplayNameEditActionModeChanged() {
|
||||||
val isEditingDisplayName = this.displayNameEditActionMode !== null
|
val isEditingDisplayName = this.displayNameEditActionMode != null
|
||||||
|
|
||||||
binding.btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
|
binding.btnGroupNameDisplay.isInvisible = isEditingDisplayName
|
||||||
binding.displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
|
binding.displayNameEditText.isInvisible = !isEditingDisplayName
|
||||||
|
|
||||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
if (isEditingDisplayName) {
|
if (isEditingDisplayName) {
|
||||||
@@ -277,11 +294,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
val userConfig = configFactory.user
|
val userConfig = configFactory.user
|
||||||
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
||||||
TextSecurePreferences.setProfileAvatarId(this, profilePicture.let { SecureRandom().nextInt() } )
|
prefs.setProfileAvatarId(SecureRandom().nextInt() )
|
||||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||||
|
|
||||||
// Attempt to grab the details we require to update the profile picture
|
// Attempt to grab the details we require to update the profile picture
|
||||||
val url = TextSecurePreferences.getProfilePictureURL(this)
|
val url = prefs.getProfilePictureURL()
|
||||||
val profileKey = ProfileKeyUtil.getProfileKey(this)
|
val profileKey = ProfileKeyUtil.getProfileKey(this)
|
||||||
|
|
||||||
// If we have a URL and a profile key then set the user's profile picture
|
// If we have a URL and a profile key then set the user's profile picture
|
||||||
@@ -354,40 +371,35 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
Toast.makeText(this, R.string.activity_settings_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.activity_settings_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (displayName.toByteArray().size > ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH) {
|
if (displayName.toByteArray().size > ProfileManagerProtocol.NAME_PADDED_LENGTH) {
|
||||||
Toast.makeText(this, R.string.activity_settings_display_name_too_long_error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.activity_settings_display_name_too_long_error, Toast.LENGTH_SHORT).show()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return updateDisplayName(displayName)
|
return updateDisplayName(displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showQRCode() {
|
|
||||||
val intent = Intent(this, QRCodeActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showEditProfilePictureUI() {
|
private fun showEditProfilePictureUI() {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.activity_settings_set_display_picture)
|
title(R.string.activity_settings_set_display_picture)
|
||||||
view(R.layout.dialog_change_avatar)
|
view(R.layout.dialog_change_avatar)
|
||||||
button(R.string.activity_settings_upload) { startAvatarSelection() }
|
button(R.string.activity_settings_upload) { startAvatarSelection() }
|
||||||
if (TextSecurePreferences.getProfileAvatarId(context) != 0) {
|
if (prefs.getProfileAvatarId() != 0) {
|
||||||
button(R.string.activity_settings_remove) { removeProfilePicture() }
|
button(R.string.activity_settings_remove) { removeProfilePicture() }
|
||||||
}
|
}
|
||||||
cancelButton()
|
cancelButton()
|
||||||
}.apply {
|
}.apply {
|
||||||
val profilePic = findViewById<ProfilePictureView>(R.id.profile_picture_view)
|
val profilePic = findViewById<ProfilePictureView>(R.id.profile_picture_view)
|
||||||
?.also(::setupProfilePictureView)
|
?.also(::setupProfilePictureView)
|
||||||
|
|
||||||
val pictureIcon = findViewById<View>(R.id.ic_pictures)
|
val pictureIcon = findViewById<View>(R.id.ic_pictures)
|
||||||
|
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false)
|
||||||
|
|
||||||
val photoSet = (recipient.contactPhoto as ProfileContactPhoto).avatarObject !in setOf("0", "")
|
val photoSet = (recipient.contactPhoto as ProfileContactPhoto).avatarObject !in setOf("0", "")
|
||||||
|
|
||||||
profilePic?.isVisible = photoSet
|
profilePic?.isVisible = photoSet
|
||||||
pictureIcon?.isVisible = !photoSet
|
pictureIcon?.isVisible = !photoSet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startAvatarSelection() {
|
private fun startAvatarSelection() {
|
||||||
@@ -399,76 +411,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyPublicKey() {
|
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clip = ClipData.newPlainText("Session ID", hexEncodedPublicKey)
|
|
||||||
clipboard.setPrimaryClip(clip)
|
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sharePublicKey() {
|
|
||||||
val intent = Intent()
|
|
||||||
intent.action = Intent.ACTION_SEND
|
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
|
|
||||||
intent.type = "text/plain"
|
|
||||||
val chooser = Intent.createChooser(intent, getString(R.string.share))
|
|
||||||
startActivity(chooser)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showPrivacySettings() {
|
|
||||||
val intent = Intent(this, PrivacySettingsActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNotificationSettings() {
|
|
||||||
val intent = Intent(this, NotificationSettingsActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showMessageRequests() {
|
|
||||||
val intent = Intent(this, MessageRequestsActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showChatSettings() {
|
|
||||||
val intent = Intent(this, ChatSettingsActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showAppearanceSettings() {
|
|
||||||
val intent = Intent(this, AppearanceSettingsActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendInvitation() {
|
|
||||||
val intent = Intent()
|
|
||||||
intent.action = Intent.ACTION_SEND
|
|
||||||
val invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is $hexEncodedPublicKey !"
|
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, invitation)
|
|
||||||
intent.type = "text/plain"
|
|
||||||
val chooser = Intent.createChooser(intent, getString(R.string.activity_settings_invite_button_title))
|
|
||||||
startActivity(chooser)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showHelp() {
|
|
||||||
val intent = Intent(this, HelpSettingsActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showPath() {
|
|
||||||
val intent = Intent(this, PathActivity::class.java)
|
|
||||||
show(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSeed() {
|
|
||||||
SeedDialog().show(supportFragmentManager, "Recovery Phrase Dialog")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearAllData() {
|
|
||||||
ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog")
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
private inner class DisplayNameEditActionModeCallback: ActionMode.Callback {
|
private inner class DisplayNameEditActionModeCallback: ActionMode.Callback {
|
||||||
@@ -497,7 +439,74 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Buttons() {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = LocalDimensions.current.smallMargin)
|
||||||
|
.padding(top = LocalDimensions.current.xxxsMargin),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallItemSpacing),
|
||||||
|
) {
|
||||||
|
PrimaryOutlineButton(
|
||||||
|
stringResource(R.string.share),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = ::sendInvitationToUseSession
|
||||||
|
)
|
||||||
|
|
||||||
|
PrimaryOutlineCopyButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = ::copyPublicKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(LocalDimensions.current.itemSpacing))
|
||||||
|
|
||||||
|
val hasPaths by hasPaths().collectAsState(initial = false)
|
||||||
|
|
||||||
|
Cell {
|
||||||
|
Column {
|
||||||
|
Crossfade(if (hasPaths) R.drawable.ic_status else R.drawable.ic_path_yellow, label = "path") {
|
||||||
|
LargeItemButtonWithDrawable(R.string.activity_path_title, it) { show<PathActivity>() }
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
LargeItemButton(R.string.activity_settings_privacy_button_title, R.drawable.ic_privacy_icon) { show<PrivacySettingsActivity>() }
|
||||||
|
Divider()
|
||||||
|
LargeItemButton(R.string.activity_settings_notifications_button_title, R.drawable.ic_speaker, Modifier.contentDescription(R.string.AccessibilityId_notifications)) { show<NotificationSettingsActivity>() }
|
||||||
|
Divider()
|
||||||
|
LargeItemButton(R.string.activity_settings_conversations_button_title, R.drawable.ic_conversations, Modifier.contentDescription(R.string.AccessibilityId_conversations)) { show<ChatSettingsActivity>() }
|
||||||
|
Divider()
|
||||||
|
LargeItemButton(R.string.activity_settings_message_requests_button_title, R.drawable.ic_message_requests, Modifier.contentDescription(R.string.AccessibilityId_message_requests)) { show<MessageRequestsActivity>() }
|
||||||
|
Divider()
|
||||||
|
LargeItemButton(R.string.activity_settings_message_appearance_button_title, R.drawable.ic_appearance, Modifier.contentDescription(R.string.AccessibilityId_appearance)) { show<AppearanceSettingsActivity>() }
|
||||||
|
Divider()
|
||||||
|
LargeItemButton(R.string.activity_settings_invite_button_title, R.drawable.ic_invite_friend, Modifier.contentDescription(R.string.AccessibilityId_invite_friend)) { sendInvitationToUseSession() }
|
||||||
|
Divider()
|
||||||
|
if (!prefs.getHidePassword()) {
|
||||||
|
LargeItemButton(R.string.sessionRecoveryPassword, R.drawable.ic_shield_outline, Modifier.contentDescription(R.string.AccessibilityId_recovery_password_menu_item)) { show<RecoveryPasswordActivity>() }
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
LargeItemButton(R.string.activity_settings_help_button, R.drawable.ic_help, Modifier.contentDescription(R.string.AccessibilityId_help)) { show<HelpSettingsActivity>() }
|
||||||
|
Divider()
|
||||||
|
LargeItemButton(R.string.activity_settings_clear_all_data_button_title, R.drawable.ic_clear_data, Modifier.contentDescription(R.string.AccessibilityId_clear_data), destructiveButtonColors()) { ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog") }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Context.hasPaths(): Flow<Boolean> = LocalBroadcastManager.getInstance(this).hasPaths()
|
||||||
|
private fun LocalBroadcastManager.hasPaths(): Flow<Boolean> = callbackFlow {
|
||||||
|
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) { trySend(Unit) }
|
||||||
|
}
|
||||||
|
|
||||||
|
registerReceiver(receiver, IntentFilter("buildingPaths"))
|
||||||
|
registerReceiver(receiver, IntentFilter("pathsBuilt"))
|
||||||
|
|
||||||
|
awaitClose { unregisterReceiver(receiver) }
|
||||||
|
}.onStart { emit(Unit) }.map { OnionRequestAPI.paths.isNotEmpty() }
|
||||||
|
@@ -0,0 +1,31 @@
|
|||||||
|
package org.thoughtcrime.securesms.preferences
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.widget.Toast
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
|
||||||
|
fun Context.sendInvitationToUseSession() {
|
||||||
|
Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
getString(
|
||||||
|
R.string.accountIdShare,
|
||||||
|
TextSecurePreferences.getLocalNumber(this@sendInvitationToUseSession)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
type = "text/plain"
|
||||||
|
}.let { Intent.createChooser(it, getString(R.string.activity_settings_invite_button_title)) }
|
||||||
|
.let(::startActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.copyPublicKey() {
|
||||||
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("Account ID", TextSecurePreferences.getLocalNumber(this))
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
@@ -5,7 +5,6 @@ import android.os.Parcelable
|
|||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.SwitchCompat
|
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -31,8 +30,8 @@ class AppearanceSettingsActivity: PassphraseRequiredActionBarActivity(), View.On
|
|||||||
|
|
||||||
var currentTheme: ThemeState? = null
|
var currentTheme: ThemeState? = null
|
||||||
|
|
||||||
private val accentColors
|
private val accentColors by lazy {
|
||||||
get() = mapOf(
|
mapOf(
|
||||||
binding.accentGreen to R.style.PrimaryGreen,
|
binding.accentGreen to R.style.PrimaryGreen,
|
||||||
binding.accentBlue to R.style.PrimaryBlue,
|
binding.accentBlue to R.style.PrimaryBlue,
|
||||||
binding.accentYellow to R.style.PrimaryYellow,
|
binding.accentYellow to R.style.PrimaryYellow,
|
||||||
@@ -41,9 +40,10 @@ class AppearanceSettingsActivity: PassphraseRequiredActionBarActivity(), View.On
|
|||||||
binding.accentOrange to R.style.PrimaryOrange,
|
binding.accentOrange to R.style.PrimaryOrange,
|
||||||
binding.accentRed to R.style.PrimaryRed
|
binding.accentRed to R.style.PrimaryRed
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val themeViews
|
private val themeViews by lazy {
|
||||||
get() = listOf(
|
listOf(
|
||||||
binding.themeOptionClassicDark,
|
binding.themeOptionClassicDark,
|
||||||
binding.themeRadioClassicDark,
|
binding.themeRadioClassicDark,
|
||||||
binding.themeOptionClassicLight,
|
binding.themeOptionClassicLight,
|
||||||
@@ -53,6 +53,7 @@ class AppearanceSettingsActivity: PassphraseRequiredActionBarActivity(), View.On
|
|||||||
binding.themeOptionOceanLight,
|
binding.themeOptionOceanLight,
|
||||||
binding.themeRadioOceanLight
|
binding.themeRadioOceanLight
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onClick(v: View?) {
|
override fun onClick(v: View?) {
|
||||||
v ?: return
|
v ?: return
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user