Merge remote-tracking branch 'upstream/dev'

# Conflicts:
#	app/build.gradle
This commit is contained in:
jubb 2022-02-07 17:08:48 +11:00
commit 07ccc2696b
188 changed files with 5431 additions and 3057 deletions

View File

@ -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
} }
} }

View File

@ -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" />

View File

@ -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

View File

@ -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);
} }
}); });

View File

@ -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);

View File

@ -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)

View File

@ -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
} }
} }

View File

@ -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;
} }

View File

@ -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
} }
} }

View File

@ -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) {

View File

@ -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

View File

@ -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() {

View File

@ -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)

View File

@ -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)
} }
} }

View File

@ -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()
)

View File

@ -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() {

View File

@ -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
} }
} }
} }

View File

@ -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()
} }
} }
} }

View File

@ -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")
} }

View File

@ -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() {

View File

@ -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

View File

@ -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

View File

@ -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)
} }
} }
} }

View File

@ -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()
} }
} }

View File

@ -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() {

View File

@ -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() {

View File

@ -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() {

View File

@ -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() {

View File

@ -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() {

View File

@ -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
} }

View File

@ -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 =

View File

@ -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() }
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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()
} }

View File

@ -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() {

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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)
} }
} }

View File

@ -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() {

View File

@ -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
} }

View File

@ -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

View File

@ -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)

View File

@ -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
} }

View File

@ -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

View File

@ -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) {

View File

@ -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)
}
} }

View File

@ -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)

View File

@ -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);
} }

View File

@ -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<>();

View File

@ -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

View File

@ -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();
} }
} }

View File

@ -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;
} }

View File

@ -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
)
}
} }

View File

@ -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;

View File

@ -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
}

View File

@ -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)
} }

View File

@ -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() }
} }
} }

View File

@ -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()
} }

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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() {

View File

@ -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
} }

View File

@ -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)

View File

@ -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)
} }

View File

@ -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
} }
} }
} }

View File

@ -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)
} }
} }

View File

@ -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()
}
}

View File

@ -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)"

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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();
}); });
} }

View File

@ -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)) {

View File

@ -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()

View File

@ -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)
} }
} }

View File

@ -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));

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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)
} }
} }

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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
} }
} }

View File

@ -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);

View File

@ -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() {

View File

@ -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() {

View File

@ -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"

View File

@ -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)
} }

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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);

View File

@ -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