mirror of
https://github.com/oxen-io/session-android.git
synced 2025-01-11 18:43:49 +00:00
Merge remote-tracking branch 'upstream/dev'
# Conflicts: # app/build.gradle
This commit is contained in:
commit
07ccc2696b
@ -4,18 +4,17 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||||
classpath files('libs/gradle-witness.jar')
|
classpath files('libs/gradle-witness.jar')
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||||
classpath "com.google.gms:google-services:4.3.3"
|
classpath "com.google.gms:google-services:4.3.10"
|
||||||
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
|
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
apply plugin: 'witness'
|
apply plugin: 'witness'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
@ -32,16 +31,16 @@ dependencies {
|
|||||||
implementation 'com.google.android.material:material:1.2.1'
|
implementation 'com.google.android.material:material:1.2.1'
|
||||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
||||||
implementation 'androidx.activity:activity-ktx:1.2.2'
|
implementation 'androidx.activity:activity-ktx:1.2.2'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.3.2'
|
implementation 'androidx.fragment:fragment-ktx:1.3.2'
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
@ -62,9 +61,9 @@ dependencies {
|
|||||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
||||||
implementation 'commons-net:commons-net:3.7.2'
|
implementation 'commons-net:commons-net:3.7.2'
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
||||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||||
@ -72,8 +71,8 @@ dependencies {
|
|||||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||||
implementation "com.google.dagger:hilt-android:2.38.1"
|
implementation "com.google.dagger:hilt-android:$daggerVersion"
|
||||||
kapt "com.google.dagger:hilt-compiler:2.38.1"
|
kapt "com.google.dagger:hilt-compiler:$daggerVersion"
|
||||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||||
implementation 'com.google.zxing:core:3.2.1'
|
implementation 'com.google.zxing:core:3.2.1'
|
||||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||||
@ -103,7 +102,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
implementation project(":libsignal")
|
implementation project(":libsignal")
|
||||||
implementation project(":libsession")
|
implementation project(":libsession")
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
||||||
implementation "org.whispersystems:curve25519-java:$curve25519Version"
|
implementation "org.whispersystems:curve25519-java:$curve25519Version"
|
||||||
implementation 'com.goterl:lazysodium-android:5.0.2@aar'
|
implementation 'com.goterl:lazysodium-android:5.0.2@aar'
|
||||||
implementation "net.java.dev.jna:jna:5.8.0@aar"
|
implementation "net.java.dev.jna:jna:5.8.0@aar"
|
||||||
@ -111,7 +110,7 @@ dependencies {
|
|||||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||||
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
|
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
|
||||||
implementation "com.github.lelloman:android-identicons:v11"
|
implementation "com.github.lelloman:android-identicons:v11"
|
||||||
@ -122,12 +121,16 @@ dependencies {
|
|||||||
implementation "com.opencsv:opencsv:4.6"
|
implementation "com.opencsv:opencsv:4.6"
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||||
testImplementation 'org.mockito:mockito-core:1.10.8'
|
testImplementation "org.mockito:mockito-inline:4.0.0"
|
||||||
|
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
||||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
||||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||||
testImplementation 'androidx.test:core:1.3.0'
|
testImplementation 'androidx.test:core:1.3.0'
|
||||||
|
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||||
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||||
|
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||||
// Core library
|
// Core library
|
||||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
androidTestImplementation 'androidx.test:core:1.4.0'
|
||||||
|
|
||||||
@ -154,8 +157,8 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 246
|
def canonicalVersionCode = 249
|
||||||
def canonicalVersionName = "1.11.15"
|
def canonicalVersionName = "1.11.16"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
@ -231,6 +234,12 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
String sharedTestDir = 'src/sharedTest/java'
|
||||||
|
test.java.srcDirs += sharedTestDir
|
||||||
|
androidTest.java.srcDirs += sharedTestDir
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
@ -279,6 +288,7 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
dataBinding true
|
dataBinding true
|
||||||
|
viewBinding true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<manifest
|
<manifest
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="network.loki.messenger">
|
package="network.loki.messenger.test">
|
||||||
<application>
|
<application>
|
||||||
<uses-library android:name="android.test.runner"
|
<uses-library android:name="android.test.runner"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
@ -15,6 +15,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||||||
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
||||||
import network.loki.messenger.util.NewConversationButtonDrawableMatcher.Companion.newConversationButtonWithDrawable
|
import network.loki.messenger.util.NewConversationButtonDrawableMatcher.Companion.newConversationButtonWithDrawable
|
||||||
import org.hamcrest.Matchers.allOf
|
import org.hamcrest.Matchers.allOf
|
||||||
|
import org.hamcrest.Matchers.not
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@ -73,7 +74,7 @@ class HomeActivityTests {
|
|||||||
onView(allOf(withId(R.id.button), isDescendantOfA(withId(R.id.seedReminderView)))).perform(ViewActions.click())
|
onView(allOf(withId(R.id.button), isDescendantOfA(withId(R.id.seedReminderView)))).perform(ViewActions.click())
|
||||||
onView(withId(R.id.copyButton)).perform(ViewActions.click())
|
onView(withId(R.id.copyButton)).perform(ViewActions.click())
|
||||||
pressBack()
|
pressBack()
|
||||||
onView(withId(R.id.seedReminderView)).check(matches(withEffectiveVisibility(Visibility.GONE)))
|
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -85,7 +86,7 @@ class HomeActivityTests {
|
|||||||
@Test
|
@Test
|
||||||
fun testIsVisible_alreadyDismissed_seedView() {
|
fun testIsVisible_alreadyDismissed_seedView() {
|
||||||
setupLoggedInState(hasViewedSeed = true)
|
setupLoggedInState(hasViewedSeed = true)
|
||||||
onView(withId(R.id.seedReminderView)).check(doesNotExist())
|
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -27,6 +27,8 @@ import android.os.Build;
|
|||||||
import android.os.Build.VERSION;
|
import android.os.Build.VERSION;
|
||||||
import android.os.Build.VERSION_CODES;
|
import android.os.Build.VERSION_CODES;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.view.GestureDetector;
|
import android.view.GestureDetector;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@ -36,6 +38,8 @@ import android.view.MotionEvent;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@ -92,6 +96,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
|
|
||||||
private final static String TAG = MediaPreviewActivity.class.getSimpleName();
|
private final static String TAG = MediaPreviewActivity.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final int UI_ANIMATION_DELAY = 300;
|
||||||
|
|
||||||
public static final String ADDRESS_EXTRA = "address";
|
public static final String ADDRESS_EXTRA = "address";
|
||||||
public static final String DATE_EXTRA = "date";
|
public static final String DATE_EXTRA = "date";
|
||||||
public static final String SIZE_EXTRA = "size";
|
public static final String SIZE_EXTRA = "size";
|
||||||
@ -99,6 +105,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
public static final String OUTGOING_EXTRA = "outgoing";
|
public static final String OUTGOING_EXTRA = "outgoing";
|
||||||
public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent";
|
public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent";
|
||||||
|
|
||||||
|
private View rootContainer;
|
||||||
private ViewPager mediaPager;
|
private ViewPager mediaPager;
|
||||||
private View detailsContainer;
|
private View detailsContainer;
|
||||||
private TextView caption;
|
private TextView caption;
|
||||||
@ -118,6 +125,26 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
|
|
||||||
private int restartItem = -1;
|
private int restartItem = -1;
|
||||||
|
|
||||||
|
private boolean isFullscreen = false;
|
||||||
|
private final Handler hideHandler = new Handler(Looper.myLooper());
|
||||||
|
private final Runnable showRunnable = () -> {
|
||||||
|
getSupportActionBar().show();
|
||||||
|
};
|
||||||
|
private final Runnable hideRunnable = () -> {
|
||||||
|
if (VERSION.SDK_INT >= 30) {
|
||||||
|
rootContainer.getWindowInsetsController().hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
|
||||||
|
rootContainer.getWindowInsetsController().setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
} else {
|
||||||
|
rootContainer.setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_LOW_PROFILE |
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
|
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
|
||||||
Intent previewIntent = null;
|
Intent previewIntent = null;
|
||||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||||
@ -147,6 +174,32 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
initializeObservers();
|
initializeObservers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void toggleFullscreen() {
|
||||||
|
if (isFullscreen) {
|
||||||
|
exitFullscreen();
|
||||||
|
} else {
|
||||||
|
enterFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enterFullscreen() {
|
||||||
|
getSupportActionBar().hide();
|
||||||
|
isFullscreen = true;
|
||||||
|
hideHandler.removeCallbacks(showRunnable);
|
||||||
|
hideHandler.postDelayed(hideRunnable, UI_ANIMATION_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exitFullscreen() {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
rootContainer.getWindowInsetsController().show(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
|
||||||
|
} else {
|
||||||
|
rootContainer.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
|
||||||
|
}
|
||||||
|
isFullscreen = false;
|
||||||
|
hideHandler.removeCallbacks(hideRunnable);
|
||||||
|
hideHandler.postDelayed(showRunnable, UI_ANIMATION_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||||
clickDetector.onTouchEvent(ev);
|
clickDetector.onTouchEvent(ev);
|
||||||
@ -223,6 +276,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
|
rootContainer = findViewById(R.id.media_preview_root);
|
||||||
mediaPager = findViewById(R.id.media_pager);
|
mediaPager = findViewById(R.id.media_pager);
|
||||||
mediaPager.setOffscreenPageLimit(1);
|
mediaPager.setOffscreenPageLimit(1);
|
||||||
|
|
||||||
@ -295,12 +349,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
clickDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
|
clickDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onSingleTapUp(MotionEvent e) {
|
public boolean onSingleTapUp(MotionEvent e) {
|
||||||
if (e.getY() < detailsContainer.getTop()) {
|
if (e.getY() < detailsContainer.getTop()) {
|
||||||
detailsContainer.setVisibility(detailsContainer.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
|
detailsContainer.setVisibility(detailsContainer.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
toggleFullscreen();
|
||||||
return super.onSingleTapUp(e);
|
return super.onSingleTapUp(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -35,12 +35,10 @@ public class AudioCodec {
|
|||||||
|
|
||||||
public AudioCodec() throws IOException {
|
public AudioCodec() throws IOException {
|
||||||
this.bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
|
this.bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
|
||||||
this.audioRecord = createAudioRecord(this.bufferSize);
|
|
||||||
this.mediaCodec = createMediaCodec(this.bufferSize);
|
this.mediaCodec = createMediaCodec(this.bufferSize);
|
||||||
|
|
||||||
this.mediaCodec.start();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.audioRecord = createAudioRecord(this.bufferSize);
|
||||||
|
this.mediaCodec.start();
|
||||||
audioRecord.startRecording();
|
audioRecord.startRecording();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
@ -167,7 +165,7 @@ public class AudioCodec {
|
|||||||
return adtsHeader;
|
return adtsHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AudioRecord createAudioRecord(int bufferSize) {
|
private AudioRecord createAudioRecord(int bufferSize) throws SecurityException {
|
||||||
return new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE,
|
return new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE,
|
||||||
AudioFormat.CHANNEL_IN_MONO,
|
AudioFormat.CHANNEL_IN_MONO,
|
||||||
AudioFormat.ENCODING_PCM_16BIT, bufferSize * 10);
|
AudioFormat.ENCODING_PCM_16BIT, bufferSize * 10);
|
||||||
|
@ -7,13 +7,14 @@ import android.graphics.Path
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import kotlinx.android.synthetic.main.view_separator.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewSeparatorBinding
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import org.session.libsession.utilities.ThemeUtil
|
import org.session.libsession.utilities.ThemeUtil
|
||||||
|
|
||||||
class LabeledSeparatorView : RelativeLayout {
|
class LabeledSeparatorView : RelativeLayout {
|
||||||
|
|
||||||
|
private lateinit var binding: ViewSeparatorBinding
|
||||||
private val path = Path()
|
private val path = Path()
|
||||||
|
|
||||||
private val paint: Paint by lazy {
|
private val paint: Paint by lazy {
|
||||||
@ -43,10 +44,9 @@ class LabeledSeparatorView : RelativeLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpViewHierarchy() {
|
private fun setUpViewHierarchy() {
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context))
|
||||||
val contentView = inflater.inflate(R.layout.view_separator, null)
|
|
||||||
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||||
addView(contentView, layoutParams)
|
addView(binding.root, layoutParams)
|
||||||
setWillNotDraw(false)
|
setWillNotDraw(false)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
@ -59,9 +59,9 @@ class LabeledSeparatorView : RelativeLayout {
|
|||||||
val hMargin = toPx(16, resources).toFloat()
|
val hMargin = toPx(16, resources).toFloat()
|
||||||
path.reset()
|
path.reset()
|
||||||
path.moveTo(0.0f, h / 2)
|
path.moveTo(0.0f, h / 2)
|
||||||
path.lineTo(titleTextView.left - hMargin, h / 2)
|
path.lineTo(binding.titleTextView.left - hMargin, h / 2)
|
||||||
path.addRoundRect(titleTextView.left - hMargin, toPx(1, resources).toFloat(), titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
|
path.addRoundRect(binding.titleTextView.left - hMargin, toPx(1, resources).toFloat(), binding.titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
|
||||||
path.moveTo(titleTextView.right + hMargin, h / 2)
|
path.moveTo(binding.titleTextView.right + hMargin, h / 2)
|
||||||
path.lineTo(w, h / 2)
|
path.lineTo(w, h / 2)
|
||||||
path.close()
|
path.close()
|
||||||
c.drawPath(path, paint)
|
c.drawPath(path, paint)
|
||||||
|
@ -8,8 +8,8 @@ import android.widget.ImageView
|
|||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.DimenRes
|
import androidx.annotation.DimenRes
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import kotlinx.android.synthetic.main.view_profile_picture.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
||||||
import org.session.libsession.avatars.ProfileContactPhoto
|
import org.session.libsession.avatars.ProfileContactPhoto
|
||||||
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
|
||||||
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests
|
|||||||
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
|
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
|
||||||
|
|
||||||
class ProfilePictureView : RelativeLayout {
|
class ProfilePictureView : RelativeLayout {
|
||||||
|
private lateinit var binding: ViewProfilePictureBinding
|
||||||
lateinit var glide: GlideRequests
|
lateinit var glide: GlideRequests
|
||||||
var publicKey: String? = null
|
var publicKey: String? = null
|
||||||
var displayName: String? = null
|
var displayName: String? = null
|
||||||
@ -35,14 +36,12 @@ class ProfilePictureView : RelativeLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
val contentView = inflater.inflate(R.layout.view_profile_picture, null)
|
|
||||||
addView(contentView)
|
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun update(recipient: Recipient, threadID: Long) {
|
fun update(recipient: Recipient) {
|
||||||
fun getUserDisplayName(publicKey: String): String {
|
fun getUserDisplayName(publicKey: String): String {
|
||||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
||||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||||
@ -75,27 +74,27 @@ class ProfilePictureView : RelativeLayout {
|
|||||||
val publicKey = publicKey ?: return
|
val publicKey = publicKey ?: return
|
||||||
val additionalPublicKey = additionalPublicKey
|
val additionalPublicKey = additionalPublicKey
|
||||||
if (additionalPublicKey != null) {
|
if (additionalPublicKey != null) {
|
||||||
setProfilePictureIfNeeded(doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
|
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
|
||||||
setProfilePictureIfNeeded(doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
|
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
|
||||||
doubleModeImageViewContainer.visibility = View.VISIBLE
|
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
glide.clear(doubleModeImageView1)
|
glide.clear(binding.doubleModeImageView1)
|
||||||
glide.clear(doubleModeImageView2)
|
glide.clear(binding.doubleModeImageView2)
|
||||||
doubleModeImageViewContainer.visibility = View.INVISIBLE
|
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (additionalPublicKey == null && !isLarge) {
|
if (additionalPublicKey == null && !isLarge) {
|
||||||
setProfilePictureIfNeeded(singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size)
|
setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size)
|
||||||
singleModeImageView.visibility = View.VISIBLE
|
binding.singleModeImageView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
glide.clear(singleModeImageView)
|
glide.clear(binding.singleModeImageView)
|
||||||
singleModeImageView.visibility = View.INVISIBLE
|
binding.singleModeImageView.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (additionalPublicKey == null && isLarge) {
|
if (additionalPublicKey == null && isLarge) {
|
||||||
setProfilePictureIfNeeded(largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size)
|
setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size)
|
||||||
largeSingleModeImageView.visibility = View.VISIBLE
|
binding.largeSingleModeImageView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
glide.clear(largeSingleModeImageView)
|
glide.clear(binding.largeSingleModeImageView)
|
||||||
largeSingleModeImageView.visibility = View.INVISIBLE
|
binding.largeSingleModeImageView.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,25 +66,18 @@ public class ContactAccessor {
|
|||||||
public List<String> getNumbersForThreadSearchFilter(Context context, String constraint) {
|
public List<String> getNumbersForThreadSearchFilter(Context context, String constraint) {
|
||||||
LinkedList<String> numberList = new LinkedList<>();
|
LinkedList<String> numberList = new LinkedList<>();
|
||||||
|
|
||||||
GroupDatabase.Reader reader = null;
|
|
||||||
GroupRecord record;
|
GroupRecord record;
|
||||||
|
try (GroupDatabase.Reader reader = DatabaseComponent.get(context).groupDatabase().getGroupsFilteredByTitle(constraint)) {
|
||||||
try {
|
|
||||||
reader = DatabaseComponent.get(context).groupDatabase().getGroupsFilteredByTitle(constraint);
|
|
||||||
|
|
||||||
while ((record = reader.getNext()) != null) {
|
while ((record = reader.getNext()) != null) {
|
||||||
numberList.add(record.getEncodedId());
|
numberList.add(record.getEncodedId());
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
if (reader != null)
|
|
||||||
reader.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) &&
|
// if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) &&
|
||||||
!numberList.contains(TextSecurePreferences.getLocalNumber(context)))
|
// !numberList.contains(TextSecurePreferences.getLocalNumber(context)))
|
||||||
{
|
// {
|
||||||
numberList.add(TextSecurePreferences.getLocalNumber(context));
|
// numberList.add(TextSecurePreferences.getLocalNumber(context));
|
||||||
}
|
// }
|
||||||
|
|
||||||
return numberList;
|
return numberList;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
package org.thoughtcrime.securesms.contacts
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import kotlinx.android.synthetic.main.contact_selection_list_divider.view.*
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.databinding.ContactSelectionListDividerBinding
|
||||||
import org.thoughtcrime.securesms.contacts.UserView
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
lateinit var glide: GlideRequests
|
lateinit var glide: GlideRequests
|
||||||
@ -24,7 +21,15 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UserViewHolder(val view: UserView) : RecyclerView.ViewHolder(view)
|
class UserViewHolder(val view: UserView) : RecyclerView.ViewHolder(view)
|
||||||
class DividerViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
class DividerViewHolder(
|
||||||
|
private val binding: ContactSelectionListDividerBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
fun bind(item: ContactSelectionListItem.Header) {
|
||||||
|
with(binding){
|
||||||
|
label.text = item.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return items.size
|
return items.size
|
||||||
@ -41,8 +46,9 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
|
|||||||
return if (viewType == ViewType.Contact) {
|
return if (viewType == ViewType.Contact) {
|
||||||
UserViewHolder(UserView(context))
|
UserViewHolder(UserView(context))
|
||||||
} else {
|
} else {
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false)
|
DividerViewHolder(
|
||||||
DividerViewHolder(view)
|
ContactSelectionListDividerBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,8 +64,7 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
|
|||||||
if (multiSelect) UserView.ActionIndicator.Tick else UserView.ActionIndicator.None,
|
if (multiSelect) UserView.ActionIndicator.Tick else UserView.ActionIndicator.None,
|
||||||
isSelected)
|
isSelected)
|
||||||
} else if (viewHolder is DividerViewHolder) {
|
} else if (viewHolder is DividerViewHolder) {
|
||||||
item as ContactSelectionListItem.Header
|
viewHolder.bind(item as ContactSelectionListItem.Header)
|
||||||
viewHolder.view.label.text = item.name
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,21 @@
|
|||||||
package org.thoughtcrime.securesms.contacts
|
package org.thoughtcrime.securesms.contacts
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.loader.app.LoaderManager
|
|
||||||
import androidx.loader.content.Loader
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
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 kotlinx.android.synthetic.main.contact_selection_list_fragment.*
|
import androidx.fragment.app.Fragment
|
||||||
import network.loki.messenger.R
|
import androidx.loader.app.LoaderManager
|
||||||
|
import androidx.loader.content.Loader
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||||
|
import network.loki.messenger.databinding.ContactSelectionListFragmentBinding
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
|
||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem
|
|
||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListLoader
|
|
||||||
|
|
||||||
class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener {
|
class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener {
|
||||||
|
private lateinit var binding: ContactSelectionListFragmentBinding
|
||||||
private var cursorFilter: String? = null
|
private var cursorFilter: String? = null
|
||||||
var onContactSelectedListener: OnContactSelectedListener? = null
|
var onContactSelectedListener: OnContactSelectedListener? = null
|
||||||
|
|
||||||
@ -46,20 +44,21 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
|||||||
fun onContactDeselected(number: String?)
|
fun onContactDeselected(number: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
||||||
super.onActivityCreated(savedInstanceState)
|
|
||||||
recyclerView.layoutManager = LinearLayoutManager(activity)
|
|
||||||
recyclerView.adapter = listAdapter
|
|
||||||
swipeRefreshLayout.isEnabled = requireActivity().intent.getBooleanExtra(REFRESHABLE, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return inflater.inflate(R.layout.contact_selection_list_fragment, container, false)
|
binding = ContactSelectionListFragmentBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||||
|
binding.recyclerView.adapter = listAdapter
|
||||||
|
binding.swipeRefreshLayout.isEnabled = requireActivity().intent.getBooleanExtra(REFRESHABLE, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
@ -74,15 +73,15 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
|||||||
|
|
||||||
fun resetQueryFilter() {
|
fun resetQueryFilter() {
|
||||||
setQueryFilter(null)
|
setQueryFilter(null)
|
||||||
swipeRefreshLayout.isRefreshing = false
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setRefreshing(refreshing: Boolean) {
|
fun setRefreshing(refreshing: Boolean) {
|
||||||
swipeRefreshLayout.isRefreshing = refreshing
|
binding.swipeRefreshLayout.isRefreshing = refreshing
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnRefreshListener(onRefreshListener: OnRefreshListener?) {
|
fun setOnRefreshListener(onRefreshListener: OnRefreshListener?) {
|
||||||
swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
|
binding.swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> {
|
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> {
|
||||||
@ -107,8 +106,8 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
listAdapter.items = items
|
listAdapter.items = items
|
||||||
mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
|
binding.mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
|
||||||
emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
|
binding.emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContactClick(contact: Recipient) {
|
override fun onContactClick(contact: Recipient) {
|
||||||
|
@ -9,16 +9,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import kotlinx.android.synthetic.main.activity_create_closed_group.emptyStateContainer
|
|
||||||
import kotlinx.android.synthetic.main.activity_create_closed_group.mainContentContainer
|
|
||||||
import kotlinx.android.synthetic.main.activity_select_contacts.*
|
|
||||||
import kotlinx.android.synthetic.main.activity_select_contacts.recyclerView
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivitySelectContactsBinding
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
|
|
||||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
|
||||||
class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||||
|
private lateinit var binding: ActivitySelectContactsBinding
|
||||||
private var members = listOf<String>()
|
private var members = listOf<String>()
|
||||||
set(value) { field = value; selectContactsAdapter.members = value }
|
set(value) { field = value; selectContactsAdapter.members = value }
|
||||||
private lateinit var usersToExclude: Set<String>
|
private lateinit var usersToExclude: Set<String>
|
||||||
@ -36,18 +33,18 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
|||||||
// 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)
|
||||||
|
binding = ActivitySelectContactsBinding.inflate(layoutInflater)
|
||||||
setContentView(R.layout.activity_select_contacts)
|
setContentView(binding.root)
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
|
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
|
||||||
|
|
||||||
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
|
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
|
||||||
val emptyStateText = intent.getStringExtra(emptyStateTextKey)
|
val emptyStateText = intent.getStringExtra(emptyStateTextKey)
|
||||||
if (emptyStateText != null) {
|
if (emptyStateText != null) {
|
||||||
emptyStateMessageTextView.text = emptyStateText
|
binding.emptyStateMessageTextView.text = emptyStateText
|
||||||
}
|
}
|
||||||
|
|
||||||
recyclerView.adapter = selectContactsAdapter
|
binding.recyclerView.adapter = selectContactsAdapter
|
||||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
|
||||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||||
}
|
}
|
||||||
@ -73,8 +70,8 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
|||||||
|
|
||||||
private fun update(members: List<String>) {
|
private fun update(members: List<String>) {
|
||||||
this.members = members
|
this.members = members
|
||||||
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||||
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
@ -5,9 +5,8 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import kotlinx.android.synthetic.main.view_conversation.view.profilePictureView
|
|
||||||
import kotlinx.android.synthetic.main.view_user.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewUserBinding
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
|
||||||
@ -15,6 +14,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
|||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class UserView : LinearLayout {
|
class UserView : LinearLayout {
|
||||||
|
private lateinit var binding: ViewUserBinding
|
||||||
var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly
|
var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly
|
||||||
|
|
||||||
enum class ActionIndicator {
|
enum class ActionIndicator {
|
||||||
@ -41,9 +41,7 @@ class UserView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpViewHierarchy() {
|
private fun setUpViewHierarchy() {
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
binding = ViewUserBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
val contentView = inflater.inflate(R.layout.view_user, null)
|
|
||||||
addView(contentView)
|
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -56,28 +54,32 @@ class UserView : LinearLayout {
|
|||||||
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
|
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
|
||||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
|
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
|
||||||
val address = user.address.serialize()
|
val address = user.address.serialize()
|
||||||
profilePictureView.glide = glide
|
binding.profilePictureView.glide = glide
|
||||||
profilePictureView.update(user, threadID)
|
binding.profilePictureView.update(user)
|
||||||
actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||||
nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
||||||
when (actionIndicator) {
|
when (actionIndicator) {
|
||||||
ActionIndicator.None -> {
|
ActionIndicator.None -> {
|
||||||
actionIndicatorImageView.visibility = View.GONE
|
binding.actionIndicatorImageView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
ActionIndicator.Menu -> {
|
ActionIndicator.Menu -> {
|
||||||
actionIndicatorImageView.visibility = View.VISIBLE
|
binding.actionIndicatorImageView.visibility = View.VISIBLE
|
||||||
actionIndicatorImageView.setImageResource(R.drawable.ic_more_horiz_white)
|
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_more_horiz_white)
|
||||||
}
|
}
|
||||||
ActionIndicator.Tick -> {
|
ActionIndicator.Tick -> {
|
||||||
actionIndicatorImageView.visibility = View.VISIBLE
|
binding.actionIndicatorImageView.visibility = View.VISIBLE
|
||||||
actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle)
|
binding.actionIndicatorImageView.setImageResource(
|
||||||
|
if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleCheckbox(isSelected: Boolean = false) {
|
fun toggleCheckbox(isSelected: Boolean = false) {
|
||||||
actionIndicatorImageView.visibility = View.VISIBLE
|
binding.actionIndicatorImageView.visibility = View.VISIBLE
|
||||||
actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle)
|
binding.actionIndicatorImageView.setImageResource(
|
||||||
|
if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unbind() {
|
fun unbind() {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -4,9 +4,7 @@ import android.content.Context
|
|||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import kotlinx.android.synthetic.main.view_visible_message.view.*
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
|
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||||
@ -17,7 +15,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests
|
|||||||
|
|
||||||
class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
|
class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
|
||||||
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit,
|
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit,
|
||||||
private val glide: GlideRequests)
|
private val glide: GlideRequests, private val onDeselect: (MessageRecord, Int) -> Unit)
|
||||||
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
|
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
|
||||||
private val messageDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
private val messageDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
var selectedItems = mutableSetOf<MessageRecord>()
|
var selectedItems = mutableSetOf<MessageRecord>()
|
||||||
@ -49,15 +47,9 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
|||||||
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
@Suppress("NAME_SHADOWING")
|
@Suppress("NAME_SHADOWING")
|
||||||
val viewType = ViewType.allValues[viewType]
|
val viewType = ViewType.allValues[viewType]
|
||||||
when (viewType) {
|
return when (viewType) {
|
||||||
ViewType.Visible -> {
|
ViewType.Visible -> VisibleMessageViewHolder(VisibleMessageView(context))
|
||||||
val view = VisibleMessageView(context)
|
ViewType.Control -> ControlMessageViewHolder(ControlMessageView(context))
|
||||||
return VisibleMessageViewHolder(view)
|
|
||||||
}
|
|
||||||
ViewType.Control -> {
|
|
||||||
val view = ControlMessageView(context)
|
|
||||||
return ControlMessageViewHolder(view)
|
|
||||||
}
|
|
||||||
else -> throw IllegalStateException("Unexpected view type: $viewType.")
|
else -> throw IllegalStateException("Unexpected view type: $viewType.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,13 +63,16 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
|||||||
val view = viewHolder.view
|
val view = viewHolder.view
|
||||||
val isSelected = selectedItems.contains(message)
|
val isSelected = selectedItems.contains(message)
|
||||||
view.snIsSelected = isSelected
|
view.snIsSelected = isSelected
|
||||||
view.messageTimestampTextView.isVisible = isSelected
|
|
||||||
view.indexInAdapter = position
|
view.indexInAdapter = position
|
||||||
view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery)
|
view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery)
|
||||||
if (!message.isDeleted) {
|
if (!message.isDeleted) {
|
||||||
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
|
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
|
||||||
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||||
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
||||||
|
} else {
|
||||||
|
view.onPress = null
|
||||||
|
view.onSwipeToReply = null
|
||||||
|
view.onLongPress = null
|
||||||
}
|
}
|
||||||
view.contentViewDelegate = visibleMessageContentViewDelegate
|
view.contentViewDelegate = visibleMessageContentViewDelegate
|
||||||
}
|
}
|
||||||
@ -111,6 +106,27 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
|||||||
return messageDB.readerFor(cursor).current
|
return messageDB.readerFor(cursor).current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun changeCursor(cursor: Cursor?) {
|
||||||
|
super.changeCursor(cursor)
|
||||||
|
val toRemove = mutableSetOf<MessageRecord>()
|
||||||
|
val toDeselect = mutableSetOf<Pair<Int, MessageRecord>>()
|
||||||
|
for (selected in selectedItems) {
|
||||||
|
val position = getItemPositionForTimestamp(selected.timestamp)
|
||||||
|
if (position == null || position == -1) {
|
||||||
|
toRemove += selected
|
||||||
|
} else {
|
||||||
|
val item = getMessage(getCursorAtPositionOrThrow(position))
|
||||||
|
if (item == null || item.isDeleted) {
|
||||||
|
toDeselect += position to selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedItems -= toRemove
|
||||||
|
toDeselect.iterator().forEach { (pos, record) ->
|
||||||
|
onDeselect(record, pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleSelection(message: MessageRecord, position: Int) {
|
fun toggleSelection(message: MessageRecord, position: Int) {
|
||||||
if (selectedItems.contains(message)) selectedItems.remove(message) else selectedItems.add(message)
|
if (selectedItems.contains(message)) selectedItems.remove(message) else selectedItems.add(message)
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
|
@ -2,16 +2,12 @@ package org.thoughtcrime.securesms.conversation.v2
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.Log
|
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.VelocityTracker
|
import android.view.VelocityTracker
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class ConversationRecyclerView : RecyclerView {
|
class ConversationRecyclerView : RecyclerView {
|
||||||
private val maxLongPressVelocityY = toPx(10, resources)
|
private val maxLongPressVelocityY = toPx(10, resources)
|
||||||
@ -37,10 +33,10 @@ class ConversationRecyclerView : RecyclerView {
|
|||||||
if (abs(vy) > maxLongPressVelocityY && abs(vx) < minSwipeVelocityX) { return super.onInterceptTouchEvent(e) }
|
if (abs(vy) > maxLongPressVelocityY && abs(vx) < minSwipeVelocityX) { return super.onInterceptTouchEvent(e) }
|
||||||
// Return false if abs(v.x) > abs(v.y) so that only swipes that are more horizontal than vertical
|
// Return false if abs(v.x) > abs(v.y) so that only swipes that are more horizontal than vertical
|
||||||
// get passed on to the message view
|
// get passed on to the message view
|
||||||
if (abs(vx) > abs(vy)) {
|
return if (abs(vx) > abs(vy)) {
|
||||||
return false
|
false
|
||||||
} else {
|
} else {
|
||||||
return super.onInterceptTouchEvent(e)
|
super.onInterceptTouchEvent(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,130 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class ConversationViewModel(
|
||||||
|
val threadId: Long,
|
||||||
|
private val repository: ConversationRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(ConversationUiState())
|
||||||
|
val uiState: StateFlow<ConversationUiState> = _uiState
|
||||||
|
|
||||||
|
val recipient: Recipient by lazy {
|
||||||
|
repository.getRecipientForThreadId(threadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(isOxenHostedOpenGroup = repository.isOxenHostedOpenGroup(threadId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDraft(text: String) {
|
||||||
|
repository.saveDraft(threadId, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDraft(): String? {
|
||||||
|
return repository.getDraft(threadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inviteContacts(contacts: List<Recipient>) {
|
||||||
|
repository.inviteContacts(threadId, contacts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unblock() {
|
||||||
|
if (recipient.isContactRecipient) {
|
||||||
|
repository.unblock(recipient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteLocally(message: MessageRecord) {
|
||||||
|
repository.deleteLocally(recipient, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
|
||||||
|
repository.deleteForEveryone(threadId, recipient, message)
|
||||||
|
.onFailure {
|
||||||
|
showMessage("Couldn't delete message due to error: $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteMessagesWithoutUnsendRequest(messages: Set<MessageRecord>) = viewModelScope.launch {
|
||||||
|
repository.deleteMessageWithoutUnsendRequest(threadId, messages)
|
||||||
|
.onFailure {
|
||||||
|
showMessage("Couldn't delete message due to error: $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun banUser(recipient: Recipient) = viewModelScope.launch {
|
||||||
|
repository.banUser(threadId, recipient)
|
||||||
|
.onSuccess {
|
||||||
|
showMessage("Successfully banned user")
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
showMessage("Couldn't ban user due to error: $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch {
|
||||||
|
repository.banAndDeleteAll(threadId, recipient)
|
||||||
|
.onSuccess {
|
||||||
|
showMessage("Successfully banned user and deleted all their messages")
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
showMessage("Couldn't execute request due to error: $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showMessage(message: String) {
|
||||||
|
_uiState.update { currentUiState ->
|
||||||
|
val messages = currentUiState.uiMessages + UiMessage(
|
||||||
|
id = UUID.randomUUID().mostSignificantBits,
|
||||||
|
message = message
|
||||||
|
)
|
||||||
|
currentUiState.copy(uiMessages = messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun messageShown(messageId: Long) {
|
||||||
|
_uiState.update { currentUiState ->
|
||||||
|
val messages = currentUiState.uiMessages.filterNot { it.id == messageId }
|
||||||
|
currentUiState.copy(uiMessages = messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@dagger.assisted.AssistedFactory
|
||||||
|
interface AssistedFactory {
|
||||||
|
fun create(threadId: Long): Factory
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
class Factory @AssistedInject constructor(
|
||||||
|
@Assisted private val threadId: Long,
|
||||||
|
private val repository: ConversationRepository
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return ConversationViewModel(threadId, repository) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UiMessage(val id: Long, val message: String)
|
||||||
|
|
||||||
|
data class ConversationUiState(
|
||||||
|
val isOxenHostedOpenGroup: Boolean = false,
|
||||||
|
val uiMessages: List<UiMessage> = emptyList()
|
||||||
|
)
|
@ -7,8 +7,8 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.android.synthetic.main.fragment_delete_message_bottom_sheet.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.FragmentDeleteMessageBottomSheetBinding
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||||
@ -22,6 +22,7 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
|||||||
lateinit var contactDatabase: SessionContactDatabase
|
lateinit var contactDatabase: SessionContactDatabase
|
||||||
|
|
||||||
lateinit var recipient: Recipient
|
lateinit var recipient: Recipient
|
||||||
|
private lateinit var binding: FragmentDeleteMessageBottomSheetBinding
|
||||||
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
|
||||||
@ -37,15 +38,16 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
|||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
return inflater.inflate(R.layout.fragment_delete_message_bottom_sheet, container, false)
|
binding = FragmentDeleteMessageBottomSheetBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View?) {
|
override fun onClick(v: View?) {
|
||||||
when (v) {
|
when (v) {
|
||||||
deleteForMeTextView -> onDeleteForMeTapped?.invoke()
|
binding.deleteForMeTextView -> onDeleteForMeTapped?.invoke()
|
||||||
deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke()
|
binding.deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke()
|
||||||
cancelTextView -> onCancelTapped?.invoke()
|
binding.cancelTextView -> onCancelTapped?.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,13 +57,13 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
|||||||
return dismiss()
|
return dismiss()
|
||||||
}
|
}
|
||||||
if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) {
|
if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) {
|
||||||
deleteForEveryoneTextView.text =
|
binding.deleteForEveryoneTextView.text =
|
||||||
resources.getString(R.string.delete_message_for_me_and_recipient, contact)
|
resources.getString(R.string.delete_message_for_me_and_recipient, contact)
|
||||||
}
|
}
|
||||||
deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
|
binding.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
|
||||||
deleteForMeTextView.setOnClickListener(this)
|
binding.deleteForMeTextView.setOnClickListener(this)
|
||||||
deleteForEveryoneTextView.setOnClickListener(this)
|
binding.deleteForEveryoneTextView.setOnClickListener(this)
|
||||||
cancelTextView.setOnClickListener(this)
|
binding.cancelTextView.setOnClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.conversation.v2
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import kotlinx.android.synthetic.main.activity_message_detail.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivityMessageDetailBinding
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.ExpirationUtil
|
import org.session.libsession.utilities.ExpirationUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
@ -13,11 +13,11 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
|||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
||||||
|
private lateinit var binding: ActivityMessageDetailBinding
|
||||||
var messageRecord: MessageRecord? = null
|
var messageRecord: MessageRecord? = null
|
||||||
|
|
||||||
// region Settings
|
// region Settings
|
||||||
@ -29,7 +29,8 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
super.onCreate(savedInstanceState, ready)
|
super.onCreate(savedInstanceState, ready)
|
||||||
setContentView(R.layout.activity_message_detail)
|
binding = ActivityMessageDetailBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
title = resources.getString(R.string.conversation_context__menu_message_details)
|
title = resources.getString(R.string.conversation_context__menu_message_details)
|
||||||
val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
||||||
// We only show this screen for messages fail to send,
|
// We only show this screen for messages fail to send,
|
||||||
@ -37,7 +38,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
|
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
|
||||||
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author)
|
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author)
|
||||||
updateContent()
|
updateContent()
|
||||||
resend_button.setOnClickListener {
|
binding.resendButton.setOnClickListener {
|
||||||
ResendMessageUtilities.resend(messageRecord!!)
|
ResendMessageUtilities.resend(messageRecord!!)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
@ -46,20 +47,20 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
fun updateContent() {
|
fun updateContent() {
|
||||||
val dateLocale = Locale.getDefault()
|
val dateLocale = Locale.getDefault()
|
||||||
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
|
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
|
||||||
sent_time.text = dateFormatter.format(Date(messageRecord!!.dateSent))
|
binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
|
||||||
|
|
||||||
val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) ?: "Message failed to send."
|
val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) ?: "Message failed to send."
|
||||||
error_message.text = errorMessage
|
binding.errorMessage.text = errorMessage
|
||||||
|
|
||||||
if (messageRecord!!.getExpiresIn() <= 0 || messageRecord!!.getExpireStarted() <= 0) {
|
if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
|
||||||
expires_container.visibility = View.GONE
|
binding.expiresContainer.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
expires_container.visibility = View.VISIBLE
|
binding.expiresContainer.visibility = View.VISIBLE
|
||||||
val elapsed = System.currentTimeMillis() - messageRecord!!.expireStarted
|
val elapsed = System.currentTimeMillis() - messageRecord!!.expireStarted
|
||||||
val remaining = messageRecord!!.expiresIn - elapsed
|
val remaining = messageRecord!!.expiresIn - elapsed
|
||||||
|
|
||||||
val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
|
val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
|
||||||
expires_in.text = duration
|
binding.expiresIn.text = duration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,14 +15,16 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import kotlinx.android.synthetic.main.fragment_modal_url_bottom_sheet.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
|
|
||||||
class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener {
|
class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener {
|
||||||
|
private lateinit var binding: FragmentModalUrlBottomSheetBinding
|
||||||
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_modal_url_bottom_sheet, container, false)
|
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentModalUrlBottomSheetBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@ -31,10 +33,10 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
|||||||
val spannable = SpannableStringBuilder(explanation)
|
val spannable = SpannableStringBuilder(explanation)
|
||||||
val startIndex = explanation.indexOf(url)
|
val startIndex = explanation.indexOf(url)
|
||||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
openURLExplanationTextView.text = spannable
|
binding.openURLExplanationTextView.text = spannable
|
||||||
cancelButton.setOnClickListener(this)
|
binding.cancelButton.setOnClickListener(this)
|
||||||
copyButton.setOnClickListener(this)
|
binding.copyButton.setOnClickListener(this)
|
||||||
openURLButton.setOnClickListener(this)
|
binding.openURLButton.setOnClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun open() {
|
private fun open() {
|
||||||
@ -64,9 +66,9 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
|||||||
|
|
||||||
override fun onClick(v: View?) {
|
override fun onClick(v: View?) {
|
||||||
when (v) {
|
when (v) {
|
||||||
openURLButton -> open()
|
binding.openURLButton -> open()
|
||||||
copyButton -> copy()
|
binding.copyButton -> copy()
|
||||||
cancelButton -> dismiss()
|
binding.cancelButton -> dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,28 +11,25 @@ import android.widget.FrameLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.album_thumbnail_view.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
|
||||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
import org.session.libsession.utilities.ViewUtil
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.MediaPreviewActivity
|
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||||
import org.thoughtcrime.securesms.components.CornerMask
|
import org.thoughtcrime.securesms.components.CornerMask
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.longmessage.LongMessageActivity
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.mms.Slide
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class AlbumThumbnailView : FrameLayout {
|
class AlbumThumbnailView : FrameLayout {
|
||||||
|
|
||||||
|
private lateinit var binding: AlbumThumbnailViewBinding
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAX_ALBUM_DISPLAY_SIZE = 5
|
const val MAX_ALBUM_DISPLAY_SIZE = 5
|
||||||
}
|
}
|
||||||
@ -55,7 +52,7 @@ class AlbumThumbnailView : FrameLayout {
|
|||||||
private var slideSize: Int = 0
|
private var slideSize: Int = 0
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this)
|
binding = AlbumThumbnailViewBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispatchDraw(canvas: Canvas?) {
|
override fun dispatchDraw(canvas: Canvas?) {
|
||||||
@ -70,26 +67,9 @@ class AlbumThumbnailView : FrameLayout {
|
|||||||
val rawXInt = event.rawX.toInt()
|
val rawXInt = event.rawX.toInt()
|
||||||
val rawYInt = event.rawY.toInt()
|
val rawYInt = event.rawY.toInt()
|
||||||
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||||
// Z-check in specific order
|
|
||||||
val testRect = Rect()
|
val testRect = Rect()
|
||||||
// test "Read More"
|
|
||||||
albumCellBodyTextReadMore.getGlobalVisibleRect(testRect)
|
|
||||||
if (testRect.contains(eventRect)) {
|
|
||||||
// dispatch to activity view
|
|
||||||
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
|
||||||
LongMessageActivity.getIntent(context, mms.recipient.address, mms.getId(), true)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val intersectedSpans = albumCellBodyText.getIntersectedModalSpans(eventRect)
|
|
||||||
if (intersectedSpans.isNotEmpty()) {
|
|
||||||
intersectedSpans.forEach { span ->
|
|
||||||
span.onClick(albumCellBodyText)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// test each album child
|
// test each album child
|
||||||
albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
|
binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
|
||||||
child.getGlobalVisibleRect(testRect)
|
child.getGlobalVisibleRect(testRect)
|
||||||
if (testRect.contains(eventRect)) {
|
if (testRect.contains(eventRect)) {
|
||||||
// hit intersects with this particular child
|
// hit intersects with this particular child
|
||||||
@ -111,6 +91,11 @@ class AlbumThumbnailView : FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearViews() {
|
||||||
|
binding.albumCellContainer.removeAllViews()
|
||||||
|
slideSize = -1
|
||||||
|
}
|
||||||
|
|
||||||
fun bind(glideRequests: GlideRequests, message: MmsMessageRecord,
|
fun bind(glideRequests: GlideRequests, message: MmsMessageRecord,
|
||||||
isStart: Boolean, isEnd: Boolean) {
|
isStart: Boolean, isEnd: Boolean) {
|
||||||
slides = message.slideDeck.thumbnailSlides
|
slides = message.slideDeck.thumbnailSlides
|
||||||
@ -122,10 +107,10 @@ class AlbumThumbnailView : FrameLayout {
|
|||||||
|
|
||||||
// recreate cell views if different size to what we have already (for recycling)
|
// recreate cell views if different size to what we have already (for recycling)
|
||||||
if (slides.size != this.slideSize) {
|
if (slides.size != this.slideSize) {
|
||||||
albumCellContainer.removeAllViews()
|
binding.albumCellContainer.removeAllViews()
|
||||||
LayoutInflater.from(context).inflate(layoutRes(slides.size), albumCellContainer)
|
LayoutInflater.from(context).inflate(layoutRes(slides.size), binding.albumCellContainer)
|
||||||
val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE
|
val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE
|
||||||
albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
|
binding.albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
|
||||||
// overflowText will be null if !overflowed
|
// overflowText will be null if !overflowed
|
||||||
overflowText.isVisible = overflowed // more than max album size
|
overflowText.isVisible = overflowed // more than max album size
|
||||||
overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE)
|
overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE)
|
||||||
@ -137,19 +122,6 @@ class AlbumThumbnailView : FrameLayout {
|
|||||||
val thumbnailView = getThumbnailView(position)
|
val thumbnailView = getThumbnailView(position)
|
||||||
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
|
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
|
||||||
}
|
}
|
||||||
albumCellBodyParent.isVisible = message.body.isNotEmpty()
|
|
||||||
val body = VisibleMessageContentView.getBodySpans(context, message, null)
|
|
||||||
albumCellBodyText.text = body
|
|
||||||
post {
|
|
||||||
// post to await layout of text
|
|
||||||
albumCellBodyText.layout?.let { layout ->
|
|
||||||
val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) }
|
|
||||||
?: 0
|
|
||||||
// show read more text if at least one line is ellipsized
|
|
||||||
ViewUtil.setPaddingTop(albumCellBodyTextParent, if (maxEllipsis > 0) resources.getDimension(R.dimen.small_spacing).roundToInt() else resources.getDimension(R.dimen.medium_spacing).roundToInt())
|
|
||||||
albumCellBodyTextReadMore.isVisible = maxEllipsis > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
@ -165,11 +137,11 @@ class AlbumThumbnailView : FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getThumbnailView(position: Int): KThumbnailView = when (position) {
|
fun getThumbnailView(position: Int): KThumbnailView = when (position) {
|
||||||
0 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
|
0 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
|
||||||
1 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
|
1 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
|
||||||
2 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
|
2 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
|
||||||
3 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
|
3 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
|
||||||
4 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
|
4 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
|
||||||
else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position")
|
else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,14 +5,14 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.view_link_preview_draft.view.*
|
import network.loki.messenger.databinding.ViewLinkPreviewDraftBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||||
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
|
|
||||||
class LinkPreviewDraftView : LinearLayout {
|
class LinkPreviewDraftView : LinearLayout {
|
||||||
|
private lateinit var binding: ViewLinkPreviewDraftBinding
|
||||||
var delegate: LinkPreviewDraftViewDelegate? = null
|
var delegate: LinkPreviewDraftViewDelegate? = null
|
||||||
|
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
@ -21,22 +21,22 @@ class LinkPreviewDraftView : LinearLayout {
|
|||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
// Start out with the loader showing and the content view hidden
|
// Start out with the loader showing and the content view hidden
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_link_preview_draft, this)
|
binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
linkPreviewDraftContainer.isVisible = false
|
binding.linkPreviewDraftContainer.isVisible = false
|
||||||
thumbnailImageView.clipToOutline = true
|
binding.thumbnailImageView.clipToOutline = true
|
||||||
linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(glide: GlideRequests, linkPreview: LinkPreview) {
|
fun update(glide: GlideRequests, linkPreview: LinkPreview) {
|
||||||
// Hide the loader and show the content view
|
// Hide the loader and show the content view
|
||||||
linkPreviewDraftContainer.isVisible = true
|
binding.linkPreviewDraftContainer.isVisible = true
|
||||||
linkPreviewDraftLoader.isVisible = false
|
binding.linkPreviewDraftLoader.isVisible = false
|
||||||
thumbnailImageView.radius = toPx(4, resources)
|
binding.thumbnailImageView.radius = toPx(4, resources)
|
||||||
if (linkPreview.getThumbnail().isPresent) {
|
if (linkPreview.getThumbnail().isPresent) {
|
||||||
// This internally fetches the thumbnail
|
// This internally fetches the thumbnail
|
||||||
thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false)
|
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false)
|
||||||
}
|
}
|
||||||
linkPreviewDraftTitleTextView.text = linkPreview.title
|
binding.linkPreviewDraftTitleTextView.text = linkPreview.title
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancel() {
|
private fun cancel() {
|
||||||
|
@ -45,7 +45,7 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
|
||||||
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent)
|
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context)
|
||||||
val mentionCandidate = getItem(position)
|
val mentionCandidate = getItem(position)
|
||||||
cell.glide = glide
|
cell.glide = glide
|
||||||
cell.mentionCandidate = mentionCandidate
|
cell.mentionCandidate = mentionCandidate
|
||||||
|
@ -4,32 +4,29 @@ import android.content.Context
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
import network.loki.messenger.databinding.ViewMentionCandidateBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.session.libsession.messaging.mentions.Mention
|
import org.session.libsession.messaging.mentions.Mention
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
class MentionCandidateView : LinearLayout {
|
||||||
|
private lateinit var binding: ViewMentionCandidateBinding
|
||||||
var mentionCandidate = Mention("", "")
|
var mentionCandidate = Mention("", "")
|
||||||
set(newValue) { field = newValue; update() }
|
set(newValue) { field = newValue; update() }
|
||||||
var glide: GlideRequests? = null
|
var glide: GlideRequests? = null
|
||||||
var openGroupServer: String? = null
|
var openGroupServer: String? = null
|
||||||
var openGroupRoom: String? = null
|
var openGroupRoom: String? = null
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
|
||||||
constructor(context: Context) : this(context, null)
|
constructor(context: Context) : this(context, null)
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||||
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
companion object {
|
private fun initialize() {
|
||||||
|
binding = ViewMentionCandidateBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView {
|
|
||||||
return layoutInflater.inflate(R.layout.view_mention_candidate, parent, false) as MentionCandidateView
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun update() {
|
private fun update() = with(binding) {
|
||||||
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
||||||
profilePictureView.publicKey = mentionCandidate.publicKey
|
profilePictureView.publicKey = mentionCandidate.publicKey
|
||||||
profilePictureView.displayName = mentionCandidate.displayName
|
profilePictureView.displayName = mentionCandidate.displayName
|
||||||
|
@ -5,8 +5,7 @@ import android.content.Intent
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import kotlinx.android.synthetic.main.view_open_group_guidelines.view.*
|
import network.loki.messenger.databinding.ViewOpenGroupGuidelinesBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity
|
import org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
@ -18,13 +17,12 @@ class OpenGroupGuidelinesView : FrameLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
ViewOpenGroupGuidelinesBinding.inflate(LayoutInflater.from(context), this, true).apply {
|
||||||
val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null)
|
readButton.setOnClickListener {
|
||||||
addView(contentView)
|
val activity = context as ConversationActivityV2
|
||||||
readButton.setOnClickListener {
|
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
|
||||||
val activity = context as ConversationActivityV2
|
activity.push(intent)
|
||||||
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
|
}
|
||||||
activity.push(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,22 +4,22 @@ import android.content.Context
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import kotlinx.android.synthetic.main.view_conversation_typing_container.view.*
|
import network.loki.messenger.databinding.ViewConversationTypingContainerBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
|
||||||
class TypingIndicatorViewContainer : LinearLayout {
|
class TypingIndicatorViewContainer : LinearLayout {
|
||||||
|
private lateinit var binding: ViewConversationTypingContainerBinding
|
||||||
|
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_conversation_typing_container, this)
|
binding = ViewConversationTypingContainerBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTypists(typists: List<Recipient>) {
|
fun setTypists(typists: List<Recipient>) {
|
||||||
if (typists.isEmpty()) { typingIndicator.stopAnimation(); return }
|
if (typists.isEmpty()) { binding.typingIndicator.stopAnimation(); return }
|
||||||
typingIndicator.startAnimation()
|
binding.typingIndicator.startAnimation()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,8 +6,8 @@ import android.text.SpannableStringBuilder
|
|||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import kotlinx.android.synthetic.main.dialog_blocked.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.DialogBlockedBinding
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||||
@ -17,21 +17,21 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
|||||||
class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
|
class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
|
||||||
|
|
||||||
override fun setContentView(builder: AlertDialog.Builder) {
|
override fun setContentView(builder: AlertDialog.Builder) {
|
||||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_blocked, null)
|
val binding = DialogBlockedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||||
val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
|
val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
|
||||||
val sessionID = recipient.address.toString()
|
val sessionID = recipient.address.toString()
|
||||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
val contact = contactDB.getContactWithSessionID(sessionID)
|
||||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||||
val title = resources.getString(R.string.dialog_blocked_title, name)
|
val title = resources.getString(R.string.dialog_blocked_title, name)
|
||||||
contentView.blockedTitleTextView.text = title
|
binding.blockedTitleTextView.text = title
|
||||||
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)
|
||||||
val startIndex = explanation.indexOf(name)
|
val startIndex = explanation.indexOf(name)
|
||||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
contentView.blockedExplanationTextView.text = spannable
|
binding.blockedExplanationTextView.text = spannable
|
||||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
binding.cancelButton.setOnClickListener { dismiss() }
|
||||||
contentView.unblockButton.setOnClickListener { unblock() }
|
binding.unblockButton.setOnClickListener { unblock() }
|
||||||
builder.setView(contentView)
|
builder.setView(binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unblock() {
|
private fun unblock() {
|
||||||
|
@ -7,8 +7,8 @@ import android.text.style.StyleSpan
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.android.synthetic.main.dialog_download.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.DialogDownloadBinding
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
@ -26,20 +26,20 @@ class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
|
|||||||
@Inject lateinit var contactDB: SessionContactDatabase
|
@Inject lateinit var contactDB: SessionContactDatabase
|
||||||
|
|
||||||
override fun setContentView(builder: AlertDialog.Builder) {
|
override fun setContentView(builder: AlertDialog.Builder) {
|
||||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_download, null)
|
val binding = DialogDownloadBinding.inflate(LayoutInflater.from(requireContext()))
|
||||||
val sessionID = recipient.address.toString()
|
val sessionID = recipient.address.toString()
|
||||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
val contact = contactDB.getContactWithSessionID(sessionID)
|
||||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||||
val title = resources.getString(R.string.dialog_download_title, name)
|
val title = resources.getString(R.string.dialog_download_title, name)
|
||||||
contentView.downloadTitleTextView.text = title
|
binding.downloadTitleTextView.text = title
|
||||||
val explanation = resources.getString(R.string.dialog_download_explanation, name)
|
val explanation = resources.getString(R.string.dialog_download_explanation, name)
|
||||||
val spannable = SpannableStringBuilder(explanation)
|
val spannable = SpannableStringBuilder(explanation)
|
||||||
val startIndex = explanation.indexOf(name)
|
val startIndex = explanation.indexOf(name)
|
||||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
contentView.downloadExplanationTextView.text = spannable
|
binding.downloadExplanationTextView.text = spannable
|
||||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
binding.cancelButton.setOnClickListener { dismiss() }
|
||||||
contentView.downloadButton.setOnClickListener { trust() }
|
binding.downloadButton.setOnClickListener { trust() }
|
||||||
builder.setView(contentView)
|
builder.setView(binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trust() {
|
private fun trust() {
|
||||||
|
@ -7,8 +7,8 @@ import android.text.style.StyleSpan
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.DialogJoinOpenGroupBinding
|
||||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||||
@ -19,17 +19,17 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
|||||||
class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() {
|
class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() {
|
||||||
|
|
||||||
override fun setContentView(builder: AlertDialog.Builder) {
|
override fun setContentView(builder: AlertDialog.Builder) {
|
||||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_join_open_group, null)
|
val binding = DialogJoinOpenGroupBinding.inflate(LayoutInflater.from(requireContext()))
|
||||||
val title = resources.getString(R.string.dialog_join_open_group_title, name)
|
val title = resources.getString(R.string.dialog_join_open_group_title, name)
|
||||||
contentView.joinOpenGroupTitleTextView.text = title
|
binding.joinOpenGroupTitleTextView.text = title
|
||||||
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
|
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
|
||||||
val spannable = SpannableStringBuilder(explanation)
|
val spannable = SpannableStringBuilder(explanation)
|
||||||
val startIndex = explanation.indexOf(name)
|
val startIndex = explanation.indexOf(name)
|
||||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
contentView.joinOpenGroupExplanationTextView.text = spannable
|
binding.joinOpenGroupExplanationTextView.text = spannable
|
||||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
binding.cancelButton.setOnClickListener { dismiss() }
|
||||||
contentView.joinButton.setOnClickListener { join() }
|
binding.joinButton.setOnClickListener { join() }
|
||||||
builder.setView(contentView)
|
builder.setView(binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun join() {
|
private fun join() {
|
||||||
|
@ -2,8 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs
|
|||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import kotlinx.android.synthetic.main.dialog_link_preview.view.*
|
import network.loki.messenger.databinding.DialogLinkPreviewBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||||
|
|
||||||
@ -12,10 +11,10 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
|||||||
class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
|
class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
|
||||||
|
|
||||||
override fun setContentView(builder: AlertDialog.Builder) {
|
override fun setContentView(builder: AlertDialog.Builder) {
|
||||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_link_preview, null)
|
val binding = DialogLinkPreviewBinding.inflate(LayoutInflater.from(requireContext()))
|
||||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
binding.cancelButton.setOnClickListener { dismiss() }
|
||||||
contentView.enableLinkPreviewsButton.setOnClickListener { enable() }
|
binding.enableLinkPreviewsButton.setOnClickListener { enable() }
|
||||||
builder.setView(contentView)
|
builder.setView(binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enable() {
|
private fun enable() {
|
||||||
|
@ -2,18 +2,17 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs
|
|||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import kotlinx.android.synthetic.main.dialog_send_seed.view.*
|
import network.loki.messenger.databinding.DialogSendSeedBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||||
|
|
||||||
/** Shown if the user is about to send their recovery phrase to someone. */
|
/** Shown if the user is about to send their recovery phrase to someone. */
|
||||||
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() {
|
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() {
|
||||||
|
|
||||||
override fun setContentView(builder: AlertDialog.Builder) {
|
override fun setContentView(builder: AlertDialog.Builder) {
|
||||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_send_seed, null)
|
val binding = DialogSendSeedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
binding.cancelButton.setOnClickListener { dismiss() }
|
||||||
contentView.sendSeedButton.setOnClickListener { send() }
|
binding.sendSeedButton.setOnClickListener { send() }
|
||||||
builder.setView(contentView)
|
builder.setView(binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun send() {
|
private fun send() {
|
||||||
|
@ -4,13 +4,14 @@ import android.content.Context
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
|
import android.text.TextWatcher
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.view_input_bar.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewInputBarBinding
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
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
|
||||||
@ -27,6 +28,7 @@ import kotlin.math.max
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate {
|
class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate {
|
||||||
|
private lateinit var binding: ViewInputBarBinding
|
||||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||||
private val vMargin by lazy { toDp(4, resources) }
|
private val vMargin by lazy { toDp(4, resources) }
|
||||||
private val minHeight by lazy { toPx(56, resources) }
|
private val minHeight by lazy { toPx(56, resources) }
|
||||||
@ -39,8 +41,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
set(value) { field = value; showOrHideInputIfNeeded() }
|
set(value) { field = value; showOrHideInputIfNeeded() }
|
||||||
|
|
||||||
var text: String
|
var text: String
|
||||||
get() { return inputBarEditText.text?.toString() ?: "" }
|
get() { return binding.inputBarEditText.text?.toString() ?: "" }
|
||||||
set(value) { inputBarEditText.setText(value) }
|
set(value) { binding.inputBarEditText.setText(value) }
|
||||||
|
|
||||||
|
val attachmentButtonsContainerHeight: Int
|
||||||
|
get() = binding.attachmentsButtonContainer.height
|
||||||
|
|
||||||
private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) }
|
private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) }
|
||||||
private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone) }
|
private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone) }
|
||||||
@ -52,37 +57,28 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_input_bar, this)
|
binding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
// Attachments button
|
// Attachments button
|
||||||
attachmentsButtonContainer.addView(attachmentsButton)
|
binding.attachmentsButtonContainer.addView(attachmentsButton)
|
||||||
attachmentsButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
attachmentsButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||||
attachmentsButton.onPress = { toggleAttachmentOptions() }
|
attachmentsButton.onPress = { toggleAttachmentOptions() }
|
||||||
// Microphone button
|
// Microphone button
|
||||||
microphoneOrSendButtonContainer.addView(microphoneButton)
|
binding.microphoneOrSendButtonContainer.addView(microphoneButton)
|
||||||
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
microphoneButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||||
microphoneButton.onLongPress = { startRecordingVoiceMessage() }
|
microphoneButton.onLongPress = { startRecordingVoiceMessage() }
|
||||||
microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
|
microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
|
||||||
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
|
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
|
||||||
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }
|
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }
|
||||||
// Send button
|
// Send button
|
||||||
microphoneOrSendButtonContainer.addView(sendButton)
|
binding.microphoneOrSendButtonContainer.addView(sendButton)
|
||||||
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
sendButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||||
sendButton.isVisible = false
|
sendButton.isVisible = false
|
||||||
sendButton.onUp = { delegate?.sendMessage() }
|
sendButton.onUp = { delegate?.sendMessage() }
|
||||||
// Edit text
|
// Edit text
|
||||||
val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0
|
val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0
|
||||||
inputBarEditText.imeOptions = inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled
|
binding.inputBarEditText.imeOptions = binding.inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled
|
||||||
inputBarEditText.inputType = inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
binding.inputBarEditText.inputType = binding.inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||||
inputBarEditText.delegate = this
|
binding.inputBarEditText.delegate = this
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region General
|
|
||||||
private fun setHeight(newHeight: Int) {
|
|
||||||
val layoutParams = inputBarLinearLayout.layoutParams as LayoutParams
|
|
||||||
layoutParams.height = newHeight
|
|
||||||
inputBarLinearLayout.layoutParams = layoutParams
|
|
||||||
delegate?.inputBarHeightChanged(newHeight)
|
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -94,8 +90,6 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun inputBarEditTextHeightChanged(newValue: Int) {
|
override fun inputBarEditTextHeightChanged(newValue: Int) {
|
||||||
val newHeight = max(newValue + 2 * vMargin, minHeight) + inputBarAdditionalContentContainer.height
|
|
||||||
setHeight(newHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun commitInputContent(contentUri: Uri) {
|
override fun commitInputContent(contentUri: Uri) {
|
||||||
@ -117,45 +111,31 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
quote = message
|
quote = message
|
||||||
linkPreview = null
|
linkPreview = null
|
||||||
linkPreviewDraftView = null
|
linkPreviewDraftView = null
|
||||||
inputBarAdditionalContentContainer.removeAllViews()
|
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||||
val quoteView = QuoteView(context, QuoteView.Mode.Draft)
|
val quoteView = QuoteView(context, QuoteView.Mode.Draft)
|
||||||
quoteView.delegate = this
|
quoteView.delegate = this
|
||||||
inputBarAdditionalContentContainer.addView(quoteView)
|
binding.inputBarAdditionalContentContainer.addView(quoteView)
|
||||||
val attachments = (message as? MmsMessageRecord)?.slideDeck
|
val attachments = (message as? MmsMessageRecord)?.slideDeck
|
||||||
// The max content width is the screen width - 2 times the horizontal input bar padding - the
|
|
||||||
// quote view content area's start and end margins. This unfortunately has to be calculated manually
|
|
||||||
// here to get the layout right.
|
|
||||||
val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt()
|
|
||||||
val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize()
|
val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize()
|
||||||
quoteView.bind(sender, message.body, attachments,
|
quoteView.bind(sender, message.body, attachments,
|
||||||
thread, true, maxContentWidth, message.isOpenGroupInvitation, message.threadId, false, glide)
|
thread, true, message.isOpenGroupInvitation, message.threadId, false, glide)
|
||||||
// The 6 DP below is the padding the quote view applies to itself, which isn't included in the
|
requestLayout()
|
||||||
// intrinsic height calculation.
|
|
||||||
val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources)
|
|
||||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + quoteViewIntrinsicHeight
|
|
||||||
additionalContentHeight = quoteViewIntrinsicHeight
|
|
||||||
setHeight(newHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelQuoteDraft() {
|
override fun cancelQuoteDraft() {
|
||||||
quote = null
|
quote = null
|
||||||
inputBarAdditionalContentContainer.removeAllViews()
|
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
|
requestLayout()
|
||||||
additionalContentHeight = 0
|
|
||||||
setHeight(newHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun draftLinkPreview() {
|
fun draftLinkPreview() {
|
||||||
quote = null
|
quote = null
|
||||||
val linkPreviewDraftHeight = toPx(88, resources)
|
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||||
inputBarAdditionalContentContainer.removeAllViews()
|
|
||||||
val linkPreviewDraftView = LinkPreviewDraftView(context)
|
val linkPreviewDraftView = LinkPreviewDraftView(context)
|
||||||
linkPreviewDraftView.delegate = this
|
linkPreviewDraftView.delegate = this
|
||||||
this.linkPreviewDraftView = linkPreviewDraftView
|
this.linkPreviewDraftView = linkPreviewDraftView
|
||||||
inputBarAdditionalContentContainer.addView(linkPreviewDraftView)
|
binding.inputBarAdditionalContentContainer.addView(linkPreviewDraftView)
|
||||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + linkPreviewDraftHeight
|
requestLayout()
|
||||||
additionalContentHeight = linkPreviewDraftHeight
|
|
||||||
setHeight(newHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) {
|
fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) {
|
||||||
@ -167,24 +147,30 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
override fun cancelLinkPreviewDraft() {
|
override fun cancelLinkPreviewDraft() {
|
||||||
if (quote != null) { return }
|
if (quote != null) { return }
|
||||||
linkPreview = null
|
linkPreview = null
|
||||||
inputBarAdditionalContentContainer.removeAllViews()
|
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
|
requestLayout()
|
||||||
additionalContentHeight = 0
|
|
||||||
setHeight(newHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showOrHideInputIfNeeded() {
|
private fun showOrHideInputIfNeeded() {
|
||||||
if (showInput) {
|
if (showInput) {
|
||||||
setOf( inputBarEditText, attachmentsButton ).forEach { it.isVisible = true }
|
setOf( binding.inputBarEditText, attachmentsButton ).forEach { it.isVisible = true }
|
||||||
microphoneButton.isVisible = text.isEmpty()
|
microphoneButton.isVisible = text.isEmpty()
|
||||||
sendButton.isVisible = text.isNotEmpty()
|
sendButton.isVisible = text.isNotEmpty()
|
||||||
} else {
|
} else {
|
||||||
cancelQuoteDraft()
|
cancelQuoteDraft()
|
||||||
cancelLinkPreviewDraft()
|
cancelLinkPreviewDraft()
|
||||||
val views = setOf( inputBarEditText, attachmentsButton, microphoneButton, sendButton )
|
val views = setOf( binding.inputBarEditText, attachmentsButton, microphoneButton, sendButton )
|
||||||
views.forEach { it.isVisible = false }
|
views.forEach { it.isVisible = false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addTextChangedListener(textWatcher: TextWatcher) {
|
||||||
|
binding.inputBarEditText.addTextChangedListener(textWatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelection(index: Int) {
|
||||||
|
binding.inputBarEditText.setSelection(index)
|
||||||
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,8 +45,8 @@ class InputBarEditText : AppCompatEditText {
|
|||||||
delegate?.inputBarEditTextHeightChanged(constrainedHeight.roundToInt())
|
delegate?.inputBarEditTextHeightChanged(constrainedHeight.roundToInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? {
|
||||||
val ic: InputConnection = super.onCreateInputConnection(editorInfo)
|
val ic = super.onCreateInputConnection(editorInfo) ?: return null
|
||||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png", "image/gif", "image/jpg"))
|
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png", "image/gif", "image/jpg"))
|
||||||
|
|
||||||
val callback =
|
val callback =
|
||||||
|
@ -8,40 +8,56 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
||||||
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.animateSizeChange
|
import org.thoughtcrime.securesms.util.animateSizeChange
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import java.util.Date
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class InputBarRecordingView : RelativeLayout {
|
class InputBarRecordingView : RelativeLayout {
|
||||||
|
private lateinit var binding: ViewInputBarRecordingBinding
|
||||||
private var startTimestamp = 0L
|
private var startTimestamp = 0L
|
||||||
private val snHandler = Handler(Looper.getMainLooper())
|
private val snHandler = Handler(Looper.getMainLooper())
|
||||||
private var dotViewAnimation: ValueAnimator? = null
|
private var dotViewAnimation: ValueAnimator? = null
|
||||||
private var pulseAnimation: ValueAnimator? = null
|
private var pulseAnimation: ValueAnimator? = null
|
||||||
var delegate: InputBarRecordingViewDelegate? = null
|
var delegate: InputBarRecordingViewDelegate? = null
|
||||||
|
|
||||||
|
val lockView: LinearLayout
|
||||||
|
get() = binding.lockView
|
||||||
|
|
||||||
|
val chevronImageView: ImageView
|
||||||
|
get() = binding.inputBarChevronImageView
|
||||||
|
|
||||||
|
val slideToCancelTextView: TextView
|
||||||
|
get() = binding.inputBarSlideToCancelTextView
|
||||||
|
|
||||||
|
val recordButtonOverlay: RelativeLayout
|
||||||
|
get() = binding.recordButtonOverlay
|
||||||
|
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_input_bar_recording, this)
|
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
inputBarMiddleContentContainer.disableClipping()
|
binding.inputBarMiddleContentContainer.disableClipping()
|
||||||
inputBarCancelButton.setOnClickListener { hide() }
|
binding.inputBarCancelButton.setOnClickListener { hide() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show() {
|
fun show() {
|
||||||
startTimestamp = Date().time
|
startTimestamp = Date().time
|
||||||
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
||||||
inputBarCancelButton.alpha = 0.0f
|
binding.inputBarCancelButton.alpha = 0.0f
|
||||||
inputBarMiddleContentContainer.alpha = 1.0f
|
binding.inputBarMiddleContentContainer.alpha = 1.0f
|
||||||
lockView.alpha = 1.0f
|
binding.lockView.alpha = 1.0f
|
||||||
isVisible = true
|
isVisible = true
|
||||||
alpha = 0.0f
|
alpha = 0.0f
|
||||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||||
@ -77,7 +93,7 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
dotViewAnimation = animation
|
dotViewAnimation = animation
|
||||||
animation.duration = 500L
|
animation.duration = 500L
|
||||||
animation.addUpdateListener { animator ->
|
animation.addUpdateListener { animator ->
|
||||||
dotView.alpha = animator.animatedValue as Float
|
binding.dotView.alpha = animator.animatedValue as Float
|
||||||
}
|
}
|
||||||
animation.repeatCount = ValueAnimator.INFINITE
|
animation.repeatCount = ValueAnimator.INFINITE
|
||||||
animation.repeatMode = ValueAnimator.REVERSE
|
animation.repeatMode = ValueAnimator.REVERSE
|
||||||
@ -87,12 +103,12 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
private fun pulse() {
|
private fun pulse() {
|
||||||
val collapsedSize = toPx(80.0f, resources)
|
val collapsedSize = toPx(80.0f, resources)
|
||||||
val expandedSize = toPx(104.0f, resources)
|
val expandedSize = toPx(104.0f, resources)
|
||||||
pulseView.animateSizeChange(collapsedSize, expandedSize, 1000)
|
binding.pulseView.animateSizeChange(collapsedSize, expandedSize, 1000)
|
||||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.5, 0.0f)
|
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.5, 0.0f)
|
||||||
pulseAnimation = animation
|
pulseAnimation = animation
|
||||||
animation.duration = 1000L
|
animation.duration = 1000L
|
||||||
animation.addUpdateListener { animator ->
|
animation.addUpdateListener { animator ->
|
||||||
pulseView.alpha = animator.animatedValue as Float
|
binding.pulseView.alpha = animator.animatedValue as Float
|
||||||
if (animator.animatedFraction == 1.0f && isVisible) { pulse() }
|
if (animator.animatedFraction == 1.0f && isVisible) { pulse() }
|
||||||
}
|
}
|
||||||
animation.start()
|
animation.start()
|
||||||
@ -101,21 +117,21 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
private fun animateLockViewUp() {
|
private fun animateLockViewUp() {
|
||||||
val startMarginBottom = toPx(32, resources)
|
val startMarginBottom = toPx(32, resources)
|
||||||
val endMarginBottom = toPx(72, resources)
|
val endMarginBottom = toPx(72, resources)
|
||||||
val layoutParams = lockView.layoutParams as LayoutParams
|
val layoutParams = binding.lockView.layoutParams as LayoutParams
|
||||||
layoutParams.bottomMargin = startMarginBottom
|
layoutParams.bottomMargin = startMarginBottom
|
||||||
lockView.layoutParams = layoutParams
|
binding.lockView.layoutParams = layoutParams
|
||||||
val animation = ValueAnimator.ofObject(IntEvaluator(), startMarginBottom, endMarginBottom)
|
val animation = ValueAnimator.ofObject(IntEvaluator(), startMarginBottom, endMarginBottom)
|
||||||
animation.duration = 250L
|
animation.duration = 250L
|
||||||
animation.addUpdateListener { animator ->
|
animation.addUpdateListener { animator ->
|
||||||
layoutParams.bottomMargin = animator.animatedValue as Int
|
layoutParams.bottomMargin = animator.animatedValue as Int
|
||||||
lockView.layoutParams = layoutParams
|
binding.lockView.layoutParams = layoutParams
|
||||||
}
|
}
|
||||||
animation.start()
|
animation.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTimer() {
|
private fun updateTimer() {
|
||||||
val duration = (Date().time - startTimestamp) / 1000L
|
val duration = (Date().time - startTimestamp) / 1000L
|
||||||
recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||||
snHandler.postDelayed({ updateTimer() }, 500)
|
snHandler.postDelayed({ updateTimer() }, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,19 +139,19 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||||
fadeOutAnimation.duration = 250L
|
fadeOutAnimation.duration = 250L
|
||||||
fadeOutAnimation.addUpdateListener { animator ->
|
fadeOutAnimation.addUpdateListener { animator ->
|
||||||
inputBarMiddleContentContainer.alpha = animator.animatedValue as Float
|
binding.inputBarMiddleContentContainer.alpha = animator.animatedValue as Float
|
||||||
lockView.alpha = animator.animatedValue as Float
|
binding.lockView.alpha = animator.animatedValue as Float
|
||||||
}
|
}
|
||||||
fadeOutAnimation.start()
|
fadeOutAnimation.start()
|
||||||
val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||||
fadeInAnimation.duration = 250L
|
fadeInAnimation.duration = 250L
|
||||||
fadeInAnimation.addUpdateListener { animator ->
|
fadeInAnimation.addUpdateListener { animator ->
|
||||||
inputBarCancelButton.alpha = animator.animatedValue as Float
|
binding.inputBarCancelButton.alpha = animator.animatedValue as Float
|
||||||
}
|
}
|
||||||
fadeInAnimation.start()
|
fadeInAnimation.start()
|
||||||
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
|
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
|
||||||
recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
|
binding.recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
|
||||||
inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }
|
binding.inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,33 +4,29 @@ import android.content.Context
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
import network.loki.messenger.databinding.ViewMentionCandidateV2Binding
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.session.libsession.messaging.mentions.Mention
|
import org.session.libsession.messaging.mentions.Mention
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : RelativeLayout(context, attrs, defStyleAttr) {
|
class MentionCandidateView : RelativeLayout {
|
||||||
|
private lateinit var binding: ViewMentionCandidateV2Binding
|
||||||
var candidate = Mention("", "")
|
var candidate = Mention("", "")
|
||||||
set(newValue) { field = newValue; update() }
|
set(newValue) { field = newValue; update() }
|
||||||
var glide: GlideRequests? = null
|
var glide: GlideRequests? = null
|
||||||
var openGroupServer: String? = null
|
var openGroupServer: String? = null
|
||||||
var openGroupRoom: String? = null
|
var openGroupRoom: String? = null
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
|
||||||
constructor(context: Context) : this(context, null)
|
constructor(context: Context) : this(context, null)
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||||
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
companion object {
|
private fun initialize() {
|
||||||
|
binding = ViewMentionCandidateV2Binding.inflate(LayoutInflater.from(context), this, true)
|
||||||
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView {
|
|
||||||
return layoutInflater.inflate(R.layout.view_mention_candidate_v2, parent, false) as MentionCandidateView
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun update() {
|
private fun update() = with(binding) {
|
||||||
mentionCandidateNameTextView.text = candidate.displayName
|
mentionCandidateNameTextView.text = candidate.displayName
|
||||||
profilePictureView.publicKey = candidate.publicKey
|
profilePictureView.publicKey = candidate.publicKey
|
||||||
profilePictureView.displayName = candidate.displayName
|
profilePictureView.displayName = candidate.displayName
|
||||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.BaseAdapter
|
import android.widget.BaseAdapter
|
||||||
@ -42,7 +41,7 @@ class MentionCandidatesView(context: Context, attrs: AttributeSet?, defStyleAttr
|
|||||||
override fun getItem(position: Int): Mention { return candidates[position] }
|
override fun getItem(position: Int): Mention { return candidates[position] }
|
||||||
|
|
||||||
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
|
||||||
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent)
|
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context)
|
||||||
val mentionCandidate = getItem(position)
|
val mentionCandidate = getItem(position)
|
||||||
cell.glide = glide
|
cell.glide = glide
|
||||||
cell.candidate = mentionCandidate
|
cell.candidate = mentionCandidate
|
||||||
|
@ -12,7 +12,6 @@ import android.os.AsyncTask
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@ -24,7 +23,6 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
|||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
@ -35,7 +33,12 @@ 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.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
import org.thoughtcrime.securesms.*
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.ExpirationDialog
|
||||||
|
import org.thoughtcrime.securesms.MediaOverviewActivity
|
||||||
|
import org.thoughtcrime.securesms.MuteDialog
|
||||||
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
|
import org.thoughtcrime.securesms.ShortcutLauncherActivity
|
||||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
||||||
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
|
||||||
@ -101,15 +104,12 @@ object ConversationMenuHelper {
|
|||||||
val searchViewItem = menu.findItem(R.id.menu_search)
|
val searchViewItem = menu.findItem(R.id.menu_search)
|
||||||
(context as ConversationActivityV2).searchViewItem = searchViewItem
|
(context as ConversationActivityV2).searchViewItem = searchViewItem
|
||||||
val searchView = searchViewItem.actionView as SearchView
|
val searchView = searchViewItem.actionView as SearchView
|
||||||
val searchViewModel = context.searchViewModel!!
|
|
||||||
val queryListener = object : OnQueryTextListener {
|
val queryListener = object : OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String): Boolean {
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(query: String): Boolean {
|
override fun onQueryTextChange(query: String): Boolean {
|
||||||
searchViewModel.onQueryUpdated(query, threadId)
|
|
||||||
context.searchBottomBar.showLoading()
|
|
||||||
context.onSearchQueryUpdated(query)
|
context.onSearchQueryUpdated(query)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -117,10 +117,7 @@ object ConversationMenuHelper {
|
|||||||
searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
searchView.setOnQueryTextListener(queryListener)
|
searchView.setOnQueryTextListener(queryListener)
|
||||||
searchViewModel.onSearchOpened()
|
context.onSearchOpened()
|
||||||
context.searchBottomBar.visibility = View.VISIBLE
|
|
||||||
context.searchBottomBar.setData(0, 0)
|
|
||||||
context.inputBar.visibility = View.GONE
|
|
||||||
for (i in 0 until menu.size()) {
|
for (i in 0 until menu.size()) {
|
||||||
if (menu.getItem(i) != searchViewItem) {
|
if (menu.getItem(i) != searchViewItem) {
|
||||||
menu.getItem(i).isVisible = false
|
menu.getItem(i).isVisible = false
|
||||||
@ -131,11 +128,7 @@ object ConversationMenuHelper {
|
|||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
searchView.setOnQueryTextListener(null)
|
searchView.setOnQueryTextListener(null)
|
||||||
searchViewModel.onSearchClosed()
|
context.onSearchClosed()
|
||||||
context.searchBottomBar.visibility = View.GONE
|
|
||||||
context.inputBar.visibility = View.VISIBLE
|
|
||||||
context.onSearchQueryUpdated(null)
|
|
||||||
context.invalidateOptionsMenu()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -169,7 +162,7 @@ object ConversationMenuHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun search(context: Context) {
|
private fun search(context: Context) {
|
||||||
val searchViewModel = (context as ConversationActivityV2).searchViewModel!!
|
val searchViewModel = (context as ConversationActivityV2).searchViewModel
|
||||||
searchViewModel.onSearchOpened()
|
searchViewModel.onSearchOpened()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,35 +7,41 @@ import android.view.View
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.synthetic.main.view_control_message.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewControlMessageBinding
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
|
||||||
class ControlMessageView : LinearLayout {
|
class ControlMessageView : LinearLayout {
|
||||||
|
|
||||||
|
private lateinit var binding: ViewControlMessageBinding
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_control_message, this)
|
binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MessageRecord, previous: MessageRecord?) {
|
fun bind(message: MessageRecord, previous: MessageRecord?) {
|
||||||
dateBreakTextView.showDateBreak(message, previous)
|
binding.dateBreakTextView.showDateBreak(message, previous)
|
||||||
iconImageView.visibility = View.GONE
|
binding.iconImageView.visibility = View.GONE
|
||||||
if (message.isExpirationTimerUpdate) {
|
if (message.isExpirationTimerUpdate) {
|
||||||
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme))
|
binding.iconImageView.setImageDrawable(
|
||||||
iconImageView.visibility = View.VISIBLE
|
ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)
|
||||||
|
)
|
||||||
|
binding.iconImageView.visibility = View.VISIBLE
|
||||||
} else if (message.isMediaSavedNotification) {
|
} else if (message.isMediaSavedNotification) {
|
||||||
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme))
|
binding.iconImageView.setImageDrawable(
|
||||||
iconImageView.visibility = View.VISIBLE
|
ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme)
|
||||||
|
)
|
||||||
|
binding.iconImageView.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
textView.text = message.getDisplayBody(context)
|
binding.textView.text = message.getDisplayBody(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
|
@ -6,32 +6,28 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.view.*
|
|
||||||
import kotlinx.android.synthetic.main.view_deleted_message.view.*
|
|
||||||
import kotlinx.android.synthetic.main.view_document.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewDeletedMessageBinding
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class DeletedMessageView : LinearLayout {
|
class DeletedMessageView : LinearLayout {
|
||||||
|
private lateinit var binding: ViewDeletedMessageBinding
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_deleted_message, this)
|
binding = ViewDeletedMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||||
assert(message.isDeleted)
|
assert(message.isDeleted)
|
||||||
deleteTitleTextView.text = context.getString(R.string.deleted_message)
|
binding.deleteTitleTextView.text = context.getString(R.string.deleted_message)
|
||||||
deleteTitleTextView.setTextColor(textColor)
|
binding.deleteTitleTextView.setTextColor(textColor)
|
||||||
deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
@ -6,29 +6,27 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import kotlinx.android.synthetic.main.view_document.view.*
|
import network.loki.messenger.databinding.ViewDocumentBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
|
|
||||||
class DocumentView : LinearLayout {
|
class DocumentView : LinearLayout {
|
||||||
|
private lateinit var binding: ViewDocumentBinding
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_document, this)
|
binding = ViewDocumentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MmsMessageRecord, @ColorInt textColor: Int) {
|
fun bind(message: MmsMessageRecord, @ColorInt textColor: Int) {
|
||||||
val document = message.slideDeck.documentSlide!!
|
val document = message.slideDeck.documentSlide!!
|
||||||
documentTitleTextView.text = document.fileName.or("Untitled File")
|
binding.documentTitleTextView.text = document.fileName.or("Untitled File")
|
||||||
documentTitleTextView.setTextColor(textColor)
|
binding.documentTitleTextView.setTextColor(textColor)
|
||||||
documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
binding.documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
@ -11,8 +11,8 @@ import android.widget.TextView
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.view_link_preview.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewLinkPreviewBinding
|
||||||
import org.thoughtcrime.securesms.components.CornerMask
|
import org.thoughtcrime.securesms.components.CornerMask
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
|
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||||
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide
|
|||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
|
|
||||||
class LinkPreviewView : LinearLayout {
|
class LinkPreviewView : LinearLayout {
|
||||||
|
private lateinit var binding: ViewLinkPreviewBinding
|
||||||
private val cornerMask by lazy { CornerMask(this) }
|
private val cornerMask by lazy { CornerMask(this) }
|
||||||
private var url: String? = null
|
private var url: String? = null
|
||||||
lateinit var bodyTextView: TextView
|
lateinit var bodyTextView: TextView
|
||||||
@ -33,31 +34,35 @@ class LinkPreviewView : LinearLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_link_preview, this)
|
binding = ViewLinkPreviewBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, searchQuery: String?) {
|
fun bind(
|
||||||
|
message: MmsMessageRecord,
|
||||||
|
glide: GlideRequests,
|
||||||
|
isStartOfMessageCluster: Boolean,
|
||||||
|
isEndOfMessageCluster: Boolean
|
||||||
|
) {
|
||||||
val linkPreview = message.linkPreviews.first()
|
val linkPreview = message.linkPreviews.first()
|
||||||
url = linkPreview.url
|
url = linkPreview.url
|
||||||
// Thumbnail
|
// Thumbnail
|
||||||
if (linkPreview.getThumbnail().isPresent) {
|
if (linkPreview.getThumbnail().isPresent) {
|
||||||
// This internally fetches the thumbnail
|
// This internally fetches the thumbnail
|
||||||
thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
||||||
thumbnailImageView.loadIndicator.isVisible = false
|
binding.thumbnailImageView.loadIndicator.isVisible = false
|
||||||
}
|
}
|
||||||
// Title
|
// Title
|
||||||
titleTextView.text = linkPreview.title
|
binding.titleTextView.text = linkPreview.title
|
||||||
val textColorID = if (message.isOutgoing && UiModeUtilities.isDayUiMode(context)) {
|
val textColorID = if (message.isOutgoing && UiModeUtilities.isDayUiMode(context)) {
|
||||||
R.color.white
|
R.color.white
|
||||||
} else {
|
} else {
|
||||||
if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.white
|
if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.white
|
||||||
}
|
}
|
||||||
titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
|
binding.titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
|
||||||
// Body
|
// Body
|
||||||
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
binding.titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
|
||||||
mainLinkPreviewContainer.addView(bodyTextView)
|
|
||||||
// Corner radii
|
// Corner radii
|
||||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||||
cornerMask.setTopLeftRadius(cornerRadii[0])
|
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||||
@ -78,14 +83,14 @@ class LinkPreviewView : LinearLayout {
|
|||||||
val rawYInt = event.rawY.toInt()
|
val rawYInt = event.rawY.toInt()
|
||||||
val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||||
val previewRect = Rect()
|
val previewRect = Rect()
|
||||||
mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
|
binding.mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
|
||||||
if (previewRect.contains(hitRect)) {
|
if (previewRect.contains(hitRect)) {
|
||||||
openURL()
|
openURL()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// intersectedModalSpans should only be a list of one item
|
// intersectedModalSpans should only be a list of one item
|
||||||
val hitSpans = bodyTextView.getIntersectedModalSpans(hitRect)
|
val hitSpans = bodyTextView.getIntersectedModalSpans(hitRect)
|
||||||
hitSpans.forEach { span ->
|
hitSpans.iterator().forEach { span ->
|
||||||
span.onClick(bodyTextView)
|
span.onClick(bodyTextView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,15 @@ import android.view.LayoutInflater
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import kotlinx.android.synthetic.main.view_open_group_invitation.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
import network.loki.messenger.databinding.ViewOpenGroupInvitationBinding
|
||||||
import org.session.libsession.messaging.utilities.UpdateMessageData
|
import org.session.libsession.messaging.utilities.UpdateMessageData
|
||||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
|
||||||
class OpenGroupInvitationView : LinearLayout {
|
class OpenGroupInvitationView : LinearLayout {
|
||||||
|
private lateinit var binding: ViewOpenGroupInvitationBinding
|
||||||
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
|
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
|
||||||
|
|
||||||
constructor(context: Context): super(context) { initialize() }
|
constructor(context: Context): super(context) { initialize() }
|
||||||
@ -22,7 +22,7 @@ class OpenGroupInvitationView : LinearLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_open_group_invitation, this)
|
binding = ViewOpenGroupInvitationBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||||
@ -31,12 +31,14 @@ class OpenGroupInvitationView : LinearLayout {
|
|||||||
val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation
|
val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation
|
||||||
this.data = data
|
this.data = data
|
||||||
val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus
|
val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus
|
||||||
openGroupInvitationIconImageView.setImageResource(iconID)
|
with(binding){
|
||||||
openGroupTitleTextView.text = data.groupName
|
openGroupInvitationIconImageView.setImageResource(iconID)
|
||||||
openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl)
|
openGroupTitleTextView.text = data.groupName
|
||||||
openGroupTitleTextView.setTextColor(textColor)
|
openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl)
|
||||||
openGroupJoinMessageTextView.setTextColor(textColor)
|
openGroupTitleTextView.setTextColor(textColor)
|
||||||
openGroupURLTextView.setTextColor(textColor)
|
openGroupJoinMessageTextView.setTextColor(textColor)
|
||||||
|
openGroupURLTextView.setTextColor(textColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun joinOpenGroup() {
|
fun joinOpenGroup() {
|
||||||
|
@ -2,22 +2,24 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
|
import android.text.StaticLayout
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.RelativeLayout
|
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.android.synthetic.main.view_quote.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewQuoteBinding
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.Quote
|
||||||
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
@ -26,7 +28,6 @@ import org.thoughtcrime.securesms.util.toPx
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
// There's quite some calculation going on here. It's a bit complex so don't make changes
|
// There's quite some calculation going on here. It's a bit complex so don't make changes
|
||||||
// if you don't need to. If you do then test:
|
// if you don't need to. If you do then test:
|
||||||
@ -39,6 +40,7 @@ class QuoteView : LinearLayout {
|
|||||||
|
|
||||||
@Inject lateinit var contactDb: SessionContactDatabase
|
@Inject lateinit var contactDb: SessionContactDatabase
|
||||||
|
|
||||||
|
private lateinit var binding: ViewQuoteBinding
|
||||||
private lateinit var mode: Mode
|
private lateinit var mode: Mode
|
||||||
private val vPadding by lazy { toPx(6, resources) }
|
private val vPadding by lazy { toPx(6, resources) }
|
||||||
var delegate: QuoteViewDelegate? = null
|
var delegate: QuoteViewDelegate? = null
|
||||||
@ -46,25 +48,20 @@ class QuoteView : LinearLayout {
|
|||||||
enum class Mode { Regular, Draft }
|
enum class Mode { Regular, Draft }
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") }
|
constructor(context: Context) : this(context, Mode.Regular)
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") }
|
constructor(context: Context, attrs: AttributeSet) : this(context, Mode.Regular, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") }
|
|
||||||
|
|
||||||
constructor(context: Context, mode: Mode) : super(context) {
|
constructor(context: Context, mode: Mode, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
this.mode = mode
|
this.mode = mode
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_quote, this)
|
binding = ViewQuoteBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
// Add padding here (not on mainQuoteViewContainer) to get a bit of a top inset while avoiding
|
// Add padding here (not on binding.mainQuoteViewContainer) to get a bit of a top inset while avoiding
|
||||||
// the clipping issue described in getIntrinsicHeight(maxContentWidth:).
|
// the clipping issue described in getIntrinsicHeight(maxContentWidth:).
|
||||||
setPadding(0, toPx(6, resources), 0, 0)
|
setPadding(0, toPx(6, resources), 0, 0)
|
||||||
when (mode) {
|
when (mode) {
|
||||||
Mode.Draft -> quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
|
Mode.Draft -> binding.quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
|
||||||
Mode.Regular -> {
|
Mode.Regular -> {
|
||||||
quoteViewCancelButton.isVisible = false
|
binding.quoteViewCancelButton.isVisible = false
|
||||||
mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme))
|
binding.mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme))
|
||||||
val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
|
|
||||||
// Since we're not showing the cancel button we can shorten the end margin
|
|
||||||
quoteViewMainContentContainerLayoutParams.marginEnd = resources.getDimension(R.dimen.medium_spacing).roundToInt()
|
|
||||||
quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,19 +70,19 @@ class QuoteView : LinearLayout {
|
|||||||
// region General
|
// region General
|
||||||
fun getIntrinsicContentHeight(maxContentWidth: Int): Int {
|
fun getIntrinsicContentHeight(maxContentWidth: Int): Int {
|
||||||
// If we're showing an attachment thumbnail, just constrain to the height of that
|
// If we're showing an attachment thumbnail, just constrain to the height of that
|
||||||
if (quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) }
|
if (binding.quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) }
|
||||||
var result = 0
|
var result = 0
|
||||||
var authorTextViewIntrinsicHeight = 0
|
val authorTextViewIntrinsicHeight: Int
|
||||||
if (quoteViewAuthorTextView.isVisible) {
|
if (binding.quoteViewAuthorTextView.isVisible) {
|
||||||
val author = quoteViewAuthorTextView.text
|
val author = binding.quoteViewAuthorTextView.text
|
||||||
authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, quoteViewAuthorTextView.paint, maxContentWidth)
|
authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, binding.quoteViewAuthorTextView.paint, maxContentWidth)
|
||||||
result += authorTextViewIntrinsicHeight
|
result += authorTextViewIntrinsicHeight
|
||||||
}
|
}
|
||||||
val body = quoteViewBodyTextView.text
|
val body = binding.quoteViewBodyTextView.text
|
||||||
val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, quoteViewBodyTextView.paint, maxContentWidth)
|
val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
|
||||||
val staticLayout = TextUtilities.getIntrinsicLayout(body, quoteViewBodyTextView.paint, maxContentWidth)
|
val staticLayout = TextUtilities.getIntrinsicLayout(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
|
||||||
result += bodyTextViewIntrinsicHeight
|
result += bodyTextViewIntrinsicHeight
|
||||||
if (!quoteViewAuthorTextView.isVisible) {
|
if (!binding.quoteViewAuthorTextView.isVisible) {
|
||||||
// We want to at least be as high as the cancel button 36DP, and no higher than 3 lines of text.
|
// We want to at least be as high as the cancel button 36DP, and no higher than 3 lines of text.
|
||||||
// Height from intrinsic layout is the height of the text before truncation so we shorten
|
// Height from intrinsic layout is the height of the text before truncation so we shorten
|
||||||
// proportionally to our max lines setting.
|
// proportionally to our max lines setting.
|
||||||
@ -110,89 +107,114 @@ class QuoteView : LinearLayout {
|
|||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
|
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
|
||||||
isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean, threadID: Long,
|
isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long,
|
||||||
isOriginalMissing: Boolean, glide: GlideRequests) {
|
isOriginalMissing: Boolean, glide: GlideRequests) {
|
||||||
// Reduce the max body text view line count to 2 if this is a group thread because
|
// Reduce the max body text view line count to 2 if this is a group thread because
|
||||||
// we'll be showing the author text view and we don't want the overall quote view height
|
// we'll be showing the author text view and we don't want the overall quote view height
|
||||||
// to get too big.
|
// to get too big.
|
||||||
quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3
|
binding.quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3
|
||||||
// Author
|
// Author
|
||||||
if (thread.isGroupRecipient) {
|
if (thread.isGroupRecipient) {
|
||||||
val author = contactDb.getContactWithSessionID(authorPublicKey)
|
val author = contactDb.getContactWithSessionID(authorPublicKey)
|
||||||
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
|
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
|
||||||
quoteViewAuthorTextView.text = authorDisplayName
|
binding.quoteViewAuthorTextView.text = authorDisplayName
|
||||||
quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||||
}
|
}
|
||||||
quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
binding.quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
||||||
// Body
|
// Body
|
||||||
quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context);
|
binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context)
|
||||||
quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
|
binding.quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||||
// Accent line / attachment preview
|
// Accent line / attachment preview
|
||||||
val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing
|
val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing
|
||||||
quoteViewAccentLine.isVisible = !hasAttachments
|
binding.quoteViewAccentLine.isVisible = !hasAttachments
|
||||||
quoteViewAttachmentPreviewContainer.isVisible = hasAttachments
|
binding.quoteViewAttachmentPreviewContainer.isVisible = hasAttachments
|
||||||
if (!hasAttachments) {
|
if (!hasAttachments) {
|
||||||
val accentLineLayoutParams = quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams
|
binding.quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage))
|
||||||
accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height
|
|
||||||
quoteViewAccentLine.layoutParams = accentLineLayoutParams
|
|
||||||
quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage))
|
|
||||||
} else if (attachments != null) {
|
} else if (attachments != null) {
|
||||||
quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme))
|
binding.quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme))
|
||||||
val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent
|
val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent
|
||||||
val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme)
|
val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme)
|
||||||
quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
||||||
quoteViewAttachmentPreviewImageView.isVisible = false
|
binding.quoteViewAttachmentPreviewImageView.isVisible = false
|
||||||
quoteViewAttachmentThumbnailImageView.isVisible = false
|
binding.quoteViewAttachmentThumbnailImageView.isVisible = false
|
||||||
if (attachments.audioSlide != null) {
|
when {
|
||||||
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
attachments.audioSlide != null -> {
|
||||||
quoteViewAttachmentPreviewImageView.isVisible = true
|
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||||
quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
|
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
||||||
} else if (attachments.documentSlide != null) {
|
binding.quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
|
||||||
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
}
|
||||||
quoteViewAttachmentPreviewImageView.isVisible = true
|
attachments.documentSlide != null -> {
|
||||||
quoteViewBodyTextView.text = resources.getString(R.string.document)
|
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
||||||
} else if (attachments.thumbnailSlide != null) {
|
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
||||||
val slide = attachments.thumbnailSlide!!
|
binding.quoteViewBodyTextView.text = resources.getString(R.string.document)
|
||||||
// This internally fetches the thumbnail
|
}
|
||||||
quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources)
|
attachments.thumbnailSlide != null -> {
|
||||||
quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false)
|
val slide = attachments.thumbnailSlide!!
|
||||||
quoteViewAttachmentThumbnailImageView.isVisible = true
|
// This internally fetches the thumbnail
|
||||||
quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
|
binding.quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources)
|
||||||
|
binding.quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false)
|
||||||
|
binding.quoteViewAttachmentThumbnailImageView.isVisible = true
|
||||||
|
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth))
|
|
||||||
val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
|
|
||||||
// The start margin is different if we just show the accent line vs if we show an attachment thumbnail
|
|
||||||
quoteViewMainContentContainerLayoutParams.marginStart = if (!hasAttachments) toPx(16, resources) else toPx(48, resources)
|
|
||||||
quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
|
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Convenience
|
// region Convenience
|
||||||
@ColorInt private fun getLineColor(isOutgoingMessage: Boolean): Int {
|
@ColorInt private fun getLineColor(isOutgoingMessage: Boolean): Int {
|
||||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||||
if ((mode == Mode.Regular && isLightMode) || (mode == Mode.Draft && isLightMode)) {
|
return when {
|
||||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
mode == Mode.Regular && isLightMode || mode == Mode.Draft && isLightMode -> {
|
||||||
} else if (mode == Mode.Regular && !isLightMode) {
|
ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||||
if (isOutgoingMessage) {
|
}
|
||||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
mode == Mode.Regular && !isLightMode -> {
|
||||||
} else {
|
if (isOutgoingMessage) {
|
||||||
return ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||||
|
} else {
|
||||||
|
ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> { // Draft & dark mode
|
||||||
|
ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
||||||
}
|
}
|
||||||
} else { // Draft & dark mode
|
|
||||||
return ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt private fun getTextColor(isOutgoingMessage: Boolean): Int {
|
@ColorInt private fun getTextColor(isOutgoingMessage: Boolean): Int {
|
||||||
if (mode == Mode.Draft) { return ResourcesCompat.getColor(resources, R.color.text, context.theme) }
|
if (mode == Mode.Draft) { return ResourcesCompat.getColor(resources, R.color.text, context.theme) }
|
||||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||||
if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) {
|
return if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) {
|
||||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||||
} else {
|
} else {
|
||||||
return ResourcesCompat.getColor(resources, R.color.white, context.theme)
|
ResourcesCompat.getColor(resources, R.color.white, context.theme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun calculateWidth(quote: Quote, bodyWidth: Int, maxContentWidth: Int, thread: Recipient): Int {
|
||||||
|
binding.quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
||||||
|
var paddingWidth = resources.getDimensionPixelSize(R.dimen.medium_spacing) * 5 // initial horizontal padding
|
||||||
|
with (binding) {
|
||||||
|
if (quoteViewAttachmentPreviewContainer.isVisible) {
|
||||||
|
paddingWidth += toPx(40, resources)
|
||||||
|
}
|
||||||
|
if (quoteViewAccentLine.isVisible) {
|
||||||
|
paddingWidth += resources.getDimensionPixelSize(R.dimen.accent_line_thickness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val quoteBodyWidth = StaticLayout.getDesiredWidth(binding.quoteViewBodyTextView.text, binding.quoteViewBodyTextView.paint).toInt() + paddingWidth
|
||||||
|
|
||||||
|
val quoteAuthorWidth = if (thread.isGroupRecipient) {
|
||||||
|
val authorPublicKey = quote.author.serialize()
|
||||||
|
val author = contactDb.getContactWithSessionID(authorPublicKey)
|
||||||
|
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
|
||||||
|
StaticLayout.getDesiredWidth(authorDisplayName, binding.quoteViewBodyTextView.paint).toInt() + paddingWidth
|
||||||
|
} else 0
|
||||||
|
|
||||||
|
val quoteWidth = max(quoteBodyWidth, quoteAuthorWidth)
|
||||||
|
val usedWidth = max(quoteWidth, bodyWidth)
|
||||||
|
return min(maxContentWidth, usedWidth)
|
||||||
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,15 +6,15 @@ import android.view.LayoutInflater
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import kotlinx.android.synthetic.main.view_untrusted_attachment.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
|
||||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
class UntrustedAttachmentView: LinearLayout {
|
class UntrustedAttachmentView: LinearLayout {
|
||||||
|
private lateinit var binding: ViewUntrustedAttachmentBinding
|
||||||
enum class AttachmentType {
|
enum class AttachmentType {
|
||||||
AUDIO,
|
AUDIO,
|
||||||
DOCUMENT,
|
DOCUMENT,
|
||||||
@ -27,7 +27,7 @@ class UntrustedAttachmentView: LinearLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_untrusted_attachment, this)
|
binding = ViewUntrustedAttachmentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -42,8 +42,8 @@ class UntrustedAttachmentView: LinearLayout {
|
|||||||
iconDrawable.mutate().setTint(textColor)
|
iconDrawable.mutate().setTint(textColor)
|
||||||
val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT))
|
val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT))
|
||||||
|
|
||||||
untrustedAttachmentIcon.setImageDrawable(iconDrawable)
|
binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable)
|
||||||
untrustedAttachmentTitle.text = text
|
binding.untrustedAttachmentTitle.text = text
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
@ -5,16 +5,17 @@ import android.graphics.Color
|
|||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
|
import android.text.StaticLayout
|
||||||
import android.text.style.BackgroundColorSpan
|
import android.text.style.BackgroundColorSpan
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@ -23,24 +24,22 @@ import androidx.core.graphics.BlendModeColorFilterCompat
|
|||||||
import androidx.core.graphics.BlendModeCompat
|
import androidx.core.graphics.BlendModeCompat
|
||||||
import androidx.core.text.getSpans
|
import androidx.core.text.getSpans
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import kotlinx.android.synthetic.main.view_visible_message_content.view.*
|
import androidx.core.view.isVisible
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.utilities.ThemeUtil
|
import org.session.libsession.utilities.ThemeUtil
|
||||||
import org.session.libsession.utilities.ViewUtil
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
|
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
|
||||||
import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
|
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
||||||
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.database.model.SmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
|
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
import org.thoughtcrime.securesms.util.getColorWithID
|
import org.thoughtcrime.securesms.util.getColorWithID
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
@ -48,7 +47,8 @@ import java.util.*
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class VisibleMessageContentView : LinearLayout {
|
class VisibleMessageContentView : LinearLayout {
|
||||||
var onContentClick: ((event: MotionEvent) -> Unit)? = null
|
private lateinit var binding: ViewVisibleMessageContentBinding
|
||||||
|
var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
|
||||||
var onContentDoubleTap: (() -> Unit)? = null
|
var onContentDoubleTap: (() -> Unit)? = null
|
||||||
var delegate: VisibleMessageContentViewDelegate? = null
|
var delegate: VisibleMessageContentViewDelegate? = null
|
||||||
var indexInAdapter: Int = -1
|
var indexInAdapter: Int = -1
|
||||||
@ -59,7 +59,7 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_visible_message_content, this)
|
binding = ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -73,23 +73,42 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
|
val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
|
||||||
background.colorFilter = filter
|
background.colorFilter = filter
|
||||||
setBackground(background)
|
setBackground(background)
|
||||||
// Body
|
|
||||||
mainContainer.removeAllViews()
|
val onlyBodyMessage = message is SmsMessageRecord
|
||||||
onContentClick = null
|
val mediaThumbnailMessage = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.thumbnailSlide != null
|
||||||
|
|
||||||
|
// reset visibilities / containers
|
||||||
|
onContentClick.clear()
|
||||||
|
binding.albumThumbnailView.clearViews()
|
||||||
onContentDoubleTap = null
|
onContentDoubleTap = null
|
||||||
|
|
||||||
if (message.isDeleted) {
|
if (message.isDeleted) {
|
||||||
val deletedMessageView = DeletedMessageView(context)
|
binding.deletedMessageView.isVisible = true
|
||||||
deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message))
|
binding.deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message))
|
||||||
mainContainer.addView(deletedMessageView)
|
return
|
||||||
} else if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
} else {
|
||||||
val linkPreviewView = LinkPreviewView(context)
|
binding.deletedMessageView.isVisible = false
|
||||||
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
|
}
|
||||||
mainContainer.addView(linkPreviewView)
|
|
||||||
onContentClick = { event -> linkPreviewView.calculateHit(event) }
|
binding.quoteView.isVisible = message is MmsMessageRecord && message.quote != null
|
||||||
// Body text view is inside the link preview for layout convenience
|
|
||||||
} else if (message is MmsMessageRecord && message.quote != null) {
|
binding.linkPreviewView.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty()
|
||||||
|
|
||||||
|
val linkPreviewLayout = binding.linkPreviewView.layoutParams
|
||||||
|
linkPreviewLayout.width = if (mediaThumbnailMessage) 0 else ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
binding.linkPreviewView.layoutParams = linkPreviewLayout
|
||||||
|
|
||||||
|
binding.untrustedView.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null
|
||||||
|
binding.voiceMessageView.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
|
||||||
|
binding.documentView.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
|
||||||
|
binding.albumThumbnailView.isVisible = mediaThumbnailMessage
|
||||||
|
binding.openGroupInvitationView.isVisible = message.isOpenGroupInvitation
|
||||||
|
|
||||||
|
var hideBody = false
|
||||||
|
|
||||||
|
if (message is MmsMessageRecord && message.quote != null) {
|
||||||
|
binding.quoteView.isVisible = true
|
||||||
val quote = message.quote!!
|
val quote = message.quote!!
|
||||||
val quoteView = QuoteView(context, QuoteView.Mode.Regular)
|
|
||||||
// The max content width is the max message bubble size - 2 times the horizontal padding - 2
|
// The max content width is the max message bubble size - 2 times the horizontal padding - 2
|
||||||
// times the horizontal margin. This unfortunately has to be calculated manually
|
// times the horizontal margin. This unfortunately has to be calculated manually
|
||||||
// here to get the layout right.
|
// here to get the layout right.
|
||||||
@ -99,136 +118,161 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
} else {
|
} else {
|
||||||
quote.text
|
quote.text
|
||||||
}
|
}
|
||||||
quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
|
binding.quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
|
||||||
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId,
|
message.isOutgoing, message.isOpenGroupInvitation, message.threadId,
|
||||||
quote.isOriginalMissing, glide)
|
quote.isOriginalMissing, glide)
|
||||||
mainContainer.addView(quoteView)
|
onContentClick.add { event ->
|
||||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
|
||||||
ViewUtil.setPaddingTop(bodyTextView, 0)
|
|
||||||
mainContainer.addView(bodyTextView)
|
|
||||||
onContentClick = { event ->
|
|
||||||
val r = Rect()
|
val r = Rect()
|
||||||
quoteView.getGlobalVisibleRect(r)
|
binding.quoteView.getGlobalVisibleRect(r)
|
||||||
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
|
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
|
||||||
delegate?.scrollToMessageIfPossible(quote.id)
|
delegate?.scrollToMessageIfPossible(quote.id)
|
||||||
} else {
|
|
||||||
bodyTextView.getIntersectedModalSpans(event).forEach { span ->
|
|
||||||
span.onClick(bodyTextView)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
||||||
|
binding.linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||||
|
onContentClick.add { event -> binding.linkPreviewView.calculateHit(event) }
|
||||||
|
// Body text view is inside the link preview for layout convenience
|
||||||
} else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) {
|
} else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) {
|
||||||
|
hideBody = true
|
||||||
// Audio attachment
|
// Audio attachment
|
||||||
if (contactIsTrusted || message.isOutgoing) {
|
if (contactIsTrusted || message.isOutgoing) {
|
||||||
val voiceMessageView = VoiceMessageView(context)
|
binding.voiceMessageView.indexInAdapter = indexInAdapter
|
||||||
voiceMessageView.indexInAdapter = indexInAdapter
|
binding.voiceMessageView.delegate = context as? ConversationActivityV2
|
||||||
voiceMessageView.delegate = context as? ConversationActivityV2
|
binding.voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||||
voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
|
|
||||||
mainContainer.addView(voiceMessageView)
|
|
||||||
// We have to use onContentClick (rather than a click listener directly on the voice
|
// We have to use onContentClick (rather than a click listener directly on the voice
|
||||||
// message view) so as to not interfere with all the other gestures.
|
// message view) so as to not interfere with all the other gestures.
|
||||||
onContentClick = { voiceMessageView.togglePlayback() }
|
onContentClick.add { binding.voiceMessageView.togglePlayback() }
|
||||||
onContentDoubleTap = { voiceMessageView.handleDoubleTap() }
|
onContentDoubleTap = { binding.voiceMessageView.handleDoubleTap() }
|
||||||
} else {
|
} else {
|
||||||
val untrustedView = UntrustedAttachmentView(context)
|
// TODO: move this out to its own area
|
||||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
|
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
|
||||||
mainContainer.addView(untrustedView)
|
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
||||||
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
|
||||||
}
|
}
|
||||||
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
|
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
|
||||||
|
hideBody = true
|
||||||
// Document attachment
|
// Document attachment
|
||||||
if (contactIsTrusted || message.isOutgoing) {
|
if (contactIsTrusted || message.isOutgoing) {
|
||||||
val documentView = DocumentView(context)
|
binding.documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||||
documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
|
||||||
mainContainer.addView(documentView)
|
|
||||||
} else {
|
} else {
|
||||||
val untrustedView = UntrustedAttachmentView(context)
|
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
|
||||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
|
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
||||||
mainContainer.addView(untrustedView)
|
|
||||||
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
|
||||||
}
|
}
|
||||||
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
|
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
|
||||||
// Images/Video attachment
|
/*
|
||||||
|
* Images / Video attachment
|
||||||
|
*/
|
||||||
if (contactIsTrusted || message.isOutgoing) {
|
if (contactIsTrusted || message.isOutgoing) {
|
||||||
val albumThumbnailView = AlbumThumbnailView(context)
|
|
||||||
mainContainer.addView(albumThumbnailView)
|
|
||||||
// isStart and isEnd of cluster needed for calculating the mask for full bubble image groups
|
// isStart and isEnd of cluster needed for calculating the mask for full bubble image groups
|
||||||
// bind after add view because views are inflated and calculated during bind
|
// bind after add view because views are inflated and calculated during bind
|
||||||
albumThumbnailView.bind(
|
binding.albumThumbnailView.bind(
|
||||||
glideRequests = glide,
|
glideRequests = glide,
|
||||||
message = message,
|
message = message,
|
||||||
isStart = isStartOfMessageCluster,
|
isStart = isStartOfMessageCluster,
|
||||||
isEnd = isEndOfMessageCluster
|
isEnd = isEndOfMessageCluster
|
||||||
)
|
)
|
||||||
onContentClick = { event ->
|
onContentClick.add { event ->
|
||||||
albumThumbnailView.calculateHitObject(event, message, thread)
|
binding.albumThumbnailView.calculateHitObject(event, message, thread)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val untrustedView = UntrustedAttachmentView(context)
|
hideBody = true
|
||||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
binding.albumThumbnailView.clearViews()
|
||||||
mainContainer.addView(untrustedView)
|
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
||||||
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
||||||
}
|
}
|
||||||
} else if (message.isOpenGroupInvitation) {
|
} else if (message.isOpenGroupInvitation) {
|
||||||
val openGroupInvitationView = OpenGroupInvitationView(context)
|
hideBody = true
|
||||||
openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
binding.openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||||
mainContainer.addView(openGroupInvitationView)
|
onContentClick.add { binding.openGroupInvitationView.joinOpenGroup() }
|
||||||
onContentClick = { openGroupInvitationView.joinOpenGroup() }
|
}
|
||||||
} else {
|
|
||||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
binding.bodyTextView.isVisible = message.body.isNotEmpty() && !hideBody
|
||||||
mainContainer.addView(bodyTextView)
|
|
||||||
onContentClick = { event ->
|
// set it to use constraints if not only a text message, otherwise wrap content to whatever width it wants
|
||||||
// intersectedModalSpans should only be a list of one item
|
val params = binding.bodyTextView.layoutParams
|
||||||
bodyTextView.getIntersectedModalSpans(event).forEach { span ->
|
params.width = if (onlyBodyMessage || binding.barrierViewsGone()) ViewGroup.LayoutParams.WRAP_CONTENT else 0
|
||||||
span.onClick(bodyTextView)
|
binding.bodyTextView.layoutParams = params
|
||||||
|
binding.bodyTextView.maxWidth = maxWidth
|
||||||
|
|
||||||
|
val bodyWidth = with (binding.bodyTextView) {
|
||||||
|
StaticLayout.getDesiredWidth(text, paint).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val quote = (message as? MmsMessageRecord)?.quote
|
||||||
|
val quoteLayoutParams = binding.quoteView.layoutParams
|
||||||
|
quoteLayoutParams.width =
|
||||||
|
if (mediaThumbnailMessage || quote == null) 0
|
||||||
|
else binding.quoteView.calculateWidth(quote, bodyWidth, maxWidth, thread)
|
||||||
|
|
||||||
|
binding.quoteView.layoutParams = quoteLayoutParams
|
||||||
|
|
||||||
|
if (message.body.isNotEmpty() && !hideBody) {
|
||||||
|
val color = getTextColor(context, message)
|
||||||
|
binding.bodyTextView.setTextColor(color)
|
||||||
|
binding.bodyTextView.setLinkTextColor(color)
|
||||||
|
val body = getBodySpans(context, message, searchQuery)
|
||||||
|
binding.bodyTextView.text = body
|
||||||
|
onContentClick.add { e: MotionEvent ->
|
||||||
|
binding.bodyTextView.getIntersectedModalSpans(e).iterator().forEach { span ->
|
||||||
|
span.onClick(binding.bodyTextView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
||||||
|
listOf<View>(albumThumbnailView, linkPreviewView, voiceMessageView, quoteView).none { it.isVisible }
|
||||||
|
|
||||||
private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable {
|
private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable {
|
||||||
val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster)
|
val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster)
|
||||||
@DrawableRes val backgroundID: Int
|
@DrawableRes val backgroundID = when {
|
||||||
if (isSingleMessage) {
|
isSingleMessage -> {
|
||||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
|
if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
|
||||||
} else if (isStartOfMessageCluster) {
|
}
|
||||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start
|
isStartOfMessageCluster -> {
|
||||||
} else if (isEndOfMessageCluster) {
|
if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start
|
||||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end
|
}
|
||||||
} else {
|
isEndOfMessageCluster -> {
|
||||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle
|
if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!!
|
return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
mainContainer.removeAllViews()
|
arrayOf(
|
||||||
|
binding.deletedMessageView,
|
||||||
|
binding.untrustedView,
|
||||||
|
binding.voiceMessageView,
|
||||||
|
binding.openGroupInvitationView,
|
||||||
|
binding.documentView,
|
||||||
|
binding.quoteView,
|
||||||
|
binding.linkPreviewView,
|
||||||
|
binding.albumThumbnailView,
|
||||||
|
binding.bodyTextView
|
||||||
|
).forEach { view -> view.isVisible = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playVoiceMessage() {
|
||||||
|
binding.voiceMessageView.togglePlayback()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Convenience
|
// region Convenience
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun getBodyTextView(context: Context, message: MessageRecord, searchQuery: String?): TextView {
|
|
||||||
val result = EmojiTextView(context)
|
|
||||||
val vPadding = context.resources.getDimension(R.dimen.small_spacing).toInt()
|
|
||||||
val hPadding = toPx(12, context.resources)
|
|
||||||
result.setPadding(hPadding, vPadding, hPadding, vPadding)
|
|
||||||
result.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.resources.getDimension(R.dimen.small_font_size))
|
|
||||||
val color = getTextColor(context, message)
|
|
||||||
result.setTextColor(color)
|
|
||||||
result.setLinkTextColor(color)
|
|
||||||
val body = getBodySpans(context, message, searchQuery)
|
|
||||||
result.text = body
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBodySpans(context: Context, message: MessageRecord, searchQuery: String?): Spannable {
|
fun getBodySpans(context: Context, message: MessageRecord, searchQuery: String?): Spannable {
|
||||||
var body = message.body.toSpannable()
|
var body = message.body.toSpannable()
|
||||||
|
|
||||||
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context)
|
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context)
|
||||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
|
body = SearchUtil.getHighlightedSpan(Locale.getDefault(),
|
||||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
|
{ BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
|
||||||
|
body = SearchUtil.getHighlightedSpan(Locale.getDefault(),
|
||||||
|
{ ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
|
||||||
|
|
||||||
Linkify.addLinks(body, Linkify.WEB_URLS)
|
Linkify.addLinks(body, Linkify.WEB_URLS)
|
||||||
|
|
||||||
|
@ -5,39 +5,51 @@ import android.content.res.Resources
|
|||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.*
|
import android.view.Gravity
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.RelativeLayout
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.android.synthetic.main.view_visible_message.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||||
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.utilities.ViewUtil
|
import org.session.libsession.utilities.ViewUtil
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.database.*
|
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.*
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import java.util.*
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
|
import org.thoughtcrime.securesms.util.getColorWithID
|
||||||
|
import org.thoughtcrime.securesms.util.toDp
|
||||||
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class VisibleMessageView : LinearLayout {
|
class VisibleMessageView : LinearLayout {
|
||||||
|
|
||||||
@ -48,6 +60,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
@Inject lateinit var smsDb: SmsDatabase
|
@Inject lateinit var smsDb: SmsDatabase
|
||||||
@Inject lateinit var mmsDb: MmsDatabase
|
@Inject lateinit var mmsDb: MmsDatabase
|
||||||
|
|
||||||
|
private lateinit var binding: ViewVisibleMessageBinding
|
||||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||||
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
||||||
private val swipeToReplyIconRect = Rect()
|
private val swipeToReplyIconRect = Rect()
|
||||||
@ -60,7 +73,11 @@ class VisibleMessageView : LinearLayout {
|
|||||||
private var onDoubleTap: (() -> Unit)? = null
|
private var onDoubleTap: (() -> Unit)? = null
|
||||||
var indexInAdapter: Int = -1
|
var indexInAdapter: Int = -1
|
||||||
var snIsSelected = false
|
var snIsSelected = false
|
||||||
set(value) { field = value; handleIsSelectedChanged()}
|
set(value) {
|
||||||
|
field = value
|
||||||
|
binding.messageTimestampTextView.isVisible = isSelected
|
||||||
|
handleIsSelectedChanged()
|
||||||
|
}
|
||||||
var onPress: ((event: MotionEvent) -> Unit)? = null
|
var onPress: ((event: MotionEvent) -> Unit)? = null
|
||||||
var onSwipeToReply: (() -> Unit)? = null
|
var onSwipeToReply: (() -> Unit)? = null
|
||||||
var onLongPress: (() -> Unit)? = null
|
var onLongPress: (() -> Unit)? = null
|
||||||
@ -68,7 +85,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val swipeToReplyThreshold = 64.0f // dp
|
const val swipeToReplyThreshold = 64.0f // dp
|
||||||
const val longPressMovementTreshold = 10.0f // dp
|
const val longPressMovementThreshold = 10.0f // dp
|
||||||
const val longPressDurationThreshold = 250L // ms
|
const val longPressDurationThreshold = 250L // ms
|
||||||
const val maxDoubleTapInterval = 200L
|
const val maxDoubleTapInterval = 200L
|
||||||
}
|
}
|
||||||
@ -79,12 +96,12 @@ class VisibleMessageView : LinearLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_visible_message, this)
|
binding = ViewVisibleMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
isHapticFeedbackEnabled = true
|
isHapticFeedbackEnabled = true
|
||||||
setWillNotDraw(false)
|
setWillNotDraw(false)
|
||||||
expirationTimerViewContainer.disableClipping()
|
binding.expirationTimerViewContainer.disableClipping()
|
||||||
messageContentContainer.disableClipping()
|
binding.messageContentContainer.disableClipping()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -101,47 +118,46 @@ class VisibleMessageView : LinearLayout {
|
|||||||
// Show profile picture and sender name if this is a group thread AND
|
// Show profile picture and sender name if this is a group thread AND
|
||||||
// the message is incoming
|
// the message is incoming
|
||||||
if (isGroupThread && !message.isOutgoing) {
|
if (isGroupThread && !message.isOutgoing) {
|
||||||
profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
|
binding.profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
|
||||||
profilePictureView.publicKey = senderSessionID
|
binding.profilePictureView.publicKey = senderSessionID
|
||||||
profilePictureView.glide = glide
|
binding.profilePictureView.glide = glide
|
||||||
profilePictureView.update(message.individualRecipient, threadID)
|
binding.profilePictureView.update(message.individualRecipient)
|
||||||
profilePictureView.setOnClickListener {
|
binding.profilePictureView.setOnClickListener {
|
||||||
showUserDetails(senderSessionID, threadID)
|
showUserDetails(senderSessionID, threadID)
|
||||||
}
|
}
|
||||||
if (thread.isOpenGroupRecipient) {
|
if (thread.isOpenGroupRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||||
val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server)
|
val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server)
|
||||||
moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
|
binding.moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
|
||||||
} else {
|
} else {
|
||||||
moderatorIconImageView.visibility = View.INVISIBLE
|
binding.moderatorIconImageView.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
senderNameTextView.isVisible = isStartOfMessageCluster
|
binding.senderNameTextView.isVisible = isStartOfMessageCluster
|
||||||
val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||||
senderNameTextView.text = contact?.displayName(context) ?: senderSessionID
|
binding.senderNameTextView.text = contact?.displayName(context) ?: senderSessionID
|
||||||
} else {
|
} else {
|
||||||
profilePictureContainer.visibility = View.GONE
|
binding.profilePictureContainer.visibility = View.GONE
|
||||||
senderNameTextView.visibility = View.GONE
|
binding.senderNameTextView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
// Date break
|
// Date break
|
||||||
dateBreakTextView.showDateBreak(message, previous)
|
binding.dateBreakTextView.showDateBreak(message, previous)
|
||||||
// Timestamp
|
// Timestamp
|
||||||
messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
|
binding.messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
|
||||||
// Margins
|
// Margins
|
||||||
val startPadding: Int
|
val startPadding = if (isGroupThread) {
|
||||||
if (isGroupThread) {
|
if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.very_large_spacing) else toPx(50,resources)
|
||||||
startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() else 0
|
|
||||||
} else {
|
} else {
|
||||||
startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt()
|
if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.very_large_spacing)
|
||||||
else resources.getDimension(R.dimen.medium_spacing).toInt()
|
else resources.getDimensionPixelSize(R.dimen.medium_spacing)
|
||||||
}
|
}
|
||||||
val endPadding = if (message.isOutgoing) resources.getDimension(R.dimen.medium_spacing).toInt()
|
val endPadding = if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.medium_spacing)
|
||||||
else resources.getDimension(R.dimen.very_large_spacing).toInt()
|
else resources.getDimensionPixelSize(R.dimen.very_large_spacing)
|
||||||
messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0)
|
binding.messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0)
|
||||||
// Set inter-message spacing
|
// Set inter-message spacing
|
||||||
setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster)
|
setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster)
|
||||||
// Gravity
|
// Gravity
|
||||||
val gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
val gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
||||||
mainContainer.gravity = gravity or Gravity.BOTTOM
|
binding.mainContainer.gravity = gravity or Gravity.BOTTOM
|
||||||
// Message status indicator
|
// Message status indicator
|
||||||
val (iconID, iconColor) = getMessageStatusImage(message)
|
val (iconID, iconColor) = getMessageStatusImage(message)
|
||||||
if (iconID != null) {
|
if (iconID != null) {
|
||||||
@ -149,24 +165,24 @@ class VisibleMessageView : LinearLayout {
|
|||||||
if (iconColor != null) {
|
if (iconColor != null) {
|
||||||
drawable?.setTint(iconColor)
|
drawable?.setTint(iconColor)
|
||||||
}
|
}
|
||||||
messageStatusImageView.setImageDrawable(drawable)
|
binding.messageStatusImageView.setImageDrawable(drawable)
|
||||||
}
|
}
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
||||||
messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
|
binding.messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
|
||||||
} else {
|
} else {
|
||||||
messageStatusImageView.isVisible = false
|
binding.messageStatusImageView.isVisible = false
|
||||||
}
|
}
|
||||||
// Expiration timer
|
// Expiration timer
|
||||||
updateExpirationTimer(message)
|
updateExpirationTimer(message)
|
||||||
// Calculate max message bubble width
|
// Calculate max message bubble width
|
||||||
var maxWidth = screenWidth - startPadding - endPadding
|
var maxWidth = screenWidth - startPadding - endPadding
|
||||||
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
|
if (binding.profilePictureContainer.visibility != View.GONE) { maxWidth -= binding.profilePictureContainer.width }
|
||||||
// Populate content view
|
// Populate content view
|
||||||
messageContentView.indexInAdapter = indexInAdapter
|
binding.messageContentView.indexInAdapter = indexInAdapter
|
||||||
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false))
|
binding.messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false))
|
||||||
messageContentView.delegate = contentViewDelegate
|
binding.messageContentView.delegate = contentViewDelegate
|
||||||
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
|
onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||||
@ -207,29 +223,31 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateExpirationTimer(message: MessageRecord) {
|
private fun updateExpirationTimer(message: MessageRecord) {
|
||||||
val expirationTimerViewLayoutParams = expirationTimerView.layoutParams as RelativeLayout.LayoutParams
|
val expirationTimerViewLayoutParams = binding.expirationTimerView.layoutParams as MarginLayoutParams
|
||||||
val ruleToAdd = if (message.isOutgoing) RelativeLayout.ALIGN_START else RelativeLayout.ALIGN_END
|
val container = binding.expirationTimerViewContainer
|
||||||
val ruleToRemove = if (message.isOutgoing) RelativeLayout.ALIGN_END else RelativeLayout.ALIGN_START
|
val content = binding.messageContentView
|
||||||
expirationTimerViewLayoutParams.removeRule(ruleToRemove)
|
val expiration = binding.expirationTimerView
|
||||||
expirationTimerViewLayoutParams.addRule(ruleToAdd, R.id.messageContentView)
|
container.removeAllViewsInLayout()
|
||||||
|
container.addView(if (message.isOutgoing) expiration else content)
|
||||||
|
container.addView(if (message.isOutgoing) content else expiration)
|
||||||
val expirationTimerViewSize = toPx(12, resources)
|
val expirationTimerViewSize = toPx(12, resources)
|
||||||
val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt()
|
val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt()
|
||||||
expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0
|
expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0
|
||||||
expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize)
|
expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize)
|
||||||
expirationTimerView.layoutParams = expirationTimerViewLayoutParams
|
binding.expirationTimerView.layoutParams = expirationTimerViewLayoutParams
|
||||||
if (message.expiresIn > 0 && !message.isPending) {
|
if (message.expiresIn > 0 && !message.isPending) {
|
||||||
expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
|
binding.expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
|
||||||
expirationTimerView.isVisible = true
|
binding.expirationTimerView.isVisible = true
|
||||||
expirationTimerView.setPercentComplete(0.0f)
|
binding.expirationTimerView.setPercentComplete(0.0f)
|
||||||
if (message.expireStarted > 0) {
|
if (message.expireStarted > 0) {
|
||||||
expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
||||||
expirationTimerView.startAnimation()
|
binding.expirationTimerView.startAnimation()
|
||||||
if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) {
|
if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) {
|
||||||
ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule()
|
ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule()
|
||||||
}
|
}
|
||||||
} else if (!message.isMediaPending) {
|
} else if (!message.isMediaPending) {
|
||||||
expirationTimerView.setPercentComplete(0.0f)
|
binding.expirationTimerView.setPercentComplete(0.0f)
|
||||||
expirationTimerView.stopAnimation()
|
binding.expirationTimerView.stopAnimation()
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager
|
val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager
|
||||||
val id = message.getId()
|
val id = message.getId()
|
||||||
@ -238,12 +256,13 @@ class VisibleMessageView : LinearLayout {
|
|||||||
expirationManager.scheduleDeletion(id, mms, message.expiresIn)
|
expirationManager.scheduleDeletion(id, mms, message.expiresIn)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
expirationTimerView.stopAnimation()
|
binding.expirationTimerView.stopAnimation()
|
||||||
expirationTimerView.setPercentComplete(0.0f)
|
binding.expirationTimerView.setPercentComplete(0.0f)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
expirationTimerView.isVisible = false
|
binding.expirationTimerView.isVisible = false
|
||||||
}
|
}
|
||||||
|
container.requestLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIsSelectedChanged() {
|
private fun handleIsSelectedChanged() {
|
||||||
@ -255,14 +274,14 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
if (translationX < 0 && !expirationTimerView.isVisible) {
|
if (translationX < 0 && !binding.expirationTimerView.isVisible) {
|
||||||
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
||||||
val threshold = VisibleMessageView.swipeToReplyThreshold
|
val threshold = swipeToReplyThreshold
|
||||||
val iconSize = toPx(24, context.resources)
|
val iconSize = toPx(24, context.resources)
|
||||||
val bottomVOffset = paddingBottom + messageStatusImageView.height + (messageContentView.height - iconSize) / 2
|
val bottomVOffset = paddingBottom + binding.messageStatusImageView.height + (binding.messageContentView.height - iconSize) / 2
|
||||||
swipeToReplyIconRect.left = messageContentContainer.right - messageContentContainer.paddingEnd + spacing
|
swipeToReplyIconRect.left = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + spacing
|
||||||
swipeToReplyIconRect.top = height - bottomVOffset - iconSize
|
swipeToReplyIconRect.top = height - bottomVOffset - iconSize
|
||||||
swipeToReplyIconRect.right = messageContentContainer.right - messageContentContainer.paddingEnd + iconSize + spacing
|
swipeToReplyIconRect.right = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + iconSize + spacing
|
||||||
swipeToReplyIconRect.bottom = height - bottomVOffset
|
swipeToReplyIconRect.bottom = height - bottomVOffset
|
||||||
swipeToReplyIcon.bounds = swipeToReplyIconRect
|
swipeToReplyIcon.bounds = swipeToReplyIconRect
|
||||||
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
|
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
|
||||||
@ -274,8 +293,8 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
profilePictureView.recycle()
|
binding.profilePictureView.recycle()
|
||||||
messageContentView.recycle()
|
binding.messageContentView.recycle()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -296,13 +315,13 @@ class VisibleMessageView : LinearLayout {
|
|||||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||||
val newLongPressCallback = Runnable { onLongPress() }
|
val newLongPressCallback = Runnable { onLongPress() }
|
||||||
this.longPressCallback = newLongPressCallback
|
this.longPressCallback = newLongPressCallback
|
||||||
gestureHandler.postDelayed(newLongPressCallback, VisibleMessageView.longPressDurationThreshold)
|
gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold)
|
||||||
onDownTimestamp = Date().time
|
onDownTimestamp = Date().time
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onMove(event: MotionEvent) {
|
private fun onMove(event: MotionEvent) {
|
||||||
val translationX = toDp(event.rawX + dx, context.resources)
|
val translationX = toDp(event.rawX + dx, context.resources)
|
||||||
if (abs(translationX) < VisibleMessageView.longPressMovementTreshold || snIsSelected) {
|
if (abs(translationX) < longPressMovementThreshold || snIsSelected) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||||
@ -313,20 +332,16 @@ class VisibleMessageView : LinearLayout {
|
|||||||
val sign = -1.0f
|
val sign = -1.0f
|
||||||
val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign
|
val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign
|
||||||
this.translationX = x
|
this.translationX = x
|
||||||
this.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving
|
binding.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving
|
||||||
postInvalidate() // Ensure onDraw(canvas:) is called
|
postInvalidate() // Ensure onDraw(canvas:) is called
|
||||||
if (abs(x) > VisibleMessageView.swipeToReplyThreshold && abs(previousTranslationX) < VisibleMessageView.swipeToReplyThreshold) {
|
if (abs(x) > swipeToReplyThreshold && abs(previousTranslationX) < swipeToReplyThreshold) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||||
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
|
||||||
} else {
|
|
||||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
previousTranslationX = x
|
previousTranslationX = x
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCancel(event: MotionEvent) {
|
private fun onCancel(event: MotionEvent) {
|
||||||
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) {
|
if (abs(translationX) > swipeToReplyThreshold) {
|
||||||
onSwipeToReply?.invoke()
|
onSwipeToReply?.invoke()
|
||||||
}
|
}
|
||||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||||
@ -334,9 +349,9 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onUp(event: MotionEvent) {
|
private fun onUp(event: MotionEvent) {
|
||||||
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) {
|
if (abs(translationX) > swipeToReplyThreshold) {
|
||||||
onSwipeToReply?.invoke()
|
onSwipeToReply?.invoke()
|
||||||
} else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) {
|
} else if ((Date().time - onDownTimestamp) < longPressDurationThreshold) {
|
||||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||||
val pressCallback = this.pressCallback
|
val pressCallback = this.pressCallback
|
||||||
if (pressCallback != null) {
|
if (pressCallback != null) {
|
||||||
@ -363,7 +378,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
.start()
|
.start()
|
||||||
// Bit of a hack to keep the date break text view from moving
|
// Bit of a hack to keep the date break text view from moving
|
||||||
dateBreakTextView.animate()
|
binding.dateBreakTextView.animate()
|
||||||
.translationX(0.0f)
|
.translationX(0.0f)
|
||||||
.setDuration(150)
|
.setDuration(150)
|
||||||
.start()
|
.start()
|
||||||
@ -375,7 +390,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onContentClick(event: MotionEvent) {
|
fun onContentClick(event: MotionEvent) {
|
||||||
messageContentView.onContentClick?.invoke(event)
|
binding.messageContentView.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPress(event: MotionEvent) {
|
private fun onPress(event: MotionEvent) {
|
||||||
@ -393,5 +408,9 @@ class VisibleMessageView : LinearLayout {
|
|||||||
val activity = context as AppCompatActivity
|
val activity = context as AppCompatActivity
|
||||||
userDetailsBottomSheet.show(activity.supportFragmentManager, userDetailsBottomSheet.tag)
|
userDetailsBottomSheet.show(activity.supportFragmentManager, userDetailsBottomSheet.tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun playVoiceMessage() {
|
||||||
|
binding.messageContentView.playVoiceMessage()
|
||||||
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,8 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.android.synthetic.main.view_voice_message.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewVoiceMessageBinding
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
|
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
|
||||||
import org.thoughtcrime.securesms.components.CornerMask
|
import org.thoughtcrime.securesms.components.CornerMask
|
||||||
@ -26,6 +26,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
|||||||
|
|
||||||
@Inject lateinit var attachmentDb: AttachmentDatabase
|
@Inject lateinit var attachmentDb: AttachmentDatabase
|
||||||
|
|
||||||
|
private lateinit var binding: ViewVoiceMessageBinding
|
||||||
private val cornerMask by lazy { CornerMask(this) }
|
private val cornerMask by lazy { CornerMask(this) }
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -44,8 +45,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_voice_message, this)
|
binding = ViewVoiceMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||||
TimeUnit.MILLISECONDS.toMinutes(0),
|
TimeUnit.MILLISECONDS.toMinutes(0),
|
||||||
TimeUnit.MILLISECONDS.toSeconds(0))
|
TimeUnit.MILLISECONDS.toSeconds(0))
|
||||||
}
|
}
|
||||||
@ -54,7 +55,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||||
val audio = message.slideDeck.audioSlide!!
|
val audio = message.slideDeck.audioSlide!!
|
||||||
voiceMessageViewLoader.isVisible = audio.isInProgress
|
binding.voiceMessageViewLoader.isVisible = audio.isInProgress
|
||||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||||
cornerMask.setTopLeftRadius(cornerRadii[0])
|
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||||
cornerMask.setTopRightRadius(cornerRadii[1])
|
cornerMask.setTopRightRadius(cornerRadii[1])
|
||||||
@ -74,8 +75,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
|||||||
attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras ->
|
attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras ->
|
||||||
if (audioExtras.durationMs > 0) {
|
if (audioExtras.durationMs > 0) {
|
||||||
duration = audioExtras.durationMs
|
duration = audioExtras.durationMs
|
||||||
voiceMessageViewDurationTextView.visibility = View.VISIBLE
|
binding.voiceMessageViewDurationTextView.visibility = View.VISIBLE
|
||||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||||
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
|
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
|
||||||
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs))
|
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs))
|
||||||
}
|
}
|
||||||
@ -99,12 +100,12 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
|||||||
|
|
||||||
private fun handleProgressChanged(progress: Double) {
|
private fun handleProgressChanged(progress: Double) {
|
||||||
this.progress = progress
|
this.progress = progress
|
||||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||||
TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()),
|
TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()),
|
||||||
TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong()))
|
TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong()))
|
||||||
val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams
|
val layoutParams = binding.progressView.layoutParams as RelativeLayout.LayoutParams
|
||||||
layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
|
layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
|
||||||
progressView.layoutParams = layoutParams
|
binding.progressView.layoutParams = layoutParams
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayerStop(player: AudioSlidePlayer) {
|
override fun onPlayerStop(player: AudioSlidePlayer) {
|
||||||
@ -118,7 +119,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
|||||||
|
|
||||||
private fun renderIcon() {
|
private fun renderIcon() {
|
||||||
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
|
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
|
||||||
voiceMessagePlaybackImageView.setImageResource(iconID)
|
binding.voiceMessagePlaybackImageView.setImageResource(iconID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
@ -5,11 +5,12 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import kotlinx.android.synthetic.main.view_search_bottom_bar.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewSearchBottomBarBinding
|
||||||
|
|
||||||
|
|
||||||
class SearchBottomBar : LinearLayout {
|
class SearchBottomBar : LinearLayout {
|
||||||
|
private lateinit var binding: ViewSearchBottomBarBinding
|
||||||
private var eventListener: EventListener? = null
|
private var eventListener: EventListener? = null
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
@ -18,10 +19,10 @@ class SearchBottomBar : LinearLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
fun initialize() {
|
fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_search_bottom_bar, this)
|
binding = ViewSearchBottomBarBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setData(position: Int, count: Int) {
|
fun setData(position: Int, count: Int) = with(binding) {
|
||||||
searchProgressWheel.visibility = GONE
|
searchProgressWheel.visibility = GONE
|
||||||
searchUp.setOnClickListener { v: View? ->
|
searchUp.setOnClickListener { v: View? ->
|
||||||
if (eventListener != null) {
|
if (eventListener != null) {
|
||||||
@ -43,7 +44,7 @@ class SearchBottomBar : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showLoading() {
|
fun showLoading() {
|
||||||
searchProgressWheel.visibility = VISIBLE
|
binding.searchProgressWheel.visibility = VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setViewEnabled(view: View, enabled: Boolean) {
|
private fun setViewEnabled(view: View, enabled: Boolean) {
|
||||||
|
@ -11,6 +11,7 @@ import org.session.libsession.utilities.concurrent.SignalExecutors
|
|||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor
|
import org.thoughtcrime.securesms.contacts.ContactAccessor
|
||||||
import org.thoughtcrime.securesms.database.CursorList
|
import org.thoughtcrime.securesms.database.CursorList
|
||||||
import org.thoughtcrime.securesms.database.SearchDatabase
|
import org.thoughtcrime.securesms.database.SearchDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.search.SearchRepository
|
import org.thoughtcrime.securesms.search.SearchRepository
|
||||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||||
@ -20,14 +21,11 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SearchViewModel @Inject constructor(
|
class SearchViewModel @Inject constructor(
|
||||||
@ApplicationContext context: Context,
|
private val searchRepository: SearchRepository
|
||||||
searchDb: SearchDatabase,
|
|
||||||
threadDb: ThreadDatabase
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val searchRepository: SearchRepository
|
private val result: CloseableLiveData<SearchResult> = CloseableLiveData()
|
||||||
private val result: CloseableLiveData<SearchResult>
|
private val debouncer: Debouncer = Debouncer(500)
|
||||||
private val debouncer: Debouncer
|
|
||||||
private var firstSearch = false
|
private var firstSearch = false
|
||||||
private var searchOpen = false
|
private var searchOpen = false
|
||||||
private var activeQuery: String? = null
|
private var activeQuery: String? = null
|
||||||
@ -107,13 +105,4 @@ class SearchViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
result = CloseableLiveData()
|
|
||||||
debouncer = Debouncer(500)
|
|
||||||
searchRepository = SearchRepository(context,
|
|
||||||
searchDb,
|
|
||||||
threadDb,
|
|
||||||
ContactAccessor.getInstance(),
|
|
||||||
SignalExecutors.SERIAL)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@ -13,8 +14,8 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
|||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import kotlinx.android.synthetic.main.thumbnail_view.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ThumbnailViewBinding
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||||
import org.session.libsession.utilities.Util.equals
|
import org.session.libsession.utilities.Util.equals
|
||||||
import org.session.libsignal.utilities.ListenableFuture
|
import org.session.libsignal.utilities.ListenableFuture
|
||||||
@ -22,11 +23,13 @@ import org.session.libsignal.utilities.SettableFuture
|
|||||||
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
|
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
|
||||||
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
|
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.mms.*
|
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequest
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
|
|
||||||
open class KThumbnailView: FrameLayout {
|
open class KThumbnailView: FrameLayout {
|
||||||
|
private lateinit var binding: ThumbnailViewBinding
|
||||||
companion object {
|
companion object {
|
||||||
private const val WIDTH = 0
|
private const val WIDTH = 0
|
||||||
private const val HEIGHT = 1
|
private const val HEIGHT = 1
|
||||||
@ -37,10 +40,10 @@ open class KThumbnailView: FrameLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
||||||
|
|
||||||
private val image by lazy { thumbnail_image }
|
private val image by lazy { binding.thumbnailImage }
|
||||||
private val playOverlay by lazy { play_overlay }
|
private val playOverlay by lazy { binding.playOverlay }
|
||||||
val loadIndicator: View by lazy { thumbnail_load_indicator }
|
val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
|
||||||
val downloadIndicator: View by lazy { thumbnail_download_icon }
|
val downloadIndicator: View by lazy { binding.thumbnailDownloadIcon }
|
||||||
|
|
||||||
private val dimensDelegate = ThumbnailDimensDelegate()
|
private val dimensDelegate = ThumbnailDimensDelegate()
|
||||||
|
|
||||||
@ -48,7 +51,7 @@ open class KThumbnailView: FrameLayout {
|
|||||||
private var radius: Int = 0
|
private var radius: Int = 0
|
||||||
|
|
||||||
private fun initialize(attrs: AttributeSet?) {
|
private fun initialize(attrs: AttributeSet?) {
|
||||||
inflate(context, R.layout.thumbnail_view, this)
|
binding = ThumbnailViewBinding.inflate(LayoutInflater.from(context), this)
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
|
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
|
||||||
|
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.utilities;
|
package org.thoughtcrime.securesms.conversation.v2.utilities;
|
||||||
|
|
||||||
|
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.UiThread;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
import com.bumptech.glide.RequestBuilder;
|
import com.bumptech.glide.RequestBuilder;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
||||||
@ -22,8 +22,13 @@ import com.bumptech.glide.load.resource.bitmap.FitCenter;
|
|||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||||
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
|
import org.session.libsignal.utilities.ListenableFuture;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.session.libsignal.utilities.SettableFuture;
|
||||||
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget;
|
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget;
|
||||||
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget;
|
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget;
|
||||||
import org.thoughtcrime.securesms.components.TransferControlView;
|
import org.thoughtcrime.securesms.components.TransferControlView;
|
||||||
@ -33,17 +38,11 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
|||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsignal.utilities.ListenableFuture;
|
|
||||||
import org.session.libsignal.utilities.SettableFuture;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class ThumbnailView extends FrameLayout {
|
public class ThumbnailView extends FrameLayout {
|
||||||
|
|
||||||
@ -287,7 +286,7 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
} else if (slide.hasPlaceholder()) {
|
} else if (slide.hasPlaceholder()) {
|
||||||
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(image, result));
|
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(image, result));
|
||||||
} else {
|
} else {
|
||||||
glideRequests.clear(image);
|
glideRequests.load(R.drawable.ic_image_white_24dp).centerInside().into(image);
|
||||||
result.set(false);
|
result.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ import org.session.libsignal.database.LokiOpenGroupDatabaseProtocol;
|
|||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -111,7 +112,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<GroupRecord> getGroup(Cursor cursor) {
|
public Optional<GroupRecord> getGroup(Cursor cursor) {
|
||||||
Reader reader = new Reader(cursor);
|
Reader reader = new Reader(cursor);
|
||||||
return Optional.fromNullable(reader.getCurrent());
|
return Optional.fromNullable(reader.getCurrent());
|
||||||
}
|
}
|
||||||
@ -146,6 +147,29 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Cursor getGroupsFilteredByMembers(List<String> members) {
|
||||||
|
if (members == null || members.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] queriesValues = new String[members.size()];
|
||||||
|
|
||||||
|
StringBuilder queries = new StringBuilder();
|
||||||
|
for (int i=0; i < members.size(); i++) {
|
||||||
|
boolean isEnd = i == (members.size() - 1);
|
||||||
|
queries.append(MEMBERS + " LIKE ?");
|
||||||
|
queriesValues[i] = "%"+members.get(i)+"%";
|
||||||
|
if (!isEnd) {
|
||||||
|
queries.append(" OR ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return databaseHelper.getReadableDatabase().query(TABLE_NAME, null,
|
||||||
|
queries.toString(),
|
||||||
|
queriesValues,
|
||||||
|
null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
|
public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
|
||||||
List<Address> members = getCurrentMembers(groupId, false);
|
List<Address> members = getCurrentMembers(groupId, false);
|
||||||
List<Recipient> recipients = new LinkedList<>();
|
List<Recipient> recipients = new LinkedList<>();
|
||||||
|
@ -450,7 +450,7 @@ private inline fun <reified T> wrap(x: T): Array<T> {
|
|||||||
|
|
||||||
private fun wrap(x: Map<String, String>): ContentValues {
|
private fun wrap(x: Map<String, String>): ContentValues {
|
||||||
val result = ContentValues(x.size)
|
val result = ContentValues(x.size)
|
||||||
x.forEach { result.put(it.key, it.value) }
|
x.iterator().forEach { result.put(it.key, it.value) }
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
@ -139,7 +139,7 @@ public class MmsSmsDatabase extends Database {
|
|||||||
|
|
||||||
try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) {
|
try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) {
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
return cursor.getLong(cursor.getColumnIndex(MmsSmsColumns.ID));
|
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ public class MmsSmsDatabase extends Database {
|
|||||||
try {
|
try {
|
||||||
return cursor != null ? cursor.getCount() : 0;
|
return cursor != null ? cursor.getCount() : 0;
|
||||||
} finally {
|
} finally {
|
||||||
if (cursor != null) cursor.close();;
|
if (cursor != null) cursor.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
@ -8,8 +9,8 @@ import com.annimon.stream.Stream;
|
|||||||
import net.sqlcipher.Cursor;
|
import net.sqlcipher.Cursor;
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -80,7 +81,7 @@ public class SearchDatabase extends Database {
|
|||||||
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
|
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
|
||||||
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " +
|
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " +
|
||||||
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
|
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
|
||||||
"LIMIT 500";
|
"LIMIT ?";
|
||||||
|
|
||||||
private static final String MESSAGES_FOR_THREAD_QUERY =
|
private static final String MESSAGES_FOR_THREAD_QUERY =
|
||||||
"SELECT " +
|
"SELECT " +
|
||||||
@ -115,7 +116,9 @@ public class SearchDatabase extends Database {
|
|||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
String prefixQuery = adjustQuery(query);
|
String prefixQuery = adjustQuery(query);
|
||||||
|
|
||||||
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery });
|
int queryLimit = Math.min(query.length()*50,500);
|
||||||
|
|
||||||
|
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery, String.valueOf(queryLimit) });
|
||||||
setNotifyConverationListListeners(cursor);
|
setNotifyConverationListListeners(cursor);
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
|
|||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
import net.sqlcipher.Cursor
|
import net.sqlcipher.Cursor
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
@ -73,7 +74,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||||||
notifyConversationListListeners()
|
notifyConversationListListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun contactFromCursor(cursor: Cursor): Contact {
|
fun contactFromCursor(cursor: Cursor): Contact {
|
||||||
val sessionID = cursor.getString(sessionID)
|
val sessionID = cursor.getString(sessionID)
|
||||||
val contact = Contact(sessionID)
|
val contact = Contact(sessionID)
|
||||||
contact.name = cursor.getStringOrNull(name)
|
contact.name = cursor.getStringOrNull(name)
|
||||||
@ -87,4 +88,29 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||||||
contact.isTrusted = cursor.getInt(isTrusted) != 0
|
contact.isTrusted = cursor.getInt(isTrusted) != 0
|
||||||
return contact
|
return contact
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun contactFromCursor(cursor: android.database.Cursor): Contact {
|
||||||
|
val sessionID = cursor.getString(cursor.getColumnIndexOrThrow(sessionID))
|
||||||
|
val contact = Contact(sessionID)
|
||||||
|
contact.name = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(name))
|
||||||
|
contact.nickname = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(nickname))
|
||||||
|
contact.profilePictureURL = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureURL))
|
||||||
|
contact.profilePictureFileName = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureFileName))
|
||||||
|
cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureEncryptionKey))?.let {
|
||||||
|
contact.profilePictureEncryptionKey = Base64.decode(it)
|
||||||
|
}
|
||||||
|
contact.threadID = cursor.getLong(cursor.getColumnIndexOrThrow(threadID))
|
||||||
|
contact.isTrusted = cursor.getInt(cursor.getColumnIndexOrThrow(isTrusted)) != 0
|
||||||
|
return contact
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryContactsByName(constraint: String): Cursor {
|
||||||
|
return databaseHelper.readableDatabase.query(
|
||||||
|
sessionContactTable, null, " $name LIKE ? OR $nickname LIKE ?", arrayOf(
|
||||||
|
"%$constraint%",
|
||||||
|
"%$constraint%"
|
||||||
|
),
|
||||||
|
null, null, null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
@ -45,6 +45,7 @@ import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
|
|||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.session.libsignal.utilities.Pair;
|
import org.session.libsignal.utilities.Pair;
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
@ -55,6 +56,7 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
|||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
|
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
|
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
@ -337,6 +339,19 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Cursor searchConversationAddresses(String addressQuery) {
|
||||||
|
if (addressQuery == null || addressQuery.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
String selection = TABLE_NAME + "." + ADDRESS + " LIKE ? AND " + TABLE_NAME + "." + MESSAGE_COUNT + " != 0";
|
||||||
|
String[] selectionArgs = new String[]{addressQuery+"%"};
|
||||||
|
String query = createQuery(selection, 0);
|
||||||
|
Cursor cursor = db.rawQuery(query, selectionArgs);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
public Cursor getFilteredConversationList(@Nullable List<Address> filter) {
|
public Cursor getFilteredConversationList(@Nullable List<Address> filter) {
|
||||||
if (filter == null || filter.size() == 0)
|
if (filter == null || filter.size() == 0)
|
||||||
return null;
|
return null;
|
||||||
@ -593,6 +608,18 @@ public class ThreadDatabase extends Database {
|
|||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void markAllAsRead(long threadId, boolean isGroupRecipient) {
|
||||||
|
List<MarkedMessageInfo> messages = setRead(threadId, true);
|
||||||
|
if (isGroupRecipient) {
|
||||||
|
for (MarkedMessageInfo message: messages) {
|
||||||
|
MarkReadReceiver.scheduleDeletion(context, message.getExpirationInfo());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MarkReadReceiver.process(context, messages);
|
||||||
|
}
|
||||||
|
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, false, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean deleteThreadOnEmpty(long threadId) {
|
private boolean deleteThreadOnEmpty(long threadId) {
|
||||||
Recipient threadRecipient = getRecipientForThreadId(threadId);
|
Recipient threadRecipient = getRecipientForThreadId(threadId);
|
||||||
return threadRecipient != null && !threadRecipient.isOpenGroupRecipient();
|
return threadRecipient != null && !threadRecipient.isOpenGroupRecipient();
|
||||||
@ -692,14 +719,14 @@ public class ThreadDatabase extends Database {
|
|||||||
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
|
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
|
||||||
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT));
|
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT));
|
||||||
long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE));
|
long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE));
|
||||||
boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0;
|
boolean archived = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.ARCHIVED)) != 0;
|
||||||
int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS));
|
int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS));
|
||||||
int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DELIVERY_RECEIPT_COUNT));
|
int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DELIVERY_RECEIPT_COUNT));
|
||||||
int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ_RECEIPT_COUNT));
|
int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ_RECEIPT_COUNT));
|
||||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN));
|
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN));
|
||||||
long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN));
|
long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN));
|
||||||
Uri snippetUri = getSnippetUri(cursor);
|
Uri snippetUri = getSnippetUri(cursor);
|
||||||
boolean pinned = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.IS_PINNED)) != 0;
|
boolean pinned = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.IS_PINNED)) != 0;
|
||||||
|
|
||||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
||||||
readReceiptCount = 0;
|
readReceiptCount = 0;
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package org.thoughtcrime.securesms.dependencies
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import org.session.libsession.utilities.AppTextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
|
import org.thoughtcrime.securesms.repository.DefaultConversationRepository
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
abstract class AppModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindTextSecurePreferences(preferences: AppTextSecurePreferences): TextSecurePreferences
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindConversationRepository(repository: DefaultConversationRepository): ConversationRepository
|
||||||
|
|
||||||
|
}
|
@ -9,16 +9,18 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
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.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentPagerAdapter
|
import androidx.fragment.app.FragmentPagerAdapter
|
||||||
import kotlinx.android.synthetic.main.activity_create_private_chat.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_enter_public_key.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivityCreatePrivateChatBinding
|
||||||
|
import network.loki.messenger.databinding.FragmentEnterPublicKeyBinding
|
||||||
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.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
@ -27,13 +29,13 @@ 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.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||||
|
|
||||||
class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||||
|
private lateinit var binding: ActivityCreatePrivateChatBinding
|
||||||
private val adapter = CreatePrivateChatActivityAdapter(this)
|
private val adapter = CreatePrivateChatActivityAdapter(this)
|
||||||
private var isKeyboardShowing = false
|
private var isKeyboardShowing = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -47,37 +49,36 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
|
|||||||
// 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)
|
||||||
|
binding = ActivityCreatePrivateChatBinding.inflate(layoutInflater)
|
||||||
// Set content view
|
// Set content view
|
||||||
setContentView(R.layout.activity_create_private_chat)
|
setContentView(binding.root)
|
||||||
// Set title
|
// Set title
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_create_private_chat_title)
|
supportActionBar!!.title = resources.getString(R.string.activity_create_private_chat_title)
|
||||||
// Set up view pager
|
// Set up view pager
|
||||||
viewPager.adapter = adapter
|
binding.viewPager.adapter = adapter
|
||||||
tabLayout.setupWithViewPager(viewPager)
|
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||||
rootLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
binding.rootLayout.viewTreeObserver.addOnGlobalLayoutListener {
|
||||||
|
val diff = binding.rootLayout.rootView.height - binding.rootLayout.height
|
||||||
override fun onGlobalLayout() {
|
val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics
|
||||||
val diff = rootLayout.rootView.height - rootLayout.height
|
val estimatedKeyboardHeight =
|
||||||
val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics)
|
||||||
val estimatedKeyboardHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics)
|
this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight)
|
||||||
this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight)
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
private fun showLoader() {
|
private fun showLoader() {
|
||||||
loader.visibility = View.VISIBLE
|
binding.loader.visibility = View.VISIBLE
|
||||||
loader.animate().setDuration(150).alpha(1.0f).start()
|
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideLoader() {
|
private fun hideLoader() {
|
||||||
loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
super.onAnimationEnd(animation)
|
super.onAnimationEnd(animation)
|
||||||
loader.visibility = View.GONE
|
binding.loader.visibility = View.GONE
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -156,6 +157,8 @@ private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatAc
|
|||||||
|
|
||||||
// region Enter Public Key Fragment
|
// region Enter Public Key Fragment
|
||||||
class EnterPublicKeyFragment : Fragment() {
|
class EnterPublicKeyFragment : Fragment() {
|
||||||
|
private lateinit var binding: FragmentEnterPublicKeyBinding
|
||||||
|
|
||||||
var isKeyboardShowing = false
|
var isKeyboardShowing = false
|
||||||
set(value) { field = value; handleIsKeyboardShowingChanged() }
|
set(value) { field = value; handleIsKeyboardShowingChanged() }
|
||||||
|
|
||||||
@ -165,32 +168,34 @@ class EnterPublicKeyFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return inflater.inflate(R.layout.fragment_enter_public_key, container, false)
|
binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
with(binding) {
|
||||||
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||||
publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
|
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
createPrivateChatIfPossible()
|
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||||
true
|
createPrivateChatIfPossible()
|
||||||
} else {
|
true
|
||||||
false
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
publicKeyTextView.text = hexEncodedPublicKey
|
||||||
|
copyButton.setOnClickListener { copyPublicKey() }
|
||||||
|
shareButton.setOnClickListener { sharePublicKey() }
|
||||||
|
createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() }
|
||||||
}
|
}
|
||||||
publicKeyTextView.text = hexEncodedPublicKey
|
|
||||||
copyButton.setOnClickListener { copyPublicKey() }
|
|
||||||
shareButton.setOnClickListener { sharePublicKey() }
|
|
||||||
createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIsKeyboardShowingChanged() {
|
private fun handleIsKeyboardShowingChanged() {
|
||||||
val optionalContentContainer = optionalContentContainer ?: return
|
binding.optionalContentContainer.isVisible = !isKeyboardShowing
|
||||||
optionalContentContainer.isVisible = !isKeyboardShowing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyPublicKey() {
|
private fun copyPublicKey() {
|
||||||
@ -209,7 +214,7 @@ class EnterPublicKeyFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createPrivateChatIfPossible() {
|
private fun createPrivateChatIfPossible() {
|
||||||
val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString()
|
val hexEncodedPublicKey = binding.publicKeyEditText.text?.trim().toString()
|
||||||
val activity = requireActivity() as CreatePrivateChatActivity
|
val activity = requireActivity() as CreatePrivateChatActivity
|
||||||
activity.createPrivateChatIfPossible(hexEncodedPublicKey)
|
activity.createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
package org.thoughtcrime.securesms.groups
|
package org.thoughtcrime.securesms.groups
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
||||||
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 kotlinx.android.synthetic.main.fragment_closed_group_edit_bottom_sheet.*
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.databinding.FragmentClosedGroupEditBottomSheetBinding
|
||||||
|
|
||||||
public class ClosedGroupEditingOptionsBottomSheet : BottomSheetDialogFragment() {
|
class ClosedGroupEditingOptionsBottomSheet : BottomSheetDialogFragment() {
|
||||||
|
private lateinit var binding: FragmentClosedGroupEditBottomSheetBinding
|
||||||
var onRemoveTapped: (() -> Unit)? = null
|
var onRemoveTapped: (() -> Unit)? = null
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return inflater.inflate(R.layout.fragment_closed_group_edit_bottom_sheet, container, false)
|
binding = FragmentClosedGroupEditBottomSheetBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
removeFromGroup.setOnClickListener { onRemoveTapped?.invoke() }
|
binding.removeFromGroup.setOnClickListener { onRemoveTapped?.invoke() }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,8 +10,8 @@ import android.widget.Toast
|
|||||||
import androidx.loader.app.LoaderManager
|
import androidx.loader.app.LoaderManager
|
||||||
import androidx.loader.content.Loader
|
import androidx.loader.content.Loader
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import kotlinx.android.synthetic.main.activity_create_closed_group.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivityCreateClosedGroupBinding
|
||||||
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.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
@ -28,8 +28,8 @@ import org.thoughtcrime.securesms.mms.GlideApp
|
|||||||
import org.thoughtcrime.securesms.util.fadeIn
|
import org.thoughtcrime.securesms.util.fadeIn
|
||||||
import org.thoughtcrime.securesms.util.fadeOut
|
import org.thoughtcrime.securesms.util.fadeOut
|
||||||
|
|
||||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
|
||||||
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||||
|
private lateinit var binding: ActivityCreateClosedGroupBinding
|
||||||
private var isLoading = false
|
private var isLoading = false
|
||||||
set(newValue) { field = newValue; invalidateOptionsMenu() }
|
set(newValue) { field = newValue; invalidateOptionsMenu() }
|
||||||
private var members = listOf<String>()
|
private var members = listOf<String>()
|
||||||
@ -50,11 +50,12 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
|||||||
// 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)
|
||||||
setContentView(R.layout.activity_create_closed_group)
|
binding = ActivityCreateClosedGroupBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_create_closed_group_title)
|
supportActionBar!!.title = resources.getString(R.string.activity_create_closed_group_title)
|
||||||
recyclerView.adapter = this.selectContactsAdapter
|
binding.recyclerView.adapter = this.selectContactsAdapter
|
||||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,8 +81,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
|||||||
private fun update(members: List<String>) {
|
private fun update(members: List<String>) {
|
||||||
//if there is a Note to self conversation, it loads self in the list, so we need to remove it here
|
//if there is a Note to self conversation, it loads self in the list, so we need to remove it here
|
||||||
this.members = members.minus(publicKey)
|
this.members = members.minus(publicKey)
|
||||||
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||||
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
@ -95,12 +96,12 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createNewPrivateChat() {
|
private fun createNewPrivateChat() {
|
||||||
setResult(Companion.closedGroupCreatedResultCode)
|
setResult(closedGroupCreatedResultCode)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createClosedGroup() {
|
private fun createClosedGroup() {
|
||||||
val name = nameEditText.text.trim()
|
val name = binding.nameEditText.text.trim()
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
@ -116,9 +117,9 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
|||||||
}
|
}
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
|
||||||
isLoading = true
|
isLoading = true
|
||||||
loaderContainer.fadeIn()
|
binding.loaderContainer.fadeIn()
|
||||||
MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||||
loaderContainer.fadeOut()
|
binding.loaderContainer.fadeOut()
|
||||||
isLoading = false
|
isLoading = false
|
||||||
val threadID = DatabaseComponent.get(this).threadDatabase().getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
|
val threadID = DatabaseComponent.get(this).threadDatabase().getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||||
if (!isFinishing) {
|
if (!isFinishing) {
|
||||||
@ -126,7 +127,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}.failUi {
|
}.failUi {
|
||||||
loaderContainer.fadeOut()
|
binding.loaderContainer.fadeOut()
|
||||||
isLoading = false
|
isLoading = false
|
||||||
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
|
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,14 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
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.*
|
import android.widget.EditText
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.loader.app.LoaderManager
|
import androidx.loader.app.LoaderManager
|
||||||
import androidx.loader.content.Loader
|
import androidx.loader.content.Loader
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.synthetic.main.activity_settings.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.task
|
import nl.komponents.kovenant.task
|
||||||
|
@ -13,62 +13,63 @@ import android.view.inputmethod.InputMethodManager
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.*
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentPagerAdapter
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import kotlinx.android.synthetic.main.activity_join_public_chat.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
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
|
||||||
|
import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
|
||||||
|
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.PublicKeyValidation
|
import org.session.libsignal.utilities.PublicKeyValidation
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
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.groups.DefaultGroupsViewModel
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager
|
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
|
||||||
import org.thoughtcrime.securesms.util.State
|
import org.thoughtcrime.securesms.util.State
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||||
|
private lateinit var binding: ActivityJoinPublicChatBinding
|
||||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||||
|
|
||||||
// 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)
|
||||||
|
binding = ActivityJoinPublicChatBinding.inflate(layoutInflater)
|
||||||
// Set content view
|
// Set content view
|
||||||
setContentView(R.layout.activity_join_public_chat)
|
setContentView(binding.root)
|
||||||
// Set title
|
// Set title
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_join_public_chat_title)
|
supportActionBar!!.title = resources.getString(R.string.activity_join_public_chat_title)
|
||||||
// Set up view pager
|
// Set up view pager
|
||||||
viewPager.adapter = adapter
|
binding.viewPager.adapter = adapter
|
||||||
tabLayout.setupWithViewPager(viewPager)
|
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
private fun showLoader() {
|
private fun showLoader() {
|
||||||
loader.visibility = View.VISIBLE
|
binding.loader.visibility = View.VISIBLE
|
||||||
loader.animate().setDuration(150).alpha(1.0f).start()
|
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideLoader() {
|
private fun hideLoader() {
|
||||||
loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
super.onAnimationEnd(animation)
|
super.onAnimationEnd(animation)
|
||||||
loader.visibility = View.GONE
|
binding.loader.visibility = View.GONE
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -166,26 +167,28 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
|
|||||||
|
|
||||||
// region Enter Chat URL Fragment
|
// region Enter Chat URL Fragment
|
||||||
class EnterChatURLFragment : Fragment() {
|
class EnterChatURLFragment : Fragment() {
|
||||||
|
private lateinit var binding: FragmentEnterChatUrlBinding
|
||||||
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
|
binding = FragmentEnterChatUrlBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
binding.chatURLEditText.imeOptions = binding.chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||||
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
binding.joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||||
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
||||||
defaultRoomsContainer.isVisible = state is State.Success
|
binding.defaultRoomsContainer.isVisible = state is State.Success
|
||||||
defaultRoomsLoaderContainer.isVisible = state is State.Loading
|
binding.defaultRoomsLoaderContainer.isVisible = state is State.Loading
|
||||||
defaultRoomsLoader.isVisible = state is State.Loading
|
binding.defaultRoomsLoader.isVisible = state is State.Loading
|
||||||
when (state) {
|
when (state) {
|
||||||
State.Loading -> {
|
State.Loading -> {
|
||||||
// TODO: Show a loader
|
// TODO: Show a binding.loader
|
||||||
}
|
}
|
||||||
is State.Error -> {
|
is State.Error -> {
|
||||||
// TODO: Hide the loader
|
// TODO: Hide the binding.loader
|
||||||
}
|
}
|
||||||
is State.Success -> {
|
is State.Success -> {
|
||||||
populateDefaultGroups(state.value)
|
populateDefaultGroups(state.value)
|
||||||
@ -195,10 +198,10 @@ class EnterChatURLFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
|
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
|
||||||
defaultRoomsGridLayout.removeAllViews()
|
binding.defaultRoomsGridLayout.removeAllViews()
|
||||||
defaultRoomsGridLayout.useDefaultMargins = false
|
binding.defaultRoomsGridLayout.useDefaultMargins = false
|
||||||
groups.forEach { defaultGroup ->
|
groups.iterator().forEach { defaultGroup ->
|
||||||
val chip = layoutInflater.inflate(R.layout.default_group_chip, defaultRoomsGridLayout, false) as Chip
|
val chip = layoutInflater.inflate(R.layout.default_group_chip, binding.defaultRoomsGridLayout, false) as Chip
|
||||||
val drawable = defaultGroup.image?.let { bytes ->
|
val drawable = defaultGroup.image?.let { bytes ->
|
||||||
val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size)
|
val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size)
|
||||||
RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
|
RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
|
||||||
@ -210,18 +213,18 @@ class EnterChatURLFragment : Fragment() {
|
|||||||
chip.setOnClickListener {
|
chip.setOnClickListener {
|
||||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
|
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
|
||||||
}
|
}
|
||||||
defaultRoomsGridLayout.addView(chip)
|
binding.defaultRoomsGridLayout.addView(chip)
|
||||||
}
|
}
|
||||||
if ((groups.size and 1) != 0) { // This checks that the number of rooms is even
|
if ((groups.size and 1) != 0) { // This checks that the number of rooms is even
|
||||||
layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout)
|
layoutInflater.inflate(R.layout.grid_layout_filler, binding.defaultRoomsGridLayout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Convenience
|
// region Convenience
|
||||||
private fun joinPublicChatIfPossible() {
|
private fun joinPublicChatIfPossible() {
|
||||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
|
inputMethodManager.hideSoftInputFromWindow(binding.chatURLEditText.windowToken, 0)
|
||||||
val chatURL = chatURLEditText.text.trim().toString().toLowerCase(Locale.US)
|
val chatURL = binding.chatURLEditText.text.trim().toString().toLowerCase(Locale.US)
|
||||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
package org.thoughtcrime.securesms.groups
|
package org.thoughtcrime.securesms.groups
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import kotlinx.android.synthetic.main.activity_open_group_guidelines.*
|
import network.loki.messenger.databinding.ActivityOpenGroupGuidelinesBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
|
|
||||||
class OpenGroupGuidelinesActivity : BaseActionBarActivity() {
|
class OpenGroupGuidelinesActivity : BaseActionBarActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_open_group_guidelines)
|
val binding = ActivityOpenGroupGuidelinesBinding.inflate(layoutInflater)
|
||||||
communityGuidelinesTextView.text = """
|
setContentView(binding.root)
|
||||||
|
binding.communityGuidelinesTextView.text = """
|
||||||
Welcome to Oxen.
|
Welcome to Oxen.
|
||||||
|
|
||||||
Oxen believes privacy is an important part of our future. People have been safeguarding the right to privacy since the dawn of humanity, but the digital world has turned privacy into a privilege. Enough is enough. We're taking it back. For you. For us. For everyone.
|
Oxen believes privacy is an important part of our future. People have been safeguarding the right to privacy since the dawn of humanity, but the digital world has turned privacy into a privilege. Enough is enough. We're taking it back. For you. For us. For everyone.
|
||||||
|
@ -6,13 +6,12 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.*
|
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
|
|
||||||
public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
|
class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
|
||||||
|
private lateinit var binding: FragmentConversationBottomSheetBinding
|
||||||
//FIXME AC: Supplying a threadRecord directly into the field from an activity
|
//FIXME AC: Supplying a threadRecord directly into the field from an activity
|
||||||
// is not the best idea. It doesn't survive configuration change.
|
// is not the best idea. It doesn't survive configuration change.
|
||||||
// We should be dealing with IDs and all sorts of serializable data instead
|
// We should be dealing with IDs and all sorts of serializable data instead
|
||||||
@ -25,24 +24,27 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.
|
|||||||
var onBlockTapped: (() -> Unit)? = null
|
var onBlockTapped: (() -> Unit)? = null
|
||||||
var onUnblockTapped: (() -> Unit)? = null
|
var onUnblockTapped: (() -> Unit)? = null
|
||||||
var onDeleteTapped: (() -> Unit)? = null
|
var onDeleteTapped: (() -> Unit)? = null
|
||||||
|
var onMarkAllAsReadTapped: (() -> Unit)? = null
|
||||||
var onNotificationTapped: (() -> Unit)? = null
|
var onNotificationTapped: (() -> Unit)? = null
|
||||||
var onSetMuteTapped: ((Boolean) -> Unit)? = null
|
var onSetMuteTapped: ((Boolean) -> Unit)? = null
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.fragment_conversation_bottom_sheet, container, false)
|
binding = FragmentConversationBottomSheetBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View?) {
|
override fun onClick(v: View?) {
|
||||||
when (v) {
|
when (v) {
|
||||||
detailsTextView -> onViewDetailsTapped?.invoke()
|
binding.detailsTextView -> onViewDetailsTapped?.invoke()
|
||||||
pinTextView -> onPinTapped?.invoke()
|
binding.pinTextView -> onPinTapped?.invoke()
|
||||||
unpinTextView -> onUnpinTapped?.invoke()
|
binding.unpinTextView -> onUnpinTapped?.invoke()
|
||||||
blockTextView -> onBlockTapped?.invoke()
|
binding.blockTextView -> onBlockTapped?.invoke()
|
||||||
unblockTextView -> onUnblockTapped?.invoke()
|
binding.unblockTextView -> onUnblockTapped?.invoke()
|
||||||
deleteTextView -> onDeleteTapped?.invoke()
|
binding.deleteTextView -> onDeleteTapped?.invoke()
|
||||||
notificationsTextView -> onNotificationTapped?.invoke()
|
binding.markAllAsReadTextView -> onMarkAllAsReadTapped?.invoke()
|
||||||
unMuteNotificationsTextView -> onSetMuteTapped?.invoke(false)
|
binding.notificationsTextView -> onNotificationTapped?.invoke()
|
||||||
muteNotificationsTextView -> onSetMuteTapped?.invoke(true)
|
binding.unMuteNotificationsTextView -> onSetMuteTapped?.invoke(false)
|
||||||
|
binding.muteNotificationsTextView -> onSetMuteTapped?.invoke(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,26 +53,28 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.
|
|||||||
if (!this::thread.isInitialized) { return dismiss() }
|
if (!this::thread.isInitialized) { return dismiss() }
|
||||||
val recipient = thread.recipient
|
val recipient = thread.recipient
|
||||||
if (!recipient.isGroupRecipient && !recipient.isLocalNumber) {
|
if (!recipient.isGroupRecipient && !recipient.isLocalNumber) {
|
||||||
detailsTextView.visibility = View.VISIBLE
|
binding.detailsTextView.visibility = View.VISIBLE
|
||||||
unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE
|
binding.unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE
|
||||||
blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE
|
binding.blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE
|
||||||
detailsTextView.setOnClickListener(this)
|
binding.detailsTextView.setOnClickListener(this)
|
||||||
blockTextView.setOnClickListener(this)
|
binding.blockTextView.setOnClickListener(this)
|
||||||
unblockTextView.setOnClickListener(this)
|
binding.unblockTextView.setOnClickListener(this)
|
||||||
} else {
|
} else {
|
||||||
detailsTextView.visibility = View.GONE
|
binding.detailsTextView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
||||||
muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
||||||
unMuteNotificationsTextView.setOnClickListener(this)
|
binding.unMuteNotificationsTextView.setOnClickListener(this)
|
||||||
muteNotificationsTextView.setOnClickListener(this)
|
binding.muteNotificationsTextView.setOnClickListener(this)
|
||||||
notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
|
binding.notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
|
||||||
notificationsTextView.setOnClickListener(this)
|
binding.notificationsTextView.setOnClickListener(this)
|
||||||
deleteTextView.setOnClickListener(this)
|
binding.deleteTextView.setOnClickListener(this)
|
||||||
pinTextView.isVisible = !thread.isPinned
|
binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0
|
||||||
unpinTextView.isVisible = thread.isPinned
|
binding.markAllAsReadTextView.setOnClickListener(this)
|
||||||
pinTextView.setOnClickListener(this)
|
binding.pinTextView.isVisible = !thread.isPinned
|
||||||
unpinTextView.setOnClickListener(this)
|
binding.unpinTextView.isVisible = thread.isPinned
|
||||||
|
binding.pinTextView.setOnClickListener(this)
|
||||||
|
binding.unpinTextView.setOnClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -11,8 +11,8 @@ import android.widget.LinearLayout
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.synthetic.main.view_conversation.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewConversationBinding
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||||
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.util.DateUtils
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class ConversationView : LinearLayout {
|
class ConversationView : LinearLayout {
|
||||||
|
private lateinit var binding: ViewConversationBinding
|
||||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||||
var thread: ThreadRecord? = null
|
var thread: ThreadRecord? = null
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ class ConversationView : LinearLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_conversation, this)
|
binding = ViewConversationBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
|
layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
@ -39,84 +40,84 @@ class ConversationView : LinearLayout {
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
|
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
|
||||||
this.thread = thread
|
this.thread = thread
|
||||||
if (thread.isPinned) {
|
background = if (thread.isPinned) {
|
||||||
conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0)
|
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0)
|
||||||
background = ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background)
|
ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background)
|
||||||
} else {
|
} else {
|
||||||
conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||||
background = ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
||||||
}
|
}
|
||||||
profilePictureView.glide = glide
|
binding.profilePictureView.glide = glide
|
||||||
val unreadCount = thread.unreadCount
|
val unreadCount = thread.unreadCount
|
||||||
if (thread.recipient.isBlocked) {
|
if (thread.recipient.isBlocked) {
|
||||||
accentView.setBackgroundResource(R.color.destructive)
|
binding.accentView.setBackgroundResource(R.color.destructive)
|
||||||
accentView.visibility = View.VISIBLE
|
binding.accentView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
accentView.setBackgroundResource(R.color.accent)
|
binding.accentView.setBackgroundResource(R.color.accent)
|
||||||
// Using thread.isRead we can determine if the last message was our own, and display it as 'read' even though previous messages may not be
|
// Using thread.isRead we can determine if the last message was our own, and display it as 'read' even though previous messages may not be
|
||||||
// This would also not trigger the disappearing message timer which may or may not be desirable
|
// This would also not trigger the disappearing message timer which may or may not be desirable
|
||||||
accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE
|
binding.accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE
|
||||||
}
|
}
|
||||||
val formattedUnreadCount = if (thread.isRead) {
|
val formattedUnreadCount = if (thread.isRead) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
if (unreadCount < 100) unreadCount.toString() else "99+"
|
if (unreadCount < 10000) unreadCount.toString() else "9999+"
|
||||||
}
|
}
|
||||||
unreadCountTextView.text = formattedUnreadCount
|
binding.unreadCountTextView.text = formattedUnreadCount
|
||||||
val textSize = if (unreadCount < 100) 12.0f else 9.0f
|
val textSize = if (unreadCount < 10000) 12.0f else 9.0f
|
||||||
unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
||||||
unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL)
|
binding.unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL)
|
||||||
unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead)
|
binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead)
|
||||||
val senderDisplayName = getUserDisplayName(thread.recipient)
|
val senderDisplayName = getUserDisplayName(thread.recipient)
|
||||||
?: thread.recipient.address.toString()
|
?: thread.recipient.address.toString()
|
||||||
conversationViewDisplayNameTextView.text = senderDisplayName
|
binding.conversationViewDisplayNameTextView.text = senderDisplayName
|
||||||
timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||||
val recipient = thread.recipient
|
val recipient = thread.recipient
|
||||||
muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL
|
binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL
|
||||||
val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
|
val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
|
||||||
R.drawable.ic_outline_notifications_off_24
|
R.drawable.ic_outline_notifications_off_24
|
||||||
} else {
|
} else {
|
||||||
R.drawable.ic_notifications_mentions
|
R.drawable.ic_notifications_mentions
|
||||||
}
|
}
|
||||||
muteIndicatorImageView.setImageResource(drawableRes)
|
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
||||||
val rawSnippet = thread.getDisplayBody(context)
|
val rawSnippet = thread.getDisplayBody(context)
|
||||||
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
|
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
|
||||||
snippetTextView.text = snippet
|
binding.snippetTextView.text = snippet
|
||||||
snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||||
snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||||
if (isTyping) {
|
if (isTyping) {
|
||||||
typingIndicatorView.startAnimation()
|
binding.typingIndicatorView.startAnimation()
|
||||||
} else {
|
} else {
|
||||||
typingIndicatorView.stopAnimation()
|
binding.typingIndicatorView.stopAnimation()
|
||||||
}
|
}
|
||||||
typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE
|
binding.typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE
|
||||||
statusIndicatorImageView.visibility = View.VISIBLE
|
binding.statusIndicatorImageView.visibility = View.VISIBLE
|
||||||
when {
|
when {
|
||||||
!thread.isOutgoing -> statusIndicatorImageView.visibility = View.GONE
|
!thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE
|
||||||
thread.isFailed -> {
|
thread.isFailed -> {
|
||||||
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_error)?.mutate()
|
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_error)?.mutate()
|
||||||
drawable?.setTint(ContextCompat.getColor(context, R.color.destructive))
|
drawable?.setTint(ContextCompat.getColor(context, R.color.destructive))
|
||||||
statusIndicatorImageView.setImageDrawable(drawable)
|
binding.statusIndicatorImageView.setImageDrawable(drawable)
|
||||||
}
|
}
|
||||||
thread.isPending -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot)
|
thread.isPending -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot)
|
||||||
thread.isRead -> statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||||
else -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
||||||
}
|
}
|
||||||
post {
|
post {
|
||||||
profilePictureView.update(thread.recipient, thread.threadId)
|
binding.profilePictureView.update(thread.recipient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
profilePictureView.recycle()
|
binding.profilePictureView.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
private fun getUserDisplayName(recipient: Recipient): String? {
|
||||||
if (recipient.isLocalNumber) {
|
return if (recipient.isLocalNumber) {
|
||||||
return context.getString(R.string.note_to_self)
|
context.getString(R.string.note_to_self)
|
||||||
} else {
|
} else {
|
||||||
return recipient.name // Internally uses the Contact API
|
recipient.name // Internally uses the Contact API
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,9 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.style.ForegroundColorSpan
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
@ -19,23 +17,26 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.loader.app.LoaderManager
|
import androidx.loader.app.LoaderManager
|
||||||
import androidx.loader.content.Loader
|
import androidx.loader.content.Loader
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.android.synthetic.main.activity_home.*
|
|
||||||
import kotlinx.android.synthetic.main.seed_reminder_stub.*
|
|
||||||
import kotlinx.android.synthetic.main.seed_reminder_stub.view.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
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
|
||||||
|
import network.loki.messenger.databinding.ActivityHomeBinding
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
import org.session.libsession.utilities.*
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.Util
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsession.utilities.ProfilePictureModifiedEvent
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
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
|
||||||
@ -45,6 +46,7 @@ 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
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
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.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
@ -53,79 +55,136 @@ import org.thoughtcrime.securesms.dms.CreatePrivateChatActivity
|
|||||||
import org.thoughtcrime.securesms.groups.CreateClosedGroupActivity
|
import org.thoughtcrime.securesms.groups.CreateClosedGroupActivity
|
||||||
import org.thoughtcrime.securesms.groups.JoinPublicChatActivity
|
import org.thoughtcrime.securesms.groups.JoinPublicChatActivity
|
||||||
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.GlobalSearchInputLayout
|
||||||
|
import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
|
||||||
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.onboarding.SeedActivity
|
import org.thoughtcrime.securesms.onboarding.SeedActivity
|
||||||
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
|
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
|
||||||
import org.thoughtcrime.securesms.preferences.SettingsActivity
|
import org.thoughtcrime.securesms.preferences.SettingsActivity
|
||||||
import org.thoughtcrime.securesms.util.*
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
|
import org.thoughtcrime.securesms.util.IP2Country
|
||||||
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
|
import org.thoughtcrime.securesms.util.push
|
||||||
|
import org.thoughtcrime.securesms.util.show
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener,
|
class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||||
SeedReminderViewDelegate, NewConversationButtonSetViewDelegate, LoaderManager.LoaderCallbacks<Cursor> {
|
ConversationClickListener,
|
||||||
|
SeedReminderViewDelegate,
|
||||||
|
NewConversationButtonSetViewDelegate,
|
||||||
|
LoaderManager.LoaderCallbacks<Cursor>,
|
||||||
|
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityHomeBinding
|
||||||
private lateinit var glide: GlideRequests
|
private lateinit var glide: GlideRequests
|
||||||
private var broadcastReceiver: BroadcastReceiver? = null
|
private var broadcastReceiver: BroadcastReceiver? = null
|
||||||
|
|
||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
|
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
||||||
@Inject lateinit var recipientDatabase: RecipientDatabase
|
@Inject lateinit var recipientDatabase: RecipientDatabase
|
||||||
@Inject lateinit var groupDatabase: GroupDatabase
|
@Inject lateinit var groupDatabase: GroupDatabase
|
||||||
|
@Inject lateinit var textSecurePreferences: TextSecurePreferences
|
||||||
|
|
||||||
|
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
|
||||||
|
|
||||||
private val publicKey: String
|
private val publicKey: String
|
||||||
get() = TextSecurePreferences.getLocalNumber(this)!!
|
get() = TextSecurePreferences.getLocalNumber(this)!!
|
||||||
|
|
||||||
private val homeAdapter:HomeAdapter by lazy {
|
private val homeAdapter: HomeAdapter by lazy {
|
||||||
HomeAdapter(this, threadDb.conversationList)
|
HomeAdapter(context = this, cursor = threadDb.conversationList, listener = this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val globalSearchAdapter = GlobalSearchAdapter { model ->
|
||||||
|
when (model) {
|
||||||
|
is GlobalSearchAdapter.Model.Message -> {
|
||||||
|
val threadId = model.messageResult.threadId
|
||||||
|
val timestamp = model.messageResult.receivedTimestampMs
|
||||||
|
val author = model.messageResult.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 -> {
|
||||||
|
Log.d("Loki", "callback with model: $model")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
// Set content view
|
// Set content view
|
||||||
setContentView(R.layout.activity_home)
|
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
// Set custom toolbar
|
// Set custom toolbar
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
// Set up Glide
|
// Set up Glide
|
||||||
glide = GlideApp.with(this)
|
glide = GlideApp.with(this)
|
||||||
// Set up toolbar buttons
|
// Set up toolbar buttons
|
||||||
profileButton.glide = glide
|
binding.profileButton.glide = glide
|
||||||
profileButton.setOnClickListener { openSettings() }
|
binding.profileButton.setOnClickListener { openSettings() }
|
||||||
pathStatusViewContainer.disableClipping()
|
binding.searchViewContainer.setOnClickListener {
|
||||||
pathStatusViewContainer.setOnClickListener { showPath() }
|
binding.globalSearchInputLayout.requestFocus()
|
||||||
|
}
|
||||||
|
binding.sessionToolbar.disableClipping()
|
||||||
// Set up seed reminder view
|
// Set up seed reminder view
|
||||||
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
||||||
if (!hasViewedSeed) {
|
if (!hasViewedSeed) {
|
||||||
seedReminderStub.inflate().apply {
|
binding.seedReminderView.isVisible = true
|
||||||
val seedReminderView = this.seedReminderView
|
binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated
|
||||||
val seedReminderViewTitle = SpannableString("You're almost finished! 80%") // Intentionally not yet translated
|
binding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
|
||||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
binding.seedReminderView.setProgress(80, false)
|
||||||
seedReminderView.title = seedReminderViewTitle
|
binding.seedReminderView.delegate = this@HomeActivity
|
||||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
|
|
||||||
seedReminderView.setProgress(80, false)
|
|
||||||
seedReminderView.delegate = this@HomeActivity
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
seedReminderStub.isVisible = false
|
binding.seedReminderView.isVisible = false
|
||||||
}
|
}
|
||||||
|
setupHeaderImage()
|
||||||
// Set up recycler view
|
// Set up recycler view
|
||||||
|
binding.globalSearchInputLayout.listener = this
|
||||||
homeAdapter.setHasStableIds(true)
|
homeAdapter.setHasStableIds(true)
|
||||||
homeAdapter.glide = glide
|
homeAdapter.glide = glide
|
||||||
homeAdapter.conversationClickListener = this
|
binding.recyclerView.adapter = homeAdapter
|
||||||
recyclerView.adapter = homeAdapter
|
binding.globalSearchRecycler.adapter = globalSearchAdapter
|
||||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
|
||||||
// Set up empty state view
|
// Set up empty state view
|
||||||
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||||
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
|
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||||
// Set up new conversation button set
|
// Set up new conversation button set
|
||||||
newConversationButtonSet.delegate = this
|
binding.newConversationButtonSet.delegate = this
|
||||||
// Observe blocked contacts changed events
|
// Observe blocked contacts changed events
|
||||||
val broadcastReceiver = object : BroadcastReceiver() {
|
val broadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
recyclerView.adapter!!.notifyDataSetChanged()
|
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.broadcastReceiver = broadcastReceiver
|
this.broadcastReceiver = broadcastReceiver
|
||||||
@ -138,7 +197,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
// Set up typing observer
|
// Set up typing observer
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this@HomeActivity, Observer<Set<Long>> { threadIDs ->
|
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this@HomeActivity, Observer<Set<Long>> { threadIDs ->
|
||||||
val adapter = recyclerView.adapter as HomeAdapter
|
val adapter = binding.recyclerView.adapter as HomeAdapter
|
||||||
adapter.typingThreadIDs = threadIDs ?: setOf()
|
adapter.typingThreadIDs = threadIDs ?: setOf()
|
||||||
})
|
})
|
||||||
updateProfileButton()
|
updateProfileButton()
|
||||||
@ -155,10 +214,85 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
JobQueue.shared.resumePendingJobs()
|
JobQueue.shared.resumePendingJobs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// monitor the global search VM query
|
||||||
|
launch {
|
||||||
|
binding.globalSearchInputLayout.query
|
||||||
|
.onEach(globalSearchViewModel::postQuery)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
// Get group results and display them
|
||||||
|
launch {
|
||||||
|
globalSearchViewModel.result.collect { result ->
|
||||||
|
val currentUserPublicKey = publicKey
|
||||||
|
val contactAndGroupList = result.contacts.map { GlobalSearchAdapter.Model.Contact(it) } +
|
||||||
|
result.threads.map { GlobalSearchAdapter.Model.GroupConversation(it) }
|
||||||
|
|
||||||
|
val contactResults = contactAndGroupList.toMutableList()
|
||||||
|
|
||||||
|
if (contactResults.isEmpty()) {
|
||||||
|
contactResults.add(GlobalSearchAdapter.Model.SavedMessages(currentUserPublicKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupHeaderImage() {
|
||||||
|
val isDayUiMode = UiModeUtilities.isDayUiMode(this)
|
||||||
|
val headerTint = if (isDayUiMode) R.color.black else R.color.accent
|
||||||
|
binding.sessionHeaderImage.setColorFilter(getColor(headerTint))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInputFocusChanged(hasFocus: Boolean) {
|
||||||
|
if (hasFocus) {
|
||||||
|
setSearchShown(true)
|
||||||
|
} else {
|
||||||
|
setSearchShown(!binding.globalSearchInputLayout.query.value.isNullOrEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSearchShown(isShown: Boolean) {
|
||||||
|
binding.searchToolbar.isVisible = isShown
|
||||||
|
binding.sessionToolbar.isVisible = !isShown
|
||||||
|
binding.recyclerView.isVisible = !isShown
|
||||||
|
binding.emptyStateContainer.isVisible = (binding.recyclerView.adapter as HomeAdapter).itemCount == 0 && binding.recyclerView.isVisible
|
||||||
|
binding.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown
|
||||||
|
binding.gradientView.isVisible = !isShown
|
||||||
|
binding.globalSearchRecycler.isVisible = isShown
|
||||||
|
binding.newConversationButtonSet.isVisible = !isShown
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
|
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
|
||||||
return HomeLoader(this@HomeActivity)
|
return HomeLoader(this@HomeActivity)
|
||||||
}
|
}
|
||||||
@ -177,11 +311,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
|
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
|
||||||
if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared
|
if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared
|
||||||
IdentityKeyUtil.checkUpdate(this)
|
IdentityKeyUtil.checkUpdate(this)
|
||||||
profileButton.recycle() // clear cached image before update tje profilePictureView
|
binding.profileButton.recycle() // clear cached image before update tje profilePictureView
|
||||||
profileButton.update()
|
binding.profileButton.update()
|
||||||
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
||||||
if (hasViewedSeed) {
|
if (hasViewedSeed) {
|
||||||
seedReminderView?.isVisible = false
|
binding.seedReminderView.isVisible = false
|
||||||
}
|
}
|
||||||
if (TextSecurePreferences.getConfigurationMessageSynced(this)) {
|
if (TextSecurePreferences.getConfigurationMessageSynced(this)) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@ -214,8 +348,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
private fun updateEmptyState() {
|
private fun updateEmptyState() {
|
||||||
val threadCount = (recyclerView.adapter as HomeAdapter).itemCount
|
val threadCount = (binding.recyclerView.adapter as HomeAdapter).itemCount
|
||||||
emptyStateContainer.visibility = if (threadCount == 0) View.VISIBLE else View.GONE
|
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
@ -226,26 +360,34 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateProfileButton() {
|
private fun updateProfileButton() {
|
||||||
profileButton.publicKey = publicKey
|
binding.profileButton.publicKey = publicKey
|
||||||
profileButton.displayName = TextSecurePreferences.getProfileName(this)
|
binding.profileButton.displayName = TextSecurePreferences.getProfileName(this)
|
||||||
profileButton.recycle()
|
binding.profileButton.recycle()
|
||||||
profileButton.update()
|
binding.profileButton.update()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Interaction
|
// region Interaction
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (binding.globalSearchRecycler.isVisible) {
|
||||||
|
binding.globalSearchInputLayout.clearSearch(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
override fun handleSeedReminderViewContinueButtonTapped() {
|
override fun handleSeedReminderViewContinueButtonTapped() {
|
||||||
val intent = Intent(this, SeedActivity::class.java)
|
val intent = Intent(this, SeedActivity::class.java)
|
||||||
show(intent)
|
show(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConversationClick(view: ConversationView) {
|
override fun onConversationClick(thread: ThreadRecord) {
|
||||||
val thread = view.thread ?: return
|
val intent = Intent(this, ConversationActivityV2::class.java)
|
||||||
openConversation(thread)
|
intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId)
|
||||||
|
push(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongConversationClick(view: ConversationView) {
|
override fun onLongConversationClick(thread: ThreadRecord) {
|
||||||
val thread = view.thread ?: return
|
|
||||||
val bottomSheet = ConversationOptionsBottomSheet()
|
val bottomSheet = ConversationOptionsBottomSheet()
|
||||||
bottomSheet.thread = thread
|
bottomSheet.thread = thread
|
||||||
bottomSheet.onViewDetailsTapped = {
|
bottomSheet.onViewDetailsTapped = {
|
||||||
@ -286,15 +428,15 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
}
|
}
|
||||||
bottomSheet.onPinTapped = {
|
bottomSheet.onPinTapped = {
|
||||||
bottomSheet.dismiss()
|
bottomSheet.dismiss()
|
||||||
if (!thread.isPinned) {
|
setConversationPinned(thread.threadId, true)
|
||||||
pinConversation(thread)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
bottomSheet.onUnpinTapped = {
|
bottomSheet.onUnpinTapped = {
|
||||||
bottomSheet.dismiss()
|
bottomSheet.dismiss()
|
||||||
if (thread.isPinned) {
|
setConversationPinned(thread.threadId, false)
|
||||||
unpinConversation(thread)
|
}
|
||||||
}
|
bottomSheet.onMarkAllAsReadTapped = {
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
markAllAsRead(thread)
|
||||||
}
|
}
|
||||||
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
||||||
}
|
}
|
||||||
@ -305,10 +447,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
.setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
|
.setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
|
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
|
||||||
ThreadUtils.queue {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
recipientDatabase.setBlocked(thread.recipient, true)
|
recipientDatabase.setBlocked(thread.recipient, true)
|
||||||
Util.runOnMain {
|
withContext(Dispatchers.Main) {
|
||||||
recyclerView.adapter!!.notifyDataSetChanged()
|
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,10 +463,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
.setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
|
.setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
|
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
|
||||||
ThreadUtils.queue {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
recipientDatabase.setBlocked(thread.recipient, false)
|
recipientDatabase.setBlocked(thread.recipient, false)
|
||||||
Util.runOnMain {
|
withContext(Dispatchers.Main) {
|
||||||
recyclerView.adapter!!.notifyDataSetChanged()
|
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -333,18 +475,18 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
|
|
||||||
private fun setConversationMuted(thread: ThreadRecord, isMuted: Boolean) {
|
private fun setConversationMuted(thread: ThreadRecord, isMuted: Boolean) {
|
||||||
if (!isMuted) {
|
if (!isMuted) {
|
||||||
ThreadUtils.queue {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
recipientDatabase.setMuted(thread.recipient, 0)
|
recipientDatabase.setMuted(thread.recipient, 0)
|
||||||
Util.runOnMain {
|
withContext(Dispatchers.Main) {
|
||||||
recyclerView.adapter!!.notifyDataSetChanged()
|
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
MuteDialog.show(this) { until: Long ->
|
MuteDialog.show(this) { until: Long ->
|
||||||
ThreadUtils.queue {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
recipientDatabase.setMuted(thread.recipient, until)
|
recipientDatabase.setMuted(thread.recipient, until)
|
||||||
Util.runOnMain {
|
withContext(Dispatchers.Main) {
|
||||||
recyclerView.adapter!!.notifyDataSetChanged()
|
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,45 +494,41 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setNotifyType(thread: ThreadRecord, newNotifyType: Int) {
|
private fun setNotifyType(thread: ThreadRecord, newNotifyType: Int) {
|
||||||
ThreadUtils.queue {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
recipientDatabase.setNotifyType(thread.recipient, newNotifyType)
|
recipientDatabase.setNotifyType(thread.recipient, newNotifyType)
|
||||||
Util.runOnMain {
|
withContext(Dispatchers.Main) {
|
||||||
recyclerView.adapter!!.notifyDataSetChanged()
|
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pinConversation(thread: ThreadRecord) {
|
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
||||||
ThreadUtils.queue {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
threadDb.setPinned(thread.threadId, true)
|
threadDb.setPinned(threadId, pinned)
|
||||||
Util.runOnMain {
|
withContext(Dispatchers.Main) {
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
LoaderManager.getInstance(this@HomeActivity).restartLoader(0, null, this@HomeActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unpinConversation(thread: ThreadRecord) {
|
private fun markAllAsRead(thread: ThreadRecord) {
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
threadDb.setPinned(thread.threadId, false)
|
threadDb.markAllAsRead(thread.threadId, thread.recipient.isOpenGroupRecipient)
|
||||||
Util.runOnMain {
|
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteConversation(thread: ThreadRecord) {
|
private fun deleteConversation(thread: ThreadRecord) {
|
||||||
val threadID = thread.threadId
|
val threadID = thread.threadId
|
||||||
val recipient = thread.recipient
|
val recipient = thread.recipient
|
||||||
val message: String
|
val message = if (recipient.isGroupRecipient) {
|
||||||
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(this))) {
|
if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) {
|
||||||
message = "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 {
|
||||||
message = resources.getString(R.string.activity_home_leave_group_dialog_message)
|
resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message = resources.getString(R.string.activity_home_delete_conversation_dialog_message)
|
resources.getString(R.string.activity_home_delete_conversation_dialog_message)
|
||||||
}
|
}
|
||||||
val dialog = AlertDialog.Builder(this)
|
val dialog = AlertDialog.Builder(this)
|
||||||
dialog.setMessage(message)
|
dialog.setMessage(message)
|
||||||
@ -419,7 +557,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
if (v2OpenGroup != null) {
|
if (v2OpenGroup != null) {
|
||||||
OpenGroupManager.delete(v2OpenGroup.server, v2OpenGroup.room, this@HomeActivity)
|
OpenGroupManager.delete(v2OpenGroup.server, v2OpenGroup.room, this@HomeActivity)
|
||||||
} else {
|
} else {
|
||||||
ThreadUtils.queue {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
threadDb.deleteConversation(threadID)
|
threadDb.deleteConversation(threadID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -436,12 +574,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
|||||||
dialog.create().show()
|
dialog.create().show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openConversation(thread: ThreadRecord) {
|
|
||||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
|
||||||
intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId)
|
|
||||||
push(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openSettings() {
|
private fun openSettings() {
|
||||||
val intent = Intent(this, SettingsActivity::class.java)
|
val intent = Intent(this, SettingsActivity::class.java)
|
||||||
show(intent, isForResult = true)
|
show(intent, isForResult = true)
|
||||||
|
@ -9,20 +9,23 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord
|
|||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class HomeAdapter(context: Context, cursor: Cursor?) : CursorRecyclerViewAdapter<HomeAdapter.ViewHolder>(context, cursor) {
|
class HomeAdapter(
|
||||||
|
context: Context,
|
||||||
|
cursor: Cursor?,
|
||||||
|
val listener: ConversationClickListener
|
||||||
|
) : CursorRecyclerViewAdapter<HomeAdapter.ViewHolder>(context, cursor) {
|
||||||
private val threadDatabase = DatabaseComponent.get(context).threadDatabase()
|
private val threadDatabase = DatabaseComponent.get(context).threadDatabase()
|
||||||
lateinit var glide: GlideRequests
|
lateinit var glide: GlideRequests
|
||||||
var typingThreadIDs = setOf<Long>()
|
var typingThreadIDs = setOf<Long>()
|
||||||
set(value) { field = value; notifyDataSetChanged() }
|
set(value) { field = value; notifyDataSetChanged() }
|
||||||
var conversationClickListener: ConversationClickListener? = null
|
|
||||||
|
|
||||||
class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val view = ConversationView(context)
|
val view = ConversationView(context)
|
||||||
view.setOnClickListener { conversationClickListener?.onConversationClick(view) }
|
view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } }
|
||||||
view.setOnLongClickListener {
|
view.setOnLongClickListener {
|
||||||
conversationClickListener?.onLongConversationClick(view)
|
view.thread?.let { listener.onLongConversationClick(it) }
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
return ViewHolder(view)
|
return ViewHolder(view)
|
||||||
@ -45,6 +48,6 @@ class HomeAdapter(context: Context, cursor: Cursor?) : CursorRecyclerViewAdapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ConversationClickListener {
|
interface ConversationClickListener {
|
||||||
fun onConversationClick(view: ConversationView)
|
fun onConversationClick(thread: ThreadRecord)
|
||||||
fun onLongConversationClick(view: ConversationView)
|
fun onLongConversationClick(thread: ThreadRecord)
|
||||||
}
|
}
|
@ -17,26 +17,33 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import kotlinx.android.synthetic.main.activity_path.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivityPathBinding
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsignal.utilities.Snode
|
import org.session.libsignal.utilities.Snode
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.util.*
|
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
import org.thoughtcrime.securesms.util.IP2Country
|
import org.thoughtcrime.securesms.util.IP2Country
|
||||||
import org.thoughtcrime.securesms.util.PathDotView
|
import org.thoughtcrime.securesms.util.PathDotView
|
||||||
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
|
import org.thoughtcrime.securesms.util.animateSizeChange
|
||||||
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
|
import org.thoughtcrime.securesms.util.fadeIn
|
||||||
|
import org.thoughtcrime.securesms.util.fadeOut
|
||||||
|
import org.thoughtcrime.securesms.util.getColorWithID
|
||||||
|
|
||||||
class PathActivity : PassphraseRequiredActionBarActivity() {
|
class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||||
|
private lateinit var binding: ActivityPathBinding
|
||||||
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
|
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
|
||||||
|
|
||||||
// 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)
|
||||||
setContentView(R.layout.activity_path)
|
binding = ActivityPathBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_path_title)
|
supportActionBar!!.title = resources.getString(R.string.activity_path_title)
|
||||||
pathRowsContainer.disableClipping()
|
binding.pathRowsContainer.disableClipping()
|
||||||
learnMoreButton.setOnClickListener { learnMore() }
|
binding.learnMoreButton.setOnClickListener { learnMore() }
|
||||||
update(false)
|
update(false)
|
||||||
registerObservers()
|
registerObservers()
|
||||||
}
|
}
|
||||||
@ -82,7 +89,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
private fun handleOnionRequestPathCountriesLoaded() { update(false) }
|
private fun handleOnionRequestPathCountriesLoaded() { update(false) }
|
||||||
|
|
||||||
private fun update(isAnimated: Boolean) {
|
private fun update(isAnimated: Boolean) {
|
||||||
pathRowsContainer.removeAllViews()
|
binding.pathRowsContainer.removeAllViews()
|
||||||
if (OnionRequestAPI.paths.isNotEmpty()) {
|
if (OnionRequestAPI.paths.isNotEmpty()) {
|
||||||
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
|
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
|
||||||
val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000
|
val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000
|
||||||
@ -94,18 +101,18 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
val destinationRow = getPathRow(resources.getString(R.string.activity_path_destination_row_title), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
|
val destinationRow = getPathRow(resources.getString(R.string.activity_path_destination_row_title), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
|
||||||
val rows = listOf( youRow ) + pathRows + listOf( destinationRow )
|
val rows = listOf( youRow ) + pathRows + listOf( destinationRow )
|
||||||
for (row in rows) {
|
for (row in rows) {
|
||||||
pathRowsContainer.addView(row)
|
binding.pathRowsContainer.addView(row)
|
||||||
}
|
}
|
||||||
if (isAnimated) {
|
if (isAnimated) {
|
||||||
spinner.fadeOut()
|
binding.spinner.fadeOut()
|
||||||
} else {
|
} else {
|
||||||
spinner.alpha = 0.0f
|
binding.spinner.alpha = 0.0f
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isAnimated) {
|
if (isAnimated) {
|
||||||
spinner.fadeIn()
|
binding.spinner.fadeIn()
|
||||||
} else {
|
} else {
|
||||||
spinner.alpha = 1.0f
|
binding.spinner.alpha = 1.0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,9 @@ import android.view.inputmethod.InputMethodManager
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import dagger.hilt.EntryPoint
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.android.synthetic.main.fragment_user_details_bottom_sheet.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.FragmentUserDetailsBottomSheetBinding
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
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
|
||||||
@ -34,13 +33,15 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentUserDetailsBottomSheetBinding
|
||||||
companion object {
|
companion object {
|
||||||
const val ARGUMENT_PUBLIC_KEY = "publicKey"
|
const val ARGUMENT_PUBLIC_KEY = "publicKey"
|
||||||
const val ARGUMENT_THREAD_ID = "threadId"
|
const val ARGUMENT_THREAD_ID = "threadId"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.fragment_user_details_bottom_sheet, container, false)
|
binding = FragmentUserDetailsBottomSheetBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@ -49,58 +50,62 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
|||||||
val threadID = arguments?.getLong(ARGUMENT_THREAD_ID) ?: return dismiss()
|
val threadID = arguments?.getLong(ARGUMENT_THREAD_ID) ?: return dismiss()
|
||||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
|
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
|
||||||
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
||||||
profilePictureView.publicKey = publicKey
|
with(binding) {
|
||||||
profilePictureView.glide = GlideApp.with(this)
|
profilePictureView.publicKey = publicKey
|
||||||
profilePictureView.isLarge = true
|
profilePictureView.glide = GlideApp.with(this@UserDetailsBottomSheet)
|
||||||
profilePictureView.update(recipient, -1)
|
profilePictureView.isLarge = true
|
||||||
nameTextViewContainer.visibility = View.VISIBLE
|
profilePictureView.update(recipient)
|
||||||
nameTextViewContainer.setOnClickListener {
|
|
||||||
nameTextViewContainer.visibility = View.INVISIBLE
|
|
||||||
nameEditTextContainer.visibility = View.VISIBLE
|
|
||||||
nicknameEditText.text = null
|
|
||||||
nicknameEditText.requestFocus()
|
|
||||||
showSoftKeyboard()
|
|
||||||
}
|
|
||||||
cancelNicknameEditingButton.setOnClickListener {
|
|
||||||
nicknameEditText.clearFocus()
|
|
||||||
hideSoftKeyboard()
|
|
||||||
nameTextViewContainer.visibility = View.VISIBLE
|
nameTextViewContainer.visibility = View.VISIBLE
|
||||||
nameEditTextContainer.visibility = View.INVISIBLE
|
nameTextViewContainer.setOnClickListener {
|
||||||
}
|
nameTextViewContainer.visibility = View.INVISIBLE
|
||||||
saveNicknameButton.setOnClickListener {
|
nameEditTextContainer.visibility = View.VISIBLE
|
||||||
saveNickName(recipient)
|
nicknameEditText.text = null
|
||||||
}
|
nicknameEditText.requestFocus()
|
||||||
nicknameEditText.setOnEditorActionListener { _, actionId, _ ->
|
showSoftKeyboard()
|
||||||
when (actionId) {
|
|
||||||
EditorInfo.IME_ACTION_DONE -> {
|
|
||||||
saveNickName(recipient)
|
|
||||||
return@setOnEditorActionListener true
|
|
||||||
}
|
|
||||||
else -> return@setOnEditorActionListener false
|
|
||||||
}
|
}
|
||||||
}
|
cancelNicknameEditingButton.setOnClickListener {
|
||||||
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
|
nicknameEditText.clearFocus()
|
||||||
|
hideSoftKeyboard()
|
||||||
|
nameTextViewContainer.visibility = View.VISIBLE
|
||||||
|
nameEditTextContainer.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
saveNicknameButton.setOnClickListener {
|
||||||
|
saveNickName(recipient)
|
||||||
|
}
|
||||||
|
nicknameEditText.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
when (actionId) {
|
||||||
|
EditorInfo.IME_ACTION_DONE -> {
|
||||||
|
saveNickName(recipient)
|
||||||
|
return@setOnEditorActionListener true
|
||||||
|
}
|
||||||
|
else -> return@setOnEditorActionListener false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
|
||||||
|
|
||||||
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
|
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
|
||||||
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient
|
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient
|
||||||
publicKeyTextView.text = publicKey
|
publicKeyTextView.text = publicKey
|
||||||
publicKeyTextView.setOnLongClickListener {
|
publicKeyTextView.setOnLongClickListener {
|
||||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard =
|
||||||
val clip = ClipData.newPlainText("Session ID", publicKey)
|
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
clipboard.setPrimaryClip(clip)
|
val clip = ClipData.newPlainText("Session ID", publicKey)
|
||||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
clipboard.setPrimaryClip(clip)
|
||||||
true
|
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
|
||||||
}
|
.show()
|
||||||
messageButton.setOnClickListener {
|
true
|
||||||
val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient)
|
}
|
||||||
val intent = Intent(
|
messageButton.setOnClickListener {
|
||||||
context,
|
val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient)
|
||||||
ConversationActivityV2::class.java
|
val intent = Intent(
|
||||||
)
|
context,
|
||||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
ConversationActivityV2::class.java
|
||||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1)
|
)
|
||||||
startActivity(intent)
|
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||||
dismiss()
|
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1)
|
||||||
|
startActivity(intent)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +116,7 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
|||||||
window.setDimAmount(if (isLightMode) 0.1f else 0.75f)
|
window.setDimAmount(if (isLightMode) 0.1f else 0.75f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveNickName(recipient: Recipient) {
|
fun saveNickName(recipient: Recipient) = with(binding) {
|
||||||
nicknameEditText.clearFocus()
|
nicknameEditText.clearFocus()
|
||||||
hideSoftKeyboard()
|
hideSoftKeyboard()
|
||||||
nameTextViewContainer.visibility = View.VISIBLE
|
nameTextViewContainer.visibility = View.VISIBLE
|
||||||
@ -131,11 +136,11 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
|||||||
@SuppressLint("ServiceCast")
|
@SuppressLint("ServiceCast")
|
||||||
fun showSoftKeyboard() {
|
fun showSoftKeyboard() {
|
||||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
imm?.showSoftInput(nicknameEditText, 0)
|
imm?.showSoftInput(binding.nicknameEditText, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideSoftKeyboard() {
|
fun hideSoftKeyboard() {
|
||||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
imm?.hideSoftInputFromWindow(nicknameEditText.windowToken, 0)
|
imm?.hideSoftInputFromWindow(binding.nicknameEditText.windowToken, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
package org.thoughtcrime.securesms.home.search
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
|
||||||
|
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
|
||||||
|
import org.session.libsession.utilities.GroupRecord
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
|
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||||
|
import java.security.InvalidParameterException
|
||||||
|
import org.session.libsession.messaging.contacts.Contact as ContactModel
|
||||||
|
|
||||||
|
class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val HEADER_VIEW_TYPE = 0
|
||||||
|
const val CONTENT_VIEW_TYPE = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private var data: List<Model> = listOf()
|
||||||
|
private var query: String? = null
|
||||||
|
|
||||||
|
fun setNewData(query: String, newData: List<Model>) {
|
||||||
|
val diffResult = DiffUtil.calculateDiff(GlobalSearchDiff(this.query, query, data, newData))
|
||||||
|
this.query = query
|
||||||
|
data = newData
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int =
|
||||||
|
if (data[position] is Model.Header) HEADER_VIEW_TYPE else CONTENT_VIEW_TYPE
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = data.size
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
||||||
|
if (viewType == HEADER_VIEW_TYPE) {
|
||||||
|
HeaderView(
|
||||||
|
LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.view_global_search_header, parent, false)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ContentView(
|
||||||
|
LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.view_global_search_result, parent, false)
|
||||||
|
, modelCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(
|
||||||
|
holder: RecyclerView.ViewHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: MutableList<Any>
|
||||||
|
) {
|
||||||
|
val newUpdateQuery: String? = payloads.firstOrNull { it is String } as String?
|
||||||
|
if (newUpdateQuery != null && holder is ContentView) {
|
||||||
|
holder.bindPayload(newUpdateQuery, data[position])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (holder is HeaderView) {
|
||||||
|
holder.bind(data[position] as Model.Header)
|
||||||
|
} else if (holder is ContentView) {
|
||||||
|
holder.bind(query.orEmpty(), data[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
onBindViewHolder(holder,position, mutableListOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
class HeaderView(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
val binding = ViewGlobalSearchHeaderBinding.bind(view)
|
||||||
|
|
||||||
|
fun bind(header: Model.Header) {
|
||||||
|
binding.searchHeader.setText(header.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||||
|
if (holder is ContentView) {
|
||||||
|
holder.binding.searchResultProfilePicture.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
val binding = ViewGlobalSearchResultBinding.bind(view).apply {
|
||||||
|
searchResultProfilePicture.glide = GlideApp.with(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindPayload(newQuery: String, model: Model) {
|
||||||
|
bindQuery(newQuery, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(query: String, model: Model) {
|
||||||
|
binding.searchResultProfilePicture.recycle()
|
||||||
|
when (model) {
|
||||||
|
is Model.GroupConversation -> bindModel(query, model)
|
||||||
|
is Model.Contact -> bindModel(query, model)
|
||||||
|
is Model.Message -> bindModel(query, model)
|
||||||
|
is Model.SavedMessages -> bindModel(model)
|
||||||
|
is Model.Header -> throw InvalidParameterException("Can't display Model.Header as ContentView")
|
||||||
|
}
|
||||||
|
binding.root.setOnClickListener { modelCallback(model) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MessageModel(
|
||||||
|
val threadRecipient: Recipient,
|
||||||
|
val messageRecipient: Recipient,
|
||||||
|
val messageSnippet: String
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class Model {
|
||||||
|
data class Header(@StringRes val title: Int) : Model()
|
||||||
|
data class SavedMessages(val currentUserPublicKey: String): Model()
|
||||||
|
data class Contact(val contact: ContactModel) : Model()
|
||||||
|
data class GroupConversation(val groupRecord: GroupRecord) : Model()
|
||||||
|
data class Message(val messageResult: MessageResult, val unread: Int) : Model()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
package org.thoughtcrime.securesms.home.search
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.StyleSpan
|
||||||
|
import android.util.TypedValue
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView
|
||||||
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation
|
||||||
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message
|
||||||
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages
|
||||||
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
|
import java.util.Locale
|
||||||
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalSearchDiff(
|
||||||
|
private val oldQuery: String?,
|
||||||
|
private val newQuery: String?,
|
||||||
|
private val oldData: List<GlobalSearchAdapter.Model>,
|
||||||
|
private val newData: List<GlobalSearchAdapter.Model>
|
||||||
|
) : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize(): Int = oldData.size
|
||||||
|
override fun getNewListSize(): Int = newData.size
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
|
oldData[oldItemPosition] == newData[newItemPosition]
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
|
oldQuery == newQuery && oldData[oldItemPosition] == newData[newItemPosition]
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? =
|
||||||
|
if (oldQuery != newQuery) newQuery
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val BoldStyleFactory = { StyleSpan(Typeface.BOLD) }
|
||||||
|
|
||||||
|
fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) {
|
||||||
|
when (model) {
|
||||||
|
is ContactModel -> {
|
||||||
|
binding.searchResultTitle.text = getHighlight(
|
||||||
|
query,
|
||||||
|
model.contact.getSearchName()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is Message -> {
|
||||||
|
val textSpannable = SpannableStringBuilder()
|
||||||
|
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
||||||
|
// group chat, bind
|
||||||
|
val text = "${model.messageResult.messageRecipient.getSearchName()}: "
|
||||||
|
textSpannable.append(text)
|
||||||
|
}
|
||||||
|
textSpannable.append(getHighlight(
|
||||||
|
query,
|
||||||
|
model.messageResult.bodySnippet
|
||||||
|
))
|
||||||
|
binding.searchResultSubtitle.text = textSpannable
|
||||||
|
binding.searchResultSubtitle.isVisible = true
|
||||||
|
binding.searchResultTitle.text = model.messageResult.conversationRecipient.toShortString()
|
||||||
|
}
|
||||||
|
is GroupConversation -> {
|
||||||
|
binding.searchResultTitle.text = getHighlight(
|
||||||
|
query,
|
||||||
|
model.groupRecord.title
|
||||||
|
)
|
||||||
|
|
||||||
|
val membersString = model.groupRecord.members.joinToString { address ->
|
||||||
|
val recipient = Recipient.from(binding.root.context, address, false)
|
||||||
|
recipient.name ?: "${address.serialize().take(4)}...${address.serialize().takeLast(4)}"
|
||||||
|
}
|
||||||
|
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHighlight(query: String?, toSearch: String): Spannable? {
|
||||||
|
return SearchUtil.getHighlightedSpan(Locale.getDefault(), BoldStyleFactory, toSearch, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
||||||
|
binding.searchResultProfilePicture.isVisible = true
|
||||||
|
binding.searchResultSavedMessages.isVisible = false
|
||||||
|
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
|
||||||
|
binding.searchResultTimestamp.isVisible = false
|
||||||
|
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false)
|
||||||
|
binding.searchResultProfilePicture.update(threadRecipient)
|
||||||
|
val nameString = model.groupRecord.title
|
||||||
|
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||||
|
|
||||||
|
val groupRecipients = model.groupRecord.members.map { Recipient.from(binding.root.context, it, false) }
|
||||||
|
|
||||||
|
val membersString = groupRecipients.joinToString {
|
||||||
|
val address = it.address.serialize()
|
||||||
|
it.name ?: "${address.take(4)}...${address.takeLast(4)}"
|
||||||
|
}
|
||||||
|
if (model.groupRecord.isClosedGroup) {
|
||||||
|
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ContentView.bindModel(query: String?, model: ContactModel) {
|
||||||
|
binding.searchResultProfilePicture.isVisible = true
|
||||||
|
binding.searchResultSavedMessages.isVisible = false
|
||||||
|
binding.searchResultSubtitle.isVisible = false
|
||||||
|
binding.searchResultTimestamp.isVisible = false
|
||||||
|
binding.searchResultSubtitle.text = null
|
||||||
|
val recipient =
|
||||||
|
Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false)
|
||||||
|
binding.searchResultProfilePicture.update(recipient)
|
||||||
|
val nameString = model.contact.getSearchName()
|
||||||
|
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ContentView.bindModel(model: SavedMessages) {
|
||||||
|
binding.searchResultSubtitle.isVisible = false
|
||||||
|
binding.searchResultTimestamp.isVisible = false
|
||||||
|
binding.searchResultTitle.setText(R.string.note_to_self)
|
||||||
|
binding.searchResultProfilePicture.isVisible = false
|
||||||
|
binding.searchResultSavedMessages.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ContentView.bindModel(query: String?, model: Message) {
|
||||||
|
binding.searchResultProfilePicture.isVisible = true
|
||||||
|
binding.searchResultSavedMessages.isVisible = false
|
||||||
|
binding.searchResultTimestamp.isVisible = true
|
||||||
|
// val hasUnreads = model.unread > 0
|
||||||
|
// binding.unreadCountIndicator.isVisible = hasUnreads
|
||||||
|
// if (hasUnreads) {
|
||||||
|
// binding.unreadCountTextView.text = model.unread.toString()
|
||||||
|
// }
|
||||||
|
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.receivedTimestampMs)
|
||||||
|
binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient)
|
||||||
|
val textSpannable = SpannableStringBuilder()
|
||||||
|
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
||||||
|
// group chat, bind
|
||||||
|
val text = "${model.messageResult.messageRecipient.getSearchName()}: "
|
||||||
|
textSpannable.append(text)
|
||||||
|
}
|
||||||
|
textSpannable.append(getHighlight(
|
||||||
|
query,
|
||||||
|
model.messageResult.bodySnippet
|
||||||
|
))
|
||||||
|
binding.searchResultSubtitle.text = textSpannable
|
||||||
|
binding.searchResultTitle.text = model.messageResult.conversationRecipient.toShortString()
|
||||||
|
binding.searchResultSubtitle.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Recipient.getSearchName(): String = name ?: address.serialize().let { address -> "${address.take(4)}...${address.takeLast(4)}" }
|
||||||
|
|
||||||
|
fun Contact.getSearchName(): String =
|
||||||
|
if (nickname.isNullOrEmpty()) name ?: "${sessionID.take(4)}...${sessionID.takeLast(4)}"
|
||||||
|
else "${name ?: "${sessionID.take(4)}...${sessionID.takeLast(4)}"} ($nickname)"
|
@ -0,0 +1,88 @@
|
|||||||
|
package org.thoughtcrime.securesms.home.search
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import network.loki.messenger.databinding.ViewGlobalSearchInputBinding
|
||||||
|
|
||||||
|
class GlobalSearchInputLayout @JvmOverloads constructor(
|
||||||
|
context: Context, attrs: AttributeSet? = null
|
||||||
|
) : LinearLayout(context, attrs),
|
||||||
|
View.OnFocusChangeListener,
|
||||||
|
View.OnClickListener,
|
||||||
|
TextWatcher, TextView.OnEditorActionListener {
|
||||||
|
|
||||||
|
var binding: ViewGlobalSearchInputBinding = ViewGlobalSearchInputBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
|
||||||
|
var listener: GlobalSearchInputLayoutListener? = null
|
||||||
|
|
||||||
|
private val _query = MutableStateFlow<CharSequence?>(null)
|
||||||
|
val query: StateFlow<CharSequence?> = _query
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
binding.searchInput.onFocusChangeListener = this
|
||||||
|
binding.searchInput.addTextChangedListener(this)
|
||||||
|
binding.searchInput.setOnEditorActionListener(this)
|
||||||
|
binding.searchCancel.setOnClickListener(this)
|
||||||
|
binding.searchClear.setOnClickListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFocusChange(v: View?, hasFocus: Boolean) {
|
||||||
|
if (v === binding.searchInput) {
|
||||||
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imm.hideSoftInputFromWindow(windowToken, 0)
|
||||||
|
listener?.onInputFocusChanged(hasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||||
|
if (v === binding.searchInput && actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||||
|
binding.searchInput.clearFocus()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
if (v === binding.searchCancel) {
|
||||||
|
clearSearch(true)
|
||||||
|
} else if (v === binding.searchClear) {
|
||||||
|
clearSearch(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSearch(clearFocus: Boolean) {
|
||||||
|
binding.searchInput.text = null
|
||||||
|
if (clearFocus) {
|
||||||
|
binding.searchInput.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
_query.value = s?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalSearchInputLayoutListener {
|
||||||
|
fun onInputFocusChanged(hasFocus: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package org.thoughtcrime.securesms.home.search
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
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.SearchResult
|
||||||
|
|
||||||
|
data class GlobalSearchResult(
|
||||||
|
val query: String,
|
||||||
|
val contacts: List<Contact>,
|
||||||
|
val threads: List<GroupRecord>,
|
||||||
|
val messages: List<MessageResult>
|
||||||
|
) {
|
||||||
|
|
||||||
|
val isEmpty: Boolean
|
||||||
|
get() = contacts.isEmpty() && threads.isEmpty() && messages.isEmpty()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package org.thoughtcrime.securesms.home.search
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.buffer
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
|
import org.session.libsignal.utilities.SettableFuture
|
||||||
|
import org.thoughtcrime.securesms.search.SearchRepository
|
||||||
|
import org.thoughtcrime.securesms.search.model.SearchResult
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class GlobalSearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() {
|
||||||
|
|
||||||
|
private val executor = viewModelScope + SupervisorJob()
|
||||||
|
|
||||||
|
private val _result: MutableStateFlow<GlobalSearchResult> =
|
||||||
|
MutableStateFlow(GlobalSearchResult.EMPTY)
|
||||||
|
|
||||||
|
val result: StateFlow<GlobalSearchResult> = _result
|
||||||
|
|
||||||
|
private val _queryText: MutableStateFlow<CharSequence> = MutableStateFlow("")
|
||||||
|
|
||||||
|
fun postQuery(charSequence: CharSequence?) {
|
||||||
|
charSequence ?: return
|
||||||
|
_queryText.value = charSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
//
|
||||||
|
_queryText
|
||||||
|
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
.mapLatest { query ->
|
||||||
|
if (query.trim().length < 2) {
|
||||||
|
SearchResult.EMPTY
|
||||||
|
} else {
|
||||||
|
// user input delay here in case we get a new query within a few hundred ms
|
||||||
|
// this coroutine will be cancelled and expensive query will not be run if typing quickly
|
||||||
|
// first query of 2 characters will be instant however
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
@ -80,7 +80,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
|
|||||||
controller = (Controller) getActivity();
|
controller = (Controller) getActivity();
|
||||||
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
|
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
|
||||||
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
|
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
|
||||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
@ -66,7 +66,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
|||||||
bucketId = getArguments().getString(KEY_BUCKET_ID);
|
bucketId = getArguments().getString(KEY_BUCKET_ID);
|
||||||
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
|
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
|
||||||
maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
|
maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
|
||||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -105,7 +105,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
|||||||
onMediaSelectionChanged(new ArrayList<>(viewModel.getSelectedMedia().getValue()));
|
onMediaSelectionChanged(new ArrayList<>(viewModel.getSelectedMedia().getValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.getMediaInBucket(requireContext(), bucketId).observe(this, adapter::setMedia);
|
viewModel.getMediaInBucket(requireContext(), bucketId).observe(getViewLifecycleOwner(), adapter::setMedia);
|
||||||
|
|
||||||
initMediaObserver(viewModel);
|
initMediaObserver(viewModel);
|
||||||
}
|
}
|
||||||
@ -178,7 +178,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initMediaObserver(@NonNull MediaSendViewModel viewModel) {
|
private void initMediaObserver(@NonNull MediaSendViewModel viewModel) {
|
||||||
viewModel.getCountButtonState().observe(this, media -> {
|
viewModel.getCountButtonState().observe(getViewLifecycleOwner(), media -> {
|
||||||
requireActivity().invalidateOptionsMenu();
|
requireActivity().invalidateOptionsMenu();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
@ -313,7 +313,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initViewModel() {
|
private void initViewModel() {
|
||||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||||
|
|
||||||
viewModel.getSelectedMedia().observe(this, media -> {
|
viewModel.getSelectedMedia().observe(this, media -> {
|
||||||
if (Util.isEmpty(media)) {
|
if (Util.isEmpty(media)) {
|
||||||
|
@ -60,7 +60,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
val closedGroupPoller = ClosedGroupPollerV2() // Intentionally don't use shared
|
val closedGroupPoller = ClosedGroupPollerV2() // Intentionally don't use shared
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||||
allGroupPublicKeys.forEach { closedGroupPoller.poll(it) }
|
allGroupPublicKeys.iterator().forEach { closedGroupPoller.poll(it) }
|
||||||
|
|
||||||
// Open Groups
|
// Open Groups
|
||||||
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
||||||
|
@ -57,7 +57,7 @@ object LokiPushNotificationManager {
|
|||||||
// Unsubscribe from all closed groups
|
// Unsubscribe from all closed groups
|
||||||
val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys()
|
val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys()
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
allClosedGroupPublicKeys.forEach { closedGroup ->
|
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||||
performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ object LokiPushNotificationManager {
|
|||||||
}
|
}
|
||||||
// Subscribe to all closed groups
|
// Subscribe to all closed groups
|
||||||
val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys()
|
val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys()
|
||||||
allClosedGroupPublicKeys.forEach { closedGroup ->
|
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||||
performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,12 +70,13 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
|||||||
|
|
||||||
public static void process(@NonNull Context context, @NonNull List<MarkedMessageInfo> markedReadMessages) {
|
public static void process(@NonNull Context context, @NonNull List<MarkedMessageInfo> markedReadMessages) {
|
||||||
if (markedReadMessages.isEmpty()) return;
|
if (markedReadMessages.isEmpty()) return;
|
||||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) return;
|
|
||||||
|
|
||||||
for (MarkedMessageInfo messageInfo : markedReadMessages) {
|
for (MarkedMessageInfo messageInfo : markedReadMessages) {
|
||||||
scheduleDeletion(context, messageInfo.getExpirationInfo());
|
scheduleDeletion(context, messageInfo.getExpirationInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) return;
|
||||||
|
|
||||||
Map<Address, List<SyncMessageId>> addressMap = Stream.of(markedReadMessages)
|
Map<Address, List<SyncMessageId>> addressMap = Stream.of(markedReadMessages)
|
||||||
.map(MarkedMessageInfo::getSyncMessageId)
|
.map(MarkedMessageInfo::getSyncMessageId)
|
||||||
.collect(Collectors.groupingBy(SyncMessageId::getAddress));
|
.collect(Collectors.groupingBy(SyncMessageId::getAddress));
|
||||||
|
@ -7,8 +7,8 @@ import android.view.inputmethod.EditorInfo
|
|||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.TextView.OnEditorActionListener
|
import android.widget.TextView.OnEditorActionListener
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import kotlinx.android.synthetic.main.activity_display_name.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivityDisplayNameBinding
|
||||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
@ -16,28 +16,32 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
|||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
|
||||||
class DisplayNameActivity : BaseActionBarActivity() {
|
class DisplayNameActivity : BaseActionBarActivity() {
|
||||||
|
private lateinit var binding: ActivityDisplayNameBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setUpActionBarSessionLogo()
|
setUpActionBarSessionLogo()
|
||||||
setContentView(R.layout.activity_display_name)
|
binding = ActivityDisplayNameBinding.inflate(layoutInflater)
|
||||||
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
|
setContentView(binding.root)
|
||||||
displayNameEditText.setOnEditorActionListener(
|
with(binding) {
|
||||||
OnEditorActionListener { _, actionID, event ->
|
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||||
if (actionID == EditorInfo.IME_ACTION_SEARCH ||
|
displayNameEditText.setOnEditorActionListener(
|
||||||
actionID == EditorInfo.IME_ACTION_DONE ||
|
OnEditorActionListener { _, actionID, event ->
|
||||||
(event.action == KeyEvent.ACTION_DOWN &&
|
if (actionID == EditorInfo.IME_ACTION_SEARCH ||
|
||||||
event.keyCode == KeyEvent.KEYCODE_ENTER)) {
|
actionID == EditorInfo.IME_ACTION_DONE ||
|
||||||
this.register()
|
(event.action == KeyEvent.ACTION_DOWN &&
|
||||||
return@OnEditorActionListener true
|
event.keyCode == KeyEvent.KEYCODE_ENTER)) {
|
||||||
}
|
register()
|
||||||
false
|
return@OnEditorActionListener true
|
||||||
})
|
}
|
||||||
registerButton.setOnClickListener { register() }
|
false
|
||||||
|
})
|
||||||
|
registerButton.setOnClickListener { register() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun register() {
|
private fun register() {
|
||||||
val displayName = displayNameEditText.text.toString().trim()
|
val displayName = binding.displayNameEditText.text.toString().trim()
|
||||||
if (displayName.isEmpty()) {
|
if (displayName.isEmpty()) {
|
||||||
return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@ -45,7 +49,7 @@ class DisplayNameActivity : BaseActionBarActivity() {
|
|||||||
return Toast.makeText(this, R.string.activity_display_name_display_name_too_long_error, Toast.LENGTH_SHORT).show()
|
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
|
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0)
|
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
||||||
TextSecurePreferences.setProfileName(this, displayName)
|
TextSecurePreferences.setProfileName(this, displayName)
|
||||||
val intent = Intent(this, PNModeActivity::class.java)
|
val intent = Intent(this, PNModeActivity::class.java)
|
||||||
push(intent)
|
push(intent)
|
||||||
|
@ -3,19 +3,17 @@ package org.thoughtcrime.securesms.onboarding
|
|||||||
import android.animation.FloatEvaluator
|
import android.animation.FloatEvaluator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.LAYOUT_INFLATER_SERVICE
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
import kotlinx.android.synthetic.main.view_fake_chat.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewFakeChatBinding
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
|
|
||||||
class FakeChatView : ScrollView {
|
class FakeChatView : ScrollView {
|
||||||
|
private lateinit var binding: ViewFakeChatBinding
|
||||||
// region Settings
|
// region Settings
|
||||||
private val spacing = context.resources.getDimension(R.dimen.medium_spacing)
|
private val spacing = context.resources.getDimension(R.dimen.medium_spacing)
|
||||||
private val startDelay: Long = 1000
|
private val startDelay: Long = 1000
|
||||||
@ -41,17 +39,15 @@ class FakeChatView : ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpViewHierarchy() {
|
private fun setUpViewHierarchy() {
|
||||||
val inflater = context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
binding = ViewFakeChatBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
val contentView = inflater.inflate(R.layout.view_fake_chat, null) as LinearLayout
|
binding.root.disableClipping()
|
||||||
contentView.disableClipping()
|
|
||||||
addView(contentView)
|
|
||||||
isVerticalScrollBarEnabled = false
|
isVerticalScrollBarEnabled = false
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Animation
|
// region Animation
|
||||||
fun startAnimating() {
|
fun startAnimating() {
|
||||||
listOf( bubble1, bubble2, bubble3, bubble4, bubble5 ).forEach { it.alpha = 0.0f }
|
listOf( binding.bubble1, binding.bubble2, binding.bubble3, binding.bubble4, binding.bubble5 ).forEach { it.alpha = 0.0f }
|
||||||
fun show(bubble: View) {
|
fun show(bubble: View) {
|
||||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||||
animation.duration = animationDuration
|
animation.duration = animationDuration
|
||||||
@ -61,18 +57,18 @@ class FakeChatView : ScrollView {
|
|||||||
animation.start()
|
animation.start()
|
||||||
}
|
}
|
||||||
Handler().postDelayed({
|
Handler().postDelayed({
|
||||||
show(bubble1)
|
show(binding.bubble1)
|
||||||
Handler().postDelayed({
|
Handler().postDelayed({
|
||||||
show(bubble2)
|
show(binding.bubble2)
|
||||||
Handler().postDelayed({
|
Handler().postDelayed({
|
||||||
show(bubble3)
|
show(binding.bubble3)
|
||||||
smoothScrollTo(0, (bubble1.height + spacing).toInt())
|
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt())
|
||||||
Handler().postDelayed({
|
Handler().postDelayed({
|
||||||
show(bubble4)
|
show(binding.bubble4)
|
||||||
smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt())
|
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt())
|
||||||
Handler().postDelayed({
|
Handler().postDelayed({
|
||||||
show(bubble5)
|
show(binding.bubble5)
|
||||||
smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt() + (bubble3.height + spacing).toInt())
|
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt() + (binding.bubble3.height + spacing).toInt())
|
||||||
}, delayBetweenMessages)
|
}, delayBetweenMessages)
|
||||||
}, delayBetweenMessages)
|
}, delayBetweenMessages)
|
||||||
}, delayBetweenMessages)
|
}, delayBetweenMessages)
|
||||||
|
@ -2,25 +2,27 @@ package org.thoughtcrime.securesms.onboarding
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import network.loki.messenger.databinding.ActivityLandingBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
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.service.KeyCachingService
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
|
||||||
|
|
||||||
class LandingActivity : BaseActionBarActivity() {
|
class LandingActivity : BaseActionBarActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_landing)
|
val binding = ActivityLandingBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
setUpActionBarSessionLogo(true)
|
setUpActionBarSessionLogo(true)
|
||||||
findViewById<FakeChatView>(R.id.fakeChatView).startAnimating()
|
with(binding) {
|
||||||
findViewById<View>(R.id.registerButton).setOnClickListener { register() }
|
fakeChatView.startAnimating()
|
||||||
findViewById<View>(R.id.restoreButton).setOnClickListener { restore() }
|
registerButton.setOnClickListener { register() }
|
||||||
findViewById<View>(R.id.linkButton).setOnClickListener { link() }
|
restoreButton.setOnClickListener { restore() }
|
||||||
|
linkButton.setOnClickListener { link() }
|
||||||
|
}
|
||||||
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.
|
||||||
|
@ -4,7 +4,9 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
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
|
||||||
@ -13,14 +15,13 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.FragmentPagerAdapter
|
import androidx.fragment.app.FragmentPagerAdapter
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.activity_link_device.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_recovery_phrase.*
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
|
||||||
|
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
import org.session.libsignal.crypto.MnemonicCodec
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
@ -30,13 +31,14 @@ import org.session.libsignal.utilities.hexEncodedPublicKey
|
|||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||||
|
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
|
|
||||||
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||||
|
private lateinit var binding: ActivityLinkDeviceBinding
|
||||||
private val adapter = LinkDeviceActivityAdapter(this)
|
private val adapter = LinkDeviceActivityAdapter(this)
|
||||||
private var restoreJob: Job? = null
|
private var restoreJob: Job? = null
|
||||||
|
|
||||||
@ -55,9 +57,10 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
|||||||
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
||||||
setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
|
setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
|
||||||
}
|
}
|
||||||
setContentView(R.layout.activity_link_device)
|
binding = ActivityLinkDeviceBinding.inflate(layoutInflater)
|
||||||
viewPager.adapter = adapter
|
setContentView(binding.root)
|
||||||
tabLayout.setupWithViewPager(viewPager)
|
binding.viewPager.adapter = adapter
|
||||||
|
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -107,8 +110,8 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
|||||||
TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
||||||
TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true)
|
TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true)
|
||||||
|
|
||||||
loader.isVisible = true
|
binding.loader.isVisible = true
|
||||||
val snackBar = Snackbar.make(containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE)
|
val snackBar = Snackbar.make(binding.containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE)
|
||||||
.setAction(R.string.registration_activity__skip) { register(true) }
|
.setAction(R.string.registration_activity__skip) { register(true) }
|
||||||
|
|
||||||
val skipJob = launch {
|
val skipJob = launch {
|
||||||
@ -127,13 +130,13 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
|||||||
register(false)
|
register(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
loader.isVisible = false
|
binding.loader.isVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun register(skipped: Boolean) {
|
private fun register(skipped: Boolean) {
|
||||||
restoreJob?.cancel()
|
restoreJob?.cancel()
|
||||||
loader.isVisible = false
|
binding.loader.isVisible = false
|
||||||
TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis())
|
TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis())
|
||||||
val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java)
|
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
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
@ -175,30 +178,34 @@ private class LinkDeviceActivityAdapter(private val activity: LinkDeviceActivity
|
|||||||
|
|
||||||
// region Recovery Phrase Fragment
|
// region Recovery Phrase Fragment
|
||||||
class RecoveryPhraseFragment : Fragment() {
|
class RecoveryPhraseFragment : Fragment() {
|
||||||
|
private lateinit var binding: FragmentRecoveryPhraseBinding
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return inflater.inflate(R.layout.fragment_recovery_phrase, container, false)
|
binding = FragmentRecoveryPhraseBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
with(binding) {
|
||||||
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||||
mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
|
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
handleContinueButtonTapped()
|
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||||
true
|
handleContinueButtonTapped()
|
||||||
} else {
|
true
|
||||||
false
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
continueButton.setOnClickListener { handleContinueButtonTapped() }
|
||||||
}
|
}
|
||||||
continueButton.setOnClickListener { handleContinueButtonTapped() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleContinueButtonTapped() {
|
private fun handleContinueButtonTapped() {
|
||||||
val mnemonic = mnemonicEditText.text?.trim().toString()
|
val mnemonic = binding.mnemonicEditText.text?.trim().toString()
|
||||||
(requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic)
|
(requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,8 @@ import android.view.View
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import kotlinx.android.synthetic.main.activity_display_name.registerButton
|
|
||||||
import kotlinx.android.synthetic.main.activity_pn_mode.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivityPnModeBinding
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
@ -28,6 +27,7 @@ import org.thoughtcrime.securesms.util.GlowViewUtilities
|
|||||||
import org.thoughtcrime.securesms.util.PNModeView
|
import org.thoughtcrime.securesms.util.PNModeView
|
||||||
|
|
||||||
class PNModeActivity : BaseActionBarActivity() {
|
class PNModeActivity : BaseActionBarActivity() {
|
||||||
|
private lateinit var binding: ActivityPnModeBinding
|
||||||
private var selectedOptionView: PNModeView? = null
|
private var selectedOptionView: PNModeView? = null
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
@ -35,15 +35,18 @@ class PNModeActivity : BaseActionBarActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setUpActionBarSessionLogo(true)
|
setUpActionBarSessionLogo(true)
|
||||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||||
setContentView(R.layout.activity_pn_mode)
|
binding = ActivityPnModeBinding.inflate(layoutInflater)
|
||||||
contentView.disableClipping()
|
setContentView(binding.root)
|
||||||
fcmOptionView.setOnClickListener { toggleFCM() }
|
with(binding) {
|
||||||
fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
contentView.disableClipping()
|
||||||
fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
fcmOptionView.setOnClickListener { toggleFCM() }
|
||||||
backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() }
|
fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
||||||
backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
||||||
backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() }
|
||||||
registerButton.setOnClickListener { register() }
|
backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
||||||
|
backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
||||||
|
registerButton.setOnClickListener { register() }
|
||||||
|
}
|
||||||
toggleFCM()
|
toggleFCM()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,8 +66,7 @@ class PNModeActivity : BaseActionBarActivity() {
|
|||||||
|
|
||||||
// region Interaction
|
// region Interaction
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
val id = item.itemId
|
when(item.itemId) {
|
||||||
when(id) {
|
|
||||||
R.id.learnMoreButton -> learnMore()
|
R.id.learnMoreButton -> learnMore()
|
||||||
else -> { /* Do nothing */ }
|
else -> { /* Do nothing */ }
|
||||||
}
|
}
|
||||||
@ -81,52 +83,52 @@ class PNModeActivity : BaseActionBarActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleFCM() {
|
private fun toggleFCM() = with(binding) {
|
||||||
when (selectedOptionView) {
|
when (selectedOptionView) {
|
||||||
null -> {
|
null -> {
|
||||||
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
|
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
|
||||||
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.transparent, R.color.accent)
|
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.transparent, R.color.accent)
|
||||||
animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent)
|
animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent)
|
||||||
selectedOptionView = fcmOptionView
|
selectedOptionView = fcmOptionView
|
||||||
}
|
}
|
||||||
fcmOptionView -> {
|
fcmOptionView -> {
|
||||||
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
|
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
|
||||||
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.accent, R.color.transparent)
|
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.accent, R.color.transparent)
|
||||||
animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border)
|
animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border)
|
||||||
selectedOptionView = null
|
selectedOptionView = null
|
||||||
}
|
}
|
||||||
backgroundPollingOptionView -> {
|
backgroundPollingOptionView -> {
|
||||||
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
|
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
|
||||||
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.transparent, R.color.accent)
|
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.transparent, R.color.accent)
|
||||||
animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent)
|
animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent)
|
||||||
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
|
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
|
||||||
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.accent, R.color.transparent)
|
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.accent, R.color.transparent)
|
||||||
animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border)
|
animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border)
|
||||||
selectedOptionView = fcmOptionView
|
selectedOptionView = fcmOptionView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleBackgroundPolling() {
|
private fun toggleBackgroundPolling() = with(binding) {
|
||||||
when (selectedOptionView) {
|
when (selectedOptionView) {
|
||||||
null -> {
|
null -> {
|
||||||
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
|
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
|
||||||
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.transparent, R.color.accent)
|
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.transparent, R.color.accent)
|
||||||
animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent)
|
animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent)
|
||||||
selectedOptionView = backgroundPollingOptionView
|
selectedOptionView = backgroundPollingOptionView
|
||||||
}
|
}
|
||||||
backgroundPollingOptionView -> {
|
backgroundPollingOptionView -> {
|
||||||
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
|
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
|
||||||
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.accent, R.color.transparent)
|
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.accent, R.color.transparent)
|
||||||
animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border)
|
animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border)
|
||||||
selectedOptionView = null
|
selectedOptionView = null
|
||||||
}
|
}
|
||||||
fcmOptionView -> {
|
fcmOptionView -> {
|
||||||
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
|
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
|
||||||
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.transparent, R.color.accent)
|
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.transparent, R.color.accent)
|
||||||
animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent)
|
animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent)
|
||||||
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
|
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
|
||||||
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.accent, R.color.transparent)
|
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.accent, R.color.transparent)
|
||||||
animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border)
|
animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border)
|
||||||
selectedOptionView = backgroundPollingOptionView
|
selectedOptionView = backgroundPollingOptionView
|
||||||
}
|
}
|
||||||
@ -153,7 +155,7 @@ class PNModeActivity : BaseActionBarActivity() {
|
|||||||
dialog.create().show()
|
dialog.create().show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView))
|
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView))
|
||||||
val application = ApplicationContext.getInstance(this)
|
val application = ApplicationContext.getInstance(this)
|
||||||
application.startPollingIfNeeded()
|
application.startPollingIfNeeded()
|
||||||
application.registerForFCMIfNeeded(true)
|
application.registerForFCMIfNeeded(true)
|
||||||
|
@ -11,8 +11,8 @@ import android.text.style.ClickableSpan
|
|||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import kotlinx.android.synthetic.main.activity_recovery_phrase_restore.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
import org.session.libsignal.crypto.MnemonicCodec
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.util.push
|
|||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
|
|
||||||
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||||
|
private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -36,9 +36,10 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
|||||||
setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
||||||
setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
||||||
}
|
}
|
||||||
setContentView(R.layout.activity_recovery_phrase_restore)
|
binding = ActivityRecoveryPhraseRestoreBinding.inflate(layoutInflater)
|
||||||
mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
|
setContentView(binding.root)
|
||||||
restoreButton.setOnClickListener { restore() }
|
binding.mnemonicEditText.imeOptions = binding.mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||||
|
binding.restoreButton.setOnClickListener { restore() }
|
||||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
|
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(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
termsExplanation.setSpan(object : ClickableSpan() {
|
termsExplanation.setSpan(object : ClickableSpan() {
|
||||||
@ -54,14 +55,14 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
|||||||
openURL("https://getsession.org/privacy-policy/")
|
openURL("https://getsession.org/privacy-policy/")
|
||||||
}
|
}
|
||||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
termsTextView.text = termsExplanation
|
binding.termsTextView.text = termsExplanation
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Interaction
|
// region Interaction
|
||||||
private fun restore() {
|
private fun restore() {
|
||||||
val mnemonic = mnemonicEditText.text.toString()
|
val mnemonic = binding.mnemonicEditText.text.toString()
|
||||||
try {
|
try {
|
||||||
val loadFileContents: (String) -> String = { fileName ->
|
val loadFileContents: (String) -> String = { fileName ->
|
||||||
MnemonicUtilities.loadFileContents(this, fileName)
|
MnemonicUtilities.loadFileContents(this, fileName)
|
||||||
|
@ -16,8 +16,8 @@ import android.text.style.StyleSpan
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
import kotlinx.android.synthetic.main.activity_register.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivityRegisterBinding
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
import org.session.libsignal.utilities.KeyHelper
|
import org.session.libsignal.utilities.KeyHelper
|
||||||
@ -26,9 +26,9 @@ import org.thoughtcrime.securesms.BaseActionBarActivity
|
|||||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class RegisterActivity : BaseActionBarActivity() {
|
class RegisterActivity : BaseActionBarActivity() {
|
||||||
|
private lateinit var binding: ActivityRegisterBinding
|
||||||
private var seed: ByteArray? = null
|
private var seed: ByteArray? = null
|
||||||
private var ed25519KeyPair: KeyPair? = null
|
private var ed25519KeyPair: KeyPair? = null
|
||||||
private var x25519KeyPair: ECKeyPair? = null
|
private var x25519KeyPair: ECKeyPair? = null
|
||||||
@ -37,7 +37,8 @@ class RegisterActivity : BaseActionBarActivity() {
|
|||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_register)
|
binding = ActivityRegisterBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
setUpActionBarSessionLogo()
|
setUpActionBarSessionLogo()
|
||||||
TextSecurePreferences.apply {
|
TextSecurePreferences.apply {
|
||||||
setHasViewedSeed(this@RegisterActivity, false)
|
setHasViewedSeed(this@RegisterActivity, false)
|
||||||
@ -45,8 +46,8 @@ class RegisterActivity : BaseActionBarActivity() {
|
|||||||
setRestorationTime(this@RegisterActivity, 0)
|
setRestorationTime(this@RegisterActivity, 0)
|
||||||
setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis())
|
setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis())
|
||||||
}
|
}
|
||||||
registerButton.setOnClickListener { register() }
|
binding.registerButton.setOnClickListener { register() }
|
||||||
copyButton.setOnClickListener { copyPublicKey() }
|
binding.copyButton.setOnClickListener { copyPublicKey() }
|
||||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
|
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(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
termsExplanation.setSpan(object : ClickableSpan() {
|
termsExplanation.setSpan(object : ClickableSpan() {
|
||||||
@ -62,8 +63,8 @@ class RegisterActivity : BaseActionBarActivity() {
|
|||||||
openURL("https://getsession.org/privacy-policy/")
|
openURL("https://getsession.org/privacy-policy/")
|
||||||
}
|
}
|
||||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
termsTextView.text = termsExplanation
|
binding.termsTextView.text = termsExplanation
|
||||||
updateKeyPair()
|
updateKeyPair()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
@ -94,12 +95,12 @@ class RegisterActivity : BaseActionBarActivity() {
|
|||||||
}
|
}
|
||||||
count += 1
|
count += 1
|
||||||
if (count < limit) {
|
if (count < limit) {
|
||||||
publicKeyTextView.text = mangledHexEncodedPublicKey
|
binding.publicKeyTextView.text = mangledHexEncodedPublicKey
|
||||||
Handler().postDelayed({
|
Handler().postDelayed({
|
||||||
animate()
|
animate()
|
||||||
}, 32)
|
}, 32)
|
||||||
} else {
|
} else {
|
||||||
publicKeyTextView.text = hexEncodedPublicKey
|
binding.publicKeyTextView.text = hexEncodedPublicKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
animate()
|
animate()
|
||||||
|
@ -9,8 +9,8 @@ import android.text.SpannableString
|
|||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import kotlinx.android.synthetic.main.activity_seed.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ActivitySeedBinding
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
import org.session.libsignal.crypto.MnemonicCodec
|
||||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
||||||
@ -21,6 +21,8 @@ import org.thoughtcrime.securesms.util.getColorWithID
|
|||||||
|
|
||||||
class SeedActivity : BaseActionBarActivity() {
|
class SeedActivity : BaseActionBarActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivitySeedBinding
|
||||||
|
|
||||||
private val seed by lazy {
|
private val seed by lazy {
|
||||||
var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED)
|
var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED)
|
||||||
if (hexEncodedSeed == null) {
|
if (hexEncodedSeed == null) {
|
||||||
@ -35,27 +37,30 @@ class SeedActivity : BaseActionBarActivity() {
|
|||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_seed)
|
binding = ActivitySeedBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_seed_title)
|
supportActionBar!!.title = resources.getString(R.string.activity_seed_title)
|
||||||
val seedReminderViewTitle = SpannableString("You're almost finished! 90%") // Intentionally not yet translated
|
val seedReminderViewTitle = SpannableString("You're almost finished! 90%") // Intentionally not yet translated
|
||||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
seedReminderView.title = seedReminderViewTitle
|
with(binding) {
|
||||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2)
|
seedReminderView.title = seedReminderViewTitle
|
||||||
seedReminderView.setProgress(90, false)
|
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2)
|
||||||
seedReminderView.hideContinueButton()
|
seedReminderView.setProgress(90, false)
|
||||||
var redactedSeed = seed
|
seedReminderView.hideContinueButton()
|
||||||
var index = 0
|
var redactedSeed = seed
|
||||||
for (character in seed) {
|
var index = 0
|
||||||
if (character.isLetter()) {
|
for (character in seed) {
|
||||||
redactedSeed = redactedSeed.replaceRange(index, index + 1, "▆")
|
if (character.isLetter()) {
|
||||||
|
redactedSeed = redactedSeed.replaceRange(index, index + 1, "▆")
|
||||||
|
}
|
||||||
|
index += 1
|
||||||
}
|
}
|
||||||
index += 1
|
seedTextView.setTextColor(resources.getColorWithID(R.color.accent, theme))
|
||||||
|
seedTextView.text = redactedSeed
|
||||||
|
seedTextView.setOnLongClickListener { revealSeed(); true }
|
||||||
|
revealButton.setOnLongClickListener { revealSeed(); true }
|
||||||
|
copyButton.setOnClickListener { copySeed() }
|
||||||
}
|
}
|
||||||
seedTextView.setTextColor(resources.getColorWithID(R.color.accent, theme))
|
|
||||||
seedTextView.text = redactedSeed
|
|
||||||
seedTextView.setOnLongClickListener { revealSeed(); true }
|
|
||||||
revealButton.setOnLongClickListener { revealSeed(); true }
|
|
||||||
copyButton.setOnClickListener { copySeed() }
|
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -63,14 +68,16 @@ class SeedActivity : BaseActionBarActivity() {
|
|||||||
private fun revealSeed() {
|
private fun revealSeed() {
|
||||||
val seedReminderViewTitle = SpannableString("Account secured! 100%") // Intentionally not yet translated
|
val seedReminderViewTitle = SpannableString("Account secured! 100%") // Intentionally not yet translated
|
||||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 17, 21, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 17, 21, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
seedReminderView.title = seedReminderViewTitle
|
with(binding) {
|
||||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3)
|
seedReminderView.title = seedReminderViewTitle
|
||||||
seedReminderView.setProgress(100, true)
|
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3)
|
||||||
val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams
|
seedReminderView.setProgress(100, true)
|
||||||
seedTextViewLayoutParams.height = seedTextView.height
|
val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams
|
||||||
seedTextView.layoutParams = seedTextViewLayoutParams
|
seedTextViewLayoutParams.height = seedTextView.height
|
||||||
seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
|
seedTextView.layoutParams = seedTextViewLayoutParams
|
||||||
seedTextView.text = seed
|
seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
|
||||||
|
seedTextView.text = seed
|
||||||
|
}
|
||||||
TextSecurePreferences.setHasViewedSeed(this, true)
|
TextSecurePreferences.setHasViewedSeed(this, true)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
@ -6,16 +6,17 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import kotlinx.android.synthetic.main.view_seed_reminder.view.*
|
import network.loki.messenger.databinding.ViewSeedReminderBinding
|
||||||
import network.loki.messenger.R
|
|
||||||
|
|
||||||
class SeedReminderView : FrameLayout {
|
class SeedReminderView : FrameLayout {
|
||||||
|
private lateinit var binding: ViewSeedReminderBinding
|
||||||
|
|
||||||
var title: CharSequence
|
var title: CharSequence
|
||||||
get() = titleTextView.text
|
get() = binding.titleTextView.text
|
||||||
set(value) { titleTextView.text = value }
|
set(value) { binding.titleTextView.text = value }
|
||||||
var subtitle: CharSequence
|
var subtitle: CharSequence
|
||||||
get() = subtitleTextView.text
|
get() = binding.subtitleTextView.text
|
||||||
set(value) { subtitleTextView.text = value }
|
set(value) { binding.subtitleTextView.text = value }
|
||||||
var delegate: SeedReminderViewDelegate? = null
|
var delegate: SeedReminderViewDelegate? = null
|
||||||
|
|
||||||
constructor(context: Context) : super(context) {
|
constructor(context: Context) : super(context) {
|
||||||
@ -35,22 +36,20 @@ class SeedReminderView : FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpViewHierarchy() {
|
private fun setUpViewHierarchy() {
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
binding = ViewSeedReminderBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
val contentView = inflater.inflate(R.layout.view_seed_reminder, null)
|
binding.button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() }
|
||||||
addView(contentView)
|
|
||||||
button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProgress(progress: Int, isAnimated: Boolean) {
|
fun setProgress(progress: Int, isAnimated: Boolean) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
progressBar.setProgress(progress, isAnimated)
|
binding.progressBar.setProgress(progress, isAnimated)
|
||||||
} else {
|
} else {
|
||||||
progressBar.progress = progress
|
binding.progressBar.progress = progress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideContinueButton() {
|
fun hideContinueButton() {
|
||||||
button.visibility = View.GONE
|
binding.button.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,10 +4,12 @@ import android.view.LayoutInflater
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.android.synthetic.main.dialog_clear_all_data.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.android.synthetic.main.dialog_clear_all_data.view.*
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.DialogClearAllDataBinding
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
@ -15,6 +17,7 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||||
|
|
||||||
class ClearAllDataDialog : BaseDialog() {
|
class ClearAllDataDialog : BaseDialog() {
|
||||||
|
private lateinit var binding: DialogClearAllDataBinding
|
||||||
|
|
||||||
enum class Steps {
|
enum class Steps {
|
||||||
INFO_PROMPT,
|
INFO_PROMPT,
|
||||||
@ -34,15 +37,15 @@ class ClearAllDataDialog : BaseDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun setContentView(builder: AlertDialog.Builder) {
|
override fun setContentView(builder: AlertDialog.Builder) {
|
||||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null)
|
binding = DialogClearAllDataBinding.inflate(LayoutInflater.from(requireContext()))
|
||||||
contentView.cancelButton.setOnClickListener {
|
binding.cancelButton.setOnClickListener {
|
||||||
if (step == Steps.NETWORK_PROMPT) {
|
if (step == Steps.NETWORK_PROMPT) {
|
||||||
clearAllData(false)
|
clearAllData(false)
|
||||||
} else if (step != Steps.DELETING) {
|
} else if (step != Steps.DELETING) {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentView.clearAllDataButton.setOnClickListener {
|
binding.clearAllDataButton.setOnClickListener {
|
||||||
when(step) {
|
when(step) {
|
||||||
Steps.INFO_PROMPT -> step = Steps.NETWORK_PROMPT
|
Steps.INFO_PROMPT -> step = Steps.NETWORK_PROMPT
|
||||||
Steps.NETWORK_PROMPT -> {
|
Steps.NETWORK_PROMPT -> {
|
||||||
@ -51,36 +54,33 @@ class ClearAllDataDialog : BaseDialog() {
|
|||||||
Steps.DELETING -> { /* do nothing intentionally */ }
|
Steps.DELETING -> { /* do nothing intentionally */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.setView(contentView)
|
builder.setView(binding.root)
|
||||||
builder.setCancelable(false)
|
builder.setCancelable(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateUI() {
|
private fun updateUI() {
|
||||||
|
dialog?.let {
|
||||||
dialog?.let { view ->
|
|
||||||
|
|
||||||
val isLoading = step == Steps.DELETING
|
val isLoading = step == Steps.DELETING
|
||||||
|
|
||||||
when (step) {
|
when (step) {
|
||||||
Steps.INFO_PROMPT -> {
|
Steps.INFO_PROMPT -> {
|
||||||
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation)
|
binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation)
|
||||||
view.cancelButton.setText(R.string.cancel)
|
binding.cancelButton.setText(R.string.cancel)
|
||||||
view.clearAllDataButton.setText(R.string.delete)
|
binding.clearAllDataButton.setText(R.string.delete)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation)
|
binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation)
|
||||||
view.cancelButton.setText(R.string.dialog_clear_all_data_local_only)
|
binding.cancelButton.setText(R.string.dialog_clear_all_data_local_only)
|
||||||
view.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network)
|
binding.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view.cancelButton.isVisible = !isLoading
|
binding.cancelButton.isVisible = !isLoading
|
||||||
view.clearAllDataButton.isVisible = !isLoading
|
binding.clearAllDataButton.isVisible = !isLoading
|
||||||
view.progressBar.isVisible = isLoading
|
binding.progressBar.isVisible = isLoading
|
||||||
|
|
||||||
view.setCanceledOnTouchOutside(!isLoading)
|
it.setCanceledOnTouchOutside(!isLoading)
|
||||||
isCancelable = !isLoading
|
isCancelable = !isLoading
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,9 +54,9 @@ public abstract class CorrectedPreferenceFragment extends PreferenceFragmentComp
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
|
protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
|
||||||
return new PreferenceGroupAdapter(preferenceScreen) {
|
return new PreferenceGroupAdapter(preferenceScreen) {
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
|
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
|
||||||
super.onBindViewHolder(holder, position);
|
super.onBindViewHolder(holder, position);
|
||||||
|
@ -10,9 +10,9 @@ import android.view.ViewGroup
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentPagerAdapter
|
import androidx.fragment.app.FragmentPagerAdapter
|
||||||
import kotlinx.android.synthetic.main.activity_qr_code.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_view_my_qr_code.*
|
|
||||||
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
|
||||||
@ -20,23 +20,29 @@ 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.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.*
|
import org.thoughtcrime.securesms.util.FileProviderUtil
|
||||||
|
import org.thoughtcrime.securesms.util.QRCodeUtilities
|
||||||
|
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||||
|
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||||
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||||
|
private lateinit var binding: ActivityQrCodeBinding
|
||||||
private val adapter = QRCodeActivityAdapter(this)
|
private val adapter = QRCodeActivityAdapter(this)
|
||||||
|
|
||||||
// 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)
|
||||||
|
binding = ActivityQrCodeBinding.inflate(layoutInflater)
|
||||||
// Set content view
|
// Set content view
|
||||||
setContentView(R.layout.activity_qr_code)
|
setContentView(binding.root)
|
||||||
// Set title
|
// 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
|
// Set up view pager
|
||||||
viewPager.adapter = adapter
|
binding.viewPager.adapter = adapter
|
||||||
tabLayout.setupWithViewPager(viewPager)
|
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -91,6 +97,7 @@ private class QRCodeActivityAdapter(val activity: QRCodeActivity) : FragmentPage
|
|||||||
|
|
||||||
// region View My QR Code Fragment
|
// region View My QR Code Fragment
|
||||||
class ViewMyQRCodeFragment : Fragment() {
|
class ViewMyQRCodeFragment : Fragment() {
|
||||||
|
private lateinit var binding: FragmentViewMyQrCodeBinding
|
||||||
|
|
||||||
private val hexEncodedPublicKey: String
|
private val hexEncodedPublicKey: String
|
||||||
get() {
|
get() {
|
||||||
@ -98,18 +105,19 @@ class ViewMyQRCodeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return inflater.inflate(R.layout.fragment_view_my_qr_code, container, false)
|
binding = FragmentViewMyQrCodeBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
val size = toPx(280, resources)
|
val size = toPx(280, resources)
|
||||||
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
|
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
|
||||||
qrCodeImageView.setImageBitmap(qrCode)
|
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.")
|
// 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)
|
// explanation.setSpan(StyleSpan(Typeface.BOLD), 8, 34, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation)
|
binding.explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation)
|
||||||
shareButton.setOnClickListener { shareQRCode() }
|
binding.shareButton.setOnClickListener { shareQRCode() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareQRCode() {
|
private fun shareQRCode() {
|
||||||
|
@ -6,8 +6,8 @@ import android.content.Context
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import kotlinx.android.synthetic.main.dialog_seed.view.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.DialogSeedBinding
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
import org.session.libsignal.crypto.MnemonicCodec
|
||||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||||
@ -28,11 +28,11 @@ class SeedDialog : BaseDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun setContentView(builder: AlertDialog.Builder) {
|
override fun setContentView(builder: AlertDialog.Builder) {
|
||||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_seed, null)
|
val binding = DialogSeedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||||
contentView.seedTextView.text = seed
|
binding.seedTextView.text = seed
|
||||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
binding.cancelButton.setOnClickListener { dismiss() }
|
||||||
contentView.copyButton.setOnClickListener { copySeed() }
|
binding.copyButton.setOnClickListener { copySeed() }
|
||||||
builder.setView(contentView)
|
builder.setView(binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copySeed() {
|
private fun copySeed() {
|
||||||
|
@ -7,7 +7,11 @@ import android.content.ClipboardManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.*
|
import android.os.AsyncTask
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.view.ActionMode
|
import android.view.ActionMode
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@ -15,9 +19,9 @@ import android.view.View
|
|||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.activity_settings.*
|
|
||||||
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 nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.all
|
import nl.komponents.kovenant.all
|
||||||
import nl.komponents.kovenant.ui.alwaysUi
|
import nl.komponents.kovenant.ui.alwaysUi
|
||||||
@ -30,16 +34,24 @@ import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
|||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||||
|
import org.thoughtcrime.securesms.home.PathActivity
|
||||||
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.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
||||||
import org.thoughtcrime.securesms.util.*
|
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
||||||
|
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||||
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
|
import org.thoughtcrime.securesms.util.push
|
||||||
|
import org.thoughtcrime.securesms.util.show
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||||
|
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 lateinit var glide: GlideRequests
|
||||||
@ -59,33 +71,38 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
// 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)
|
||||||
setContentView(R.layout.activity_settings)
|
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey
|
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey
|
||||||
glide = GlideApp.with(this)
|
glide = GlideApp.with(this)
|
||||||
profilePictureView.glide = glide
|
with(binding) {
|
||||||
profilePictureView.publicKey = hexEncodedPublicKey
|
profilePictureView.glide = glide
|
||||||
profilePictureView.displayName = displayName
|
profilePictureView.publicKey = hexEncodedPublicKey
|
||||||
profilePictureView.isLarge = true
|
profilePictureView.displayName = displayName
|
||||||
profilePictureView.update()
|
profilePictureView.isLarge = true
|
||||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
profilePictureView.update()
|
||||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
||||||
btnGroupNameDisplay.text = displayName
|
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||||
publicKeyTextView.text = hexEncodedPublicKey
|
btnGroupNameDisplay.text = displayName
|
||||||
copyButton.setOnClickListener { copyPublicKey() }
|
publicKeyTextView.text = hexEncodedPublicKey
|
||||||
shareButton.setOnClickListener { sharePublicKey() }
|
copyButton.setOnClickListener { copyPublicKey() }
|
||||||
privacyButton.setOnClickListener { showPrivacySettings() }
|
shareButton.setOnClickListener { sharePublicKey() }
|
||||||
notificationsButton.setOnClickListener { showNotificationSettings() }
|
pathButton.setOnClickListener { showPath() }
|
||||||
chatsButton.setOnClickListener { showChatSettings() }
|
pathContainer.disableClipping()
|
||||||
sendInvitationButton.setOnClickListener { sendInvitation() }
|
privacyButton.setOnClickListener { showPrivacySettings() }
|
||||||
faqButton.setOnClickListener { showFAQ() }
|
notificationsButton.setOnClickListener { showNotificationSettings() }
|
||||||
surveyButton.setOnClickListener { showSurvey() }
|
chatsButton.setOnClickListener { showChatSettings() }
|
||||||
helpTranslateButton.setOnClickListener { helpTranslate() }
|
sendInvitationButton.setOnClickListener { sendInvitation() }
|
||||||
seedButton.setOnClickListener { showSeed() }
|
faqButton.setOnClickListener { showFAQ() }
|
||||||
clearAllDataButton.setOnClickListener { clearAllData() }
|
surveyButton.setOnClickListener { showSurvey() }
|
||||||
debugLogButton.setOnClickListener { shareLogs() }
|
helpTranslateButton.setOnClickListener { helpTranslate() }
|
||||||
val isLightMode = UiModeUtilities.isDayUiMode(this)
|
seedButton.setOnClickListener { showSeed() }
|
||||||
oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode)
|
clearAllDataButton.setOnClickListener { clearAllData() }
|
||||||
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
debugLogButton.setOnClickListener { shareLogs() }
|
||||||
|
val isLightMode = UiModeUtilities.isDayUiMode(this@SettingsActivity)
|
||||||
|
oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode)
|
||||||
|
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
@ -152,22 +169,22 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
private fun handleDisplayNameEditActionModeChanged() {
|
private fun handleDisplayNameEditActionModeChanged() {
|
||||||
val isEditingDisplayName = this.displayNameEditActionMode !== null
|
val isEditingDisplayName = this.displayNameEditActionMode !== null
|
||||||
|
|
||||||
btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
|
binding.btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
|
||||||
displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
|
binding.displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
|
||||||
|
|
||||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
if (isEditingDisplayName) {
|
if (isEditingDisplayName) {
|
||||||
displayNameEditText.setText(btnGroupNameDisplay.text)
|
binding.displayNameEditText.setText(binding.btnGroupNameDisplay.text)
|
||||||
displayNameEditText.selectAll()
|
binding.displayNameEditText.selectAll()
|
||||||
displayNameEditText.requestFocus()
|
binding.displayNameEditText.requestFocus()
|
||||||
inputMethodManager.showSoftInput(displayNameEditText, 0)
|
inputMethodManager.showSoftInput(binding.displayNameEditText, 0)
|
||||||
} else {
|
} else {
|
||||||
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0)
|
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateProfile(isUpdatingProfilePicture: Boolean) {
|
private fun updateProfile(isUpdatingProfilePicture: Boolean) {
|
||||||
loader.isVisible = true
|
binding.loader.isVisible = true
|
||||||
val promises = mutableListOf<Promise<*, Exception>>()
|
val promises = mutableListOf<Promise<*, Exception>>()
|
||||||
val displayName = displayNameToBeUploaded
|
val displayName = displayNameToBeUploaded
|
||||||
if (displayName != null) {
|
if (displayName != null) {
|
||||||
@ -192,15 +209,15 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
compoundPromise.alwaysUi {
|
compoundPromise.alwaysUi {
|
||||||
if (displayName != null) {
|
if (displayName != null) {
|
||||||
btnGroupNameDisplay.text = displayName
|
binding.btnGroupNameDisplay.text = displayName
|
||||||
}
|
}
|
||||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||||
profilePictureView.recycle() // Clear the cached image before updating
|
binding.profilePictureView.recycle() // Clear the cached image before updating
|
||||||
profilePictureView.update()
|
binding.profilePictureView.update()
|
||||||
}
|
}
|
||||||
displayNameToBeUploaded = null
|
displayNameToBeUploaded = null
|
||||||
profilePictureToBeUploaded = null
|
profilePictureToBeUploaded = null
|
||||||
loader.isVisible = false
|
binding.loader.isVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
@ -211,7 +228,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
* @return true if the update was successful.
|
* @return true if the update was successful.
|
||||||
*/
|
*/
|
||||||
private fun saveDisplayName(): Boolean {
|
private fun saveDisplayName(): Boolean {
|
||||||
val displayName = displayNameEditText.text.toString().trim()
|
val displayName = binding.displayNameEditText.text.toString().trim()
|
||||||
if (displayName.isEmpty()) {
|
if (displayName.isEmpty()) {
|
||||||
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
|
||||||
@ -291,6 +308,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showPath() {
|
||||||
|
val intent = Intent(this, PathActivity::class.java)
|
||||||
|
show(intent)
|
||||||
|
}
|
||||||
|
|
||||||
private fun showSurvey() {
|
private fun showSurvey() {
|
||||||
try {
|
try {
|
||||||
val url = "https://getsession.org/survey"
|
val url = "https://getsession.org/survey"
|
||||||
|
@ -13,12 +13,12 @@ import android.webkit.MimeTypeMap
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.android.synthetic.main.dialog_share_logs.view.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
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.DialogShareLogsBinding
|
||||||
import org.session.libsignal.utilities.ExternalStorageUtil
|
import org.session.libsignal.utilities.ExternalStorageUtil
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||||
@ -26,7 +26,7 @@ import org.thoughtcrime.securesms.util.StreamUtil
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.Objects
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ShareLogsDialog : BaseDialog() {
|
class ShareLogsDialog : BaseDialog() {
|
||||||
@ -34,16 +34,15 @@ class ShareLogsDialog : BaseDialog() {
|
|||||||
private var shareJob: Job? = null
|
private var shareJob: Job? = null
|
||||||
|
|
||||||
override fun setContentView(builder: AlertDialog.Builder) {
|
override fun setContentView(builder: AlertDialog.Builder) {
|
||||||
val contentView =
|
val binding = DialogShareLogsBinding.inflate(LayoutInflater.from(requireContext()))
|
||||||
LayoutInflater.from(requireContext()).inflate(R.layout.dialog_share_logs, null)
|
binding.cancelButton.setOnClickListener {
|
||||||
contentView.cancelButton.setOnClickListener {
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
contentView.shareButton.setOnClickListener {
|
binding.shareButton.setOnClickListener {
|
||||||
// start the export and share
|
// start the export and share
|
||||||
shareLogs()
|
shareLogs()
|
||||||
}
|
}
|
||||||
builder.setView(contentView)
|
builder.setView(binding.root)
|
||||||
builder.setCancelable(false)
|
builder.setCancelable(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,229 @@
|
|||||||
|
package org.thoughtcrime.securesms.repository
|
||||||
|
|
||||||
|
import org.session.libsession.database.MessageDataProvider
|
||||||
|
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
||||||
|
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
||||||
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.toHexString
|
||||||
|
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
interface ConversationRepository {
|
||||||
|
fun isOxenHostedOpenGroup(threadId: Long): Boolean
|
||||||
|
fun getRecipientForThreadId(threadId: Long): Recipient
|
||||||
|
fun saveDraft(threadId: Long, text: String)
|
||||||
|
fun getDraft(threadId: Long): String?
|
||||||
|
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
||||||
|
fun unblock(recipient: Recipient)
|
||||||
|
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
||||||
|
|
||||||
|
suspend fun deleteForEveryone(
|
||||||
|
threadId: Long,
|
||||||
|
recipient: Recipient,
|
||||||
|
message: MessageRecord
|
||||||
|
): ResultOf<Unit>
|
||||||
|
|
||||||
|
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
|
||||||
|
|
||||||
|
suspend fun deleteMessageWithoutUnsendRequest(
|
||||||
|
threadId: Long,
|
||||||
|
messages: Set<MessageRecord>
|
||||||
|
): ResultOf<Unit>
|
||||||
|
|
||||||
|
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||||
|
|
||||||
|
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultConversationRepository @Inject constructor(
|
||||||
|
private val textSecurePreferences: TextSecurePreferences,
|
||||||
|
private val messageDataProvider: MessageDataProvider,
|
||||||
|
private val threadDb: ThreadDatabase,
|
||||||
|
private val draftDb: DraftDatabase,
|
||||||
|
private val lokiThreadDb: LokiThreadDatabase,
|
||||||
|
private val smsDb: SmsDatabase,
|
||||||
|
private val mmsDb: MmsDatabase,
|
||||||
|
private val recipientDb: RecipientDatabase,
|
||||||
|
private val lokiMessageDb: LokiMessageDatabase
|
||||||
|
) : ConversationRepository {
|
||||||
|
|
||||||
|
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
|
||||||
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||||
|
return openGroup?.room == "session" || openGroup?.room == "oxen"
|
||||||
|
|| openGroup?.room == "lokinet" || openGroup?.room == "crypto"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRecipientForThreadId(threadId: Long): Recipient {
|
||||||
|
return threadDb.getRecipientForThreadId(threadId)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveDraft(threadId: Long, text: String) {
|
||||||
|
if (text.isEmpty()) return
|
||||||
|
val drafts = DraftDatabase.Drafts()
|
||||||
|
drafts.add(DraftDatabase.Draft(DraftDatabase.Draft.TEXT, text))
|
||||||
|
draftDb.insertDrafts(threadId, drafts)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDraft(threadId: Long): String? {
|
||||||
|
val drafts = draftDb.getDrafts(threadId)
|
||||||
|
draftDb.clearDrafts(threadId)
|
||||||
|
return drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun inviteContacts(threadId: Long, contacts: List<Recipient>) {
|
||||||
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
|
||||||
|
for (contact in contacts) {
|
||||||
|
val message = VisibleMessage()
|
||||||
|
message.sentTimestamp = System.currentTimeMillis()
|
||||||
|
val openGroupInvitation = OpenGroupInvitation()
|
||||||
|
openGroupInvitation.name = openGroup.name
|
||||||
|
openGroupInvitation.url = openGroup.joinURL
|
||||||
|
message.openGroupInvitation = openGroupInvitation
|
||||||
|
val outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation(
|
||||||
|
openGroupInvitation,
|
||||||
|
contact,
|
||||||
|
message.sentTimestamp
|
||||||
|
)
|
||||||
|
smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!)
|
||||||
|
MessageSender.send(message, contact.address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unblock(recipient: Recipient) {
|
||||||
|
recipientDb.setBlocked(recipient, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteLocally(recipient: Recipient, message: MessageRecord) {
|
||||||
|
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||||
|
textSecurePreferences.getLocalNumber()?.let {
|
||||||
|
MessageSender.send(unsendRequest, Address.fromSerialized(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteForEveryone(
|
||||||
|
threadId: Long,
|
||||||
|
recipient: Recipient,
|
||||||
|
message: MessageRecord
|
||||||
|
): ResultOf<Unit> = suspendCoroutine { continuation ->
|
||||||
|
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||||
|
MessageSender.send(unsendRequest, recipient.address)
|
||||||
|
}
|
||||||
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||||
|
if (openGroup != null) {
|
||||||
|
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
|
||||||
|
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||||
|
.success {
|
||||||
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
|
}.fail { error ->
|
||||||
|
continuation.resumeWithException(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
|
messageDataProvider.getServerHashForMessage(message.id)?.let { serverHash ->
|
||||||
|
var publicKey = recipient.address.serialize()
|
||||||
|
if (recipient.isClosedGroupRecipient) {
|
||||||
|
publicKey = GroupUtil.doubleDecodeGroupID(publicKey).toHexString()
|
||||||
|
}
|
||||||
|
SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
|
||||||
|
.success {
|
||||||
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
|
}.fail { error ->
|
||||||
|
continuation.resumeWithException(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? {
|
||||||
|
if (recipient.isOpenGroupRecipient) return null
|
||||||
|
messageDataProvider.getServerHashForMessage(message.id) ?: return null
|
||||||
|
val unsendRequest = UnsendRequest()
|
||||||
|
if (message.isOutgoing) {
|
||||||
|
unsendRequest.author = textSecurePreferences.getLocalNumber()
|
||||||
|
} else {
|
||||||
|
unsendRequest.author = message.individualRecipient.address.contactIdentifier()
|
||||||
|
}
|
||||||
|
unsendRequest.timestamp = message.timestamp
|
||||||
|
|
||||||
|
return unsendRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteMessageWithoutUnsendRequest(
|
||||||
|
threadId: Long,
|
||||||
|
messages: Set<MessageRecord>
|
||||||
|
): ResultOf<Unit> = suspendCoroutine { continuation ->
|
||||||
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||||
|
if (openGroup != null) {
|
||||||
|
val messageServerIDs = mutableMapOf<Long, MessageRecord>()
|
||||||
|
for (message in messages) {
|
||||||
|
val messageServerID =
|
||||||
|
lokiMessageDb.getServerID(message.id, !message.isMms) ?: continue
|
||||||
|
messageServerIDs[messageServerID] = message
|
||||||
|
}
|
||||||
|
for ((messageServerID, message) in messageServerIDs) {
|
||||||
|
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||||
|
.success {
|
||||||
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
|
}.fail { error ->
|
||||||
|
continuation.resumeWithException(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (message in messages) {
|
||||||
|
if (message.isMms) {
|
||||||
|
mmsDb.deleteMessage(message.id)
|
||||||
|
} else {
|
||||||
|
smsDb.deleteMessage(message.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit> =
|
||||||
|
suspendCoroutine { continuation ->
|
||||||
|
val sessionID = recipient.address.toString()
|
||||||
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||||
|
OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server)
|
||||||
|
.success {
|
||||||
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
|
}.fail { error ->
|
||||||
|
continuation.resumeWithException(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> =
|
||||||
|
suspendCoroutine { continuation ->
|
||||||
|
val sessionID = recipient.address.toString()
|
||||||
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||||
|
OpenGroupAPIV2.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
|
||||||
|
.success {
|
||||||
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
|
}.fail { error ->
|
||||||
|
continuation.resumeWithException(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.thoughtcrime.securesms.repository
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
||||||
|
sealed class ResultOf<out T> {
|
||||||
|
|
||||||
|
data class Success<out R>(val value: R) : ResultOf<R>()
|
||||||
|
|
||||||
|
data class Failure(val throwable: Throwable) : ResultOf<Nothing>()
|
||||||
|
|
||||||
|
inline fun onFailure(block: (throwable: Throwable) -> Unit) = this.also {
|
||||||
|
if (this is Failure) {
|
||||||
|
block(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun onSuccess(block: (value: T) -> Unit) = this.also {
|
||||||
|
if (this is Success) {
|
||||||
|
block(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <R> flatMap(mapper: (T) -> R): ResultOf<R> = when (this) {
|
||||||
|
is Success -> wrap { mapper(value) }
|
||||||
|
is Failure -> Failure(throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOrThrow(): T = when (this) {
|
||||||
|
is Success -> value
|
||||||
|
is Failure -> throw throwable
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
inline fun <T> wrap(block: () -> T): ResultOf<T> =
|
||||||
|
try {
|
||||||
|
Success(block())
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package org.thoughtcrime.securesms.search
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.components.ActivityComponent
|
||||||
|
import dagger.hilt.android.components.ViewModelComponent
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
|
import org.session.libsession.utilities.concurrent.SignalExecutors
|
||||||
|
import org.thoughtcrime.securesms.contacts.ContactAccessor
|
||||||
|
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SearchDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(ViewModelComponent::class)
|
||||||
|
object SearchModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ViewModelScoped
|
||||||
|
fun provideSearchRepository(@ApplicationContext context: Context,
|
||||||
|
searchDatabase: SearchDatabase,
|
||||||
|
threadDatabase: ThreadDatabase,
|
||||||
|
groupDatabase: GroupDatabase,
|
||||||
|
contactDatabase: SessionContactDatabase) =
|
||||||
|
SearchRepository(context, searchDatabase, threadDatabase, groupDatabase, contactDatabase, ContactAccessor.getInstance(), SignalExecutors.SERIAL)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -3,29 +3,39 @@ package org.thoughtcrime.securesms.search;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseUtils;
|
import android.database.DatabaseUtils;
|
||||||
import androidx.annotation.NonNull;
|
import android.database.MergeCursor;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
import org.session.libsession.messaging.contacts.Contact;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
|
import org.session.libsession.utilities.GroupRecord;
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||||
import org.thoughtcrime.securesms.database.CursorList;
|
import org.thoughtcrime.securesms.database.CursorList;
|
||||||
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.SessionContactDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
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;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
import kotlin.Pair;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages data retrieval for search.
|
* Manages data retrieval for search.
|
||||||
*/
|
*/
|
||||||
@ -50,21 +60,27 @@ public class SearchRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final SearchDatabase searchDatabase;
|
private final SearchDatabase searchDatabase;
|
||||||
private final ThreadDatabase threadDatabase;
|
private final ThreadDatabase threadDatabase;
|
||||||
private final ContactAccessor contactAccessor;
|
private final GroupDatabase groupDatabase;
|
||||||
private final Executor executor;
|
private final SessionContactDatabase contactDatabase;
|
||||||
|
private final ContactAccessor contactAccessor;
|
||||||
|
private final Executor executor;
|
||||||
|
|
||||||
public SearchRepository(@NonNull Context context,
|
public SearchRepository(@NonNull Context context,
|
||||||
@NonNull SearchDatabase searchDatabase,
|
@NonNull SearchDatabase searchDatabase,
|
||||||
@NonNull ThreadDatabase threadDatabase,
|
@NonNull ThreadDatabase threadDatabase,
|
||||||
|
@NonNull GroupDatabase groupDatabase,
|
||||||
|
@NonNull SessionContactDatabase contactDatabase,
|
||||||
@NonNull ContactAccessor contactAccessor,
|
@NonNull ContactAccessor contactAccessor,
|
||||||
@NonNull Executor executor)
|
@NonNull Executor executor)
|
||||||
{
|
{
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
this.searchDatabase = searchDatabase;
|
this.searchDatabase = searchDatabase;
|
||||||
this.threadDatabase = threadDatabase;
|
this.threadDatabase = threadDatabase;
|
||||||
|
this.groupDatabase = groupDatabase;
|
||||||
|
this.contactDatabase = contactDatabase;
|
||||||
this.contactAccessor = contactAccessor;
|
this.contactAccessor = contactAccessor;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
}
|
}
|
||||||
@ -81,10 +97,10 @@ public class SearchRepository {
|
|||||||
String cleanQuery = sanitizeQuery(query);
|
String cleanQuery = sanitizeQuery(query);
|
||||||
timer.split("clean");
|
timer.split("clean");
|
||||||
|
|
||||||
CursorList<Recipient> contacts = queryContacts(cleanQuery);
|
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
|
||||||
timer.split("contacts");
|
timer.split("contacts");
|
||||||
|
|
||||||
CursorList<ThreadRecord> conversations = queryConversations(cleanQuery);
|
CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond());
|
||||||
timer.split("conversations");
|
timer.split("conversations");
|
||||||
|
|
||||||
CursorList<MessageResult> messages = queryMessages(cleanQuery);
|
CursorList<MessageResult> messages = queryMessages(cleanQuery);
|
||||||
@ -92,7 +108,7 @@ public class SearchRepository {
|
|||||||
|
|
||||||
timer.stop(TAG);
|
timer.stop(TAG);
|
||||||
|
|
||||||
callback.onResult(new SearchResult(cleanQuery, contacts, conversations, messages));
|
callback.onResult(new SearchResult(cleanQuery, contacts.getFirst(), conversations, messages));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,28 +127,62 @@ public class SearchRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private CursorList<Recipient> queryContacts(String query) {
|
private Pair<CursorList<Contact>, List<String>> queryContacts(String query) {
|
||||||
return CursorList.emptyList();
|
|
||||||
/* Loki - We don't need contacts permission
|
Cursor contacts = contactDatabase.queryContactsByName(query);
|
||||||
if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
|
List<Address> contactList = new ArrayList<>();
|
||||||
return CursorList.emptyList();
|
List<String> contactStrings = new ArrayList<>();
|
||||||
|
|
||||||
|
while (contacts.moveToNext()) {
|
||||||
|
try {
|
||||||
|
Contact contact = contactDatabase.contactFromCursor(contacts);
|
||||||
|
String contactSessionId = contact.getSessionID();
|
||||||
|
Address address = Address.fromSerialized(contactSessionId);
|
||||||
|
contactList.add(address);
|
||||||
|
contactStrings.add(contactSessionId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("Loki", "Error building Contact from cursor in query", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cursor textSecureContacts = contactsDatabase.queryTextSecureContacts(query);
|
contacts.close();
|
||||||
Cursor systemContacts = contactsDatabase.querySystemContacts(query);
|
|
||||||
MergeCursor contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
|
Cursor addressThreads = threadDatabase.searchConversationAddresses(query);
|
||||||
|
Cursor individualRecipients = threadDatabase.getFilteredConversationList(contactList);
|
||||||
|
if (individualRecipients == null && addressThreads == null) {
|
||||||
|
return new Pair<>(CursorList.emptyList(),contactStrings);
|
||||||
|
}
|
||||||
|
MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients});
|
||||||
|
|
||||||
|
return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings);
|
||||||
|
|
||||||
return new CursorList<>(contacts, new RecipientModelBuilder(context));
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CursorList<ThreadRecord> queryConversations(@NonNull String query) {
|
private CursorList<GroupRecord> queryConversations(@NonNull String query, List<String> matchingAddresses) {
|
||||||
List<String> numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
|
List<String> numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
|
||||||
List<Address> addresses = Stream.of(numbers).map(number -> Address.fromExternal(context, number)).toList();
|
String localUserNumber = TextSecurePreferences.getLocalNumber(context);
|
||||||
|
if (localUserNumber != null) {
|
||||||
|
matchingAddresses.remove(localUserNumber);
|
||||||
|
}
|
||||||
|
Set<Address> addresses = new HashSet<>(Stream.of(numbers).map(number -> Address.fromExternal(context, number)).toList());
|
||||||
|
|
||||||
Cursor conversations = threadDatabase.getFilteredConversationList(addresses);
|
Cursor membersGroupList = groupDatabase.getGroupsFilteredByMembers(matchingAddresses);
|
||||||
return conversations != null ? new CursorList<>(conversations, new ThreadModelBuilder(threadDatabase))
|
if (membersGroupList != null) {
|
||||||
: CursorList.emptyList();
|
GroupDatabase.Reader reader = new GroupDatabase.Reader(membersGroupList);
|
||||||
|
while (membersGroupList.moveToNext()) {
|
||||||
|
GroupRecord record = reader.getCurrent();
|
||||||
|
if (record == null) continue;
|
||||||
|
|
||||||
|
addresses.add(Address.fromSerialized(record.getEncodedId()));
|
||||||
|
}
|
||||||
|
membersGroupList.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses));
|
||||||
|
|
||||||
|
return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase))
|
||||||
|
: CursorList.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private CursorList<MessageResult> queryMessages(@NonNull String query) {
|
private CursorList<MessageResult> queryMessages(@NonNull String query) {
|
||||||
@ -169,6 +219,28 @@ public class SearchRepository {
|
|||||||
return out.toString();
|
return out.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class ContactModelBuilder implements CursorList.ModelBuilder<Contact> {
|
||||||
|
|
||||||
|
private final SessionContactDatabase contactDb;
|
||||||
|
private final ThreadDatabase threadDb;
|
||||||
|
|
||||||
|
public ContactModelBuilder(SessionContactDatabase contactDb, ThreadDatabase threadDb) {
|
||||||
|
this.contactDb = contactDb;
|
||||||
|
this.threadDb = threadDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Contact build(@NonNull Cursor cursor) {
|
||||||
|
ThreadRecord threadRecord = threadDb.readerFor(cursor).getCurrent();
|
||||||
|
Contact contact = contactDb.getContactWithSessionID(threadRecord.getRecipient().getAddress().serialize());
|
||||||
|
if (contact == null) {
|
||||||
|
contact = new Contact(threadRecord.getRecipient().getAddress().serialize());
|
||||||
|
contact.setThreadID(threadRecord.getThreadId());
|
||||||
|
}
|
||||||
|
return contact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class RecipientModelBuilder implements CursorList.ModelBuilder<Recipient> {
|
private static class RecipientModelBuilder implements CursorList.ModelBuilder<Recipient> {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -184,6 +256,22 @@ public class SearchRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class GroupModelBuilder implements CursorList.ModelBuilder<GroupRecord> {
|
||||||
|
private final ThreadDatabase threadDatabase;
|
||||||
|
private final GroupDatabase groupDatabase;
|
||||||
|
|
||||||
|
public GroupModelBuilder(ThreadDatabase threadDatabase, GroupDatabase groupDatabase) {
|
||||||
|
this.threadDatabase = threadDatabase;
|
||||||
|
this.groupDatabase = groupDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GroupRecord build(@NonNull Cursor cursor) {
|
||||||
|
ThreadRecord threadRecord = threadDatabase.readerFor(cursor).getCurrent();
|
||||||
|
return groupDatabase.getGroup(threadRecord.getRecipient().getAddress().toGroupString()).get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class ThreadModelBuilder implements CursorList.ModelBuilder<ThreadRecord> {
|
private static class ThreadModelBuilder implements CursorList.ModelBuilder<ThreadRecord> {
|
||||||
|
|
||||||
private final ThreadDatabase threadDatabase;
|
private final ThreadDatabase threadDatabase;
|
||||||
@ -208,7 +296,7 @@ public class SearchRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MessageResult build(@NonNull Cursor cursor) {
|
public MessageResult build(@NonNull Cursor cursor) {
|
||||||
Address conversationAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndex(SearchDatabase.CONVERSATION_ADDRESS)));
|
Address conversationAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.CONVERSATION_ADDRESS)));
|
||||||
Address messageAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.MESSAGE_ADDRESS)));
|
Address messageAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.MESSAGE_ADDRESS)));
|
||||||
Recipient conversationRecipient = Recipient.from(context, conversationAddress, false);
|
Recipient conversationRecipient = Recipient.from(context, conversationAddress, false);
|
||||||
Recipient messageRecipient = Recipient.from(context, messageAddress, false);
|
Recipient messageRecipient = Recipient.from(context, messageAddress, false);
|
||||||
|
@ -4,9 +4,10 @@ import android.database.ContentObserver;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.contacts.Contact;
|
||||||
|
import org.session.libsession.utilities.GroupRecord;
|
||||||
import org.thoughtcrime.securesms.database.CursorList;
|
import org.thoughtcrime.securesms.database.CursorList;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -19,13 +20,13 @@ public class SearchResult {
|
|||||||
public static final SearchResult EMPTY = new SearchResult("", CursorList.emptyList(), CursorList.emptyList(), CursorList.emptyList());
|
public static final SearchResult EMPTY = new SearchResult("", CursorList.emptyList(), CursorList.emptyList(), CursorList.emptyList());
|
||||||
|
|
||||||
private final String query;
|
private final String query;
|
||||||
private final CursorList<Recipient> contacts;
|
private final CursorList<Contact> contacts;
|
||||||
private final CursorList<ThreadRecord> conversations;
|
private final CursorList<GroupRecord> conversations;
|
||||||
private final CursorList<MessageResult> messages;
|
private final CursorList<MessageResult> messages;
|
||||||
|
|
||||||
public SearchResult(@NonNull String query,
|
public SearchResult(@NonNull String query,
|
||||||
@NonNull CursorList<Recipient> contacts,
|
@NonNull CursorList<Contact> contacts,
|
||||||
@NonNull CursorList<ThreadRecord> conversations,
|
@NonNull CursorList<GroupRecord> conversations,
|
||||||
@NonNull CursorList<MessageResult> messages)
|
@NonNull CursorList<MessageResult> messages)
|
||||||
{
|
{
|
||||||
this.query = query;
|
this.query = query;
|
||||||
@ -34,11 +35,11 @@ public class SearchResult {
|
|||||||
this.messages = messages;
|
this.messages = messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Recipient> getContacts() {
|
public List<Contact> getContacts() {
|
||||||
return contacts;
|
return contacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ThreadRecord> getConversations() {
|
public List<GroupRecord> getConversations() {
|
||||||
return conversations;
|
return conversations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user