mirror of
https://github.com/oxen-io/session-android.git
synced 2025-01-11 16:33:39 +00:00
Merge remote-tracking branch 'upstream/dev'
# Conflicts: # app/build.gradle
This commit is contained in:
commit
07ccc2696b
@ -4,18 +4,17 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
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 "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||
classpath "com.google.gms:google-services:4.3.3"
|
||||
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
|
||||
classpath "com.google.gms:google-services:4.3.10"
|
||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'witness'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
@ -32,16 +31,16 @@ dependencies {
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'androidx.legacy:legacy-support-v13: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.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
||||
implementation 'androidx.activity:activity-ktx:1.2.2'
|
||||
implementation 'androidx.fragment:fragment-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 'commons-net:commons-net:3.7.2'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
@ -72,8 +71,8 @@ dependencies {
|
||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||
implementation "com.google.dagger:hilt-android:2.38.1"
|
||||
kapt "com.google.dagger:hilt-compiler:2.38.1"
|
||||
implementation "com.google.dagger:hilt-android:$daggerVersion"
|
||||
kapt "com.google.dagger:hilt-compiler:$daggerVersion"
|
||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||
@ -103,7 +102,7 @@ dependencies {
|
||||
}
|
||||
implementation project(":libsignal")
|
||||
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 'com.goterl:lazysodium-android:5.0.2@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.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
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-android:$kovenantVersion"
|
||||
implementation "com.github.lelloman:android-identicons:v11"
|
||||
@ -122,12 +121,16 @@ dependencies {
|
||||
implementation "com.opencsv:opencsv:4.6"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
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-module-junit4:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||
testImplementation 'androidx.test:core: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
|
||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
||||
|
||||
@ -154,8 +157,8 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 246
|
||||
def canonicalVersionName = "1.11.15"
|
||||
def canonicalVersionCode = 249
|
||||
def canonicalVersionName = "1.11.16"
|
||||
|
||||
def postFixSize = 10
|
||||
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 {
|
||||
release {
|
||||
minifyEnabled false
|
||||
@ -279,6 +288,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
dataBinding true
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="network.loki.messenger">
|
||||
package="network.loki.messenger.test">
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner"
|
||||
android:required="false" />
|
||||
|
@ -15,6 +15,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
||||
import network.loki.messenger.util.NewConversationButtonDrawableMatcher.Companion.newConversationButtonWithDrawable
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.hamcrest.Matchers.not
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
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(withId(R.id.copyButton)).perform(ViewActions.click())
|
||||
pressBack()
|
||||
onView(withId(R.id.seedReminderView)).check(matches(withEffectiveVisibility(Visibility.GONE)))
|
||||
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -85,7 +86,7 @@ class HomeActivityTests {
|
||||
@Test
|
||||
fun testIsVisible_alreadyDismissed_seedView() {
|
||||
setupLoggedInState(hasViewedSeed = true)
|
||||
onView(withId(R.id.seedReminderView)).check(doesNotExist())
|
||||
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -27,6 +27,8 @@ import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@ -36,6 +38,8 @@ import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@ -92,6 +96,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
|
||||
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 DATE_EXTRA = "date";
|
||||
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 LEFT_IS_RECENT_EXTRA = "left_is_recent";
|
||||
|
||||
private View rootContainer;
|
||||
private ViewPager mediaPager;
|
||||
private View detailsContainer;
|
||||
private TextView caption;
|
||||
@ -118,6 +125,26 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
|
||||
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) {
|
||||
Intent previewIntent = null;
|
||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||
@ -147,6 +174,32 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
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
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
clickDetector.onTouchEvent(ev);
|
||||
@ -223,6 +276,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
}
|
||||
|
||||
private void initializeViews() {
|
||||
rootContainer = findViewById(R.id.media_preview_root);
|
||||
mediaPager = findViewById(R.id.media_pager);
|
||||
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
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
if (e.getY() < detailsContainer.getTop()) {
|
||||
detailsContainer.setVisibility(detailsContainer.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
toggleFullscreen();
|
||||
return super.onSingleTapUp(e);
|
||||
}
|
||||
});
|
||||
|
@ -35,12 +35,10 @@ public class AudioCodec {
|
||||
|
||||
public AudioCodec() throws IOException {
|
||||
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.start();
|
||||
|
||||
try {
|
||||
this.audioRecord = createAudioRecord(this.bufferSize);
|
||||
this.mediaCodec.start();
|
||||
audioRecord.startRecording();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
@ -167,7 +165,7 @@ public class AudioCodec {
|
||||
return adtsHeader;
|
||||
}
|
||||
|
||||
private AudioRecord createAudioRecord(int bufferSize) {
|
||||
private AudioRecord createAudioRecord(int bufferSize) throws SecurityException {
|
||||
return new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE,
|
||||
AudioFormat.CHANNEL_IN_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT, bufferSize * 10);
|
||||
|
@ -7,13 +7,14 @@ import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.RelativeLayout
|
||||
import kotlinx.android.synthetic.main.view_separator.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewSeparatorBinding
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
|
||||
class LabeledSeparatorView : RelativeLayout {
|
||||
|
||||
private lateinit var binding: ViewSeparatorBinding
|
||||
private val path = Path()
|
||||
|
||||
private val paint: Paint by lazy {
|
||||
@ -43,10 +44,9 @@ class LabeledSeparatorView : RelativeLayout {
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_separator, null)
|
||||
binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context))
|
||||
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
addView(contentView, layoutParams)
|
||||
addView(binding.root, layoutParams)
|
||||
setWillNotDraw(false)
|
||||
}
|
||||
// endregion
|
||||
@ -59,9 +59,9 @@ class LabeledSeparatorView : RelativeLayout {
|
||||
val hMargin = toPx(16, resources).toFloat()
|
||||
path.reset()
|
||||
path.moveTo(0.0f, h / 2)
|
||||
path.lineTo(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.moveTo(titleTextView.right + hMargin, h / 2)
|
||||
path.lineTo(binding.titleTextView.left - hMargin, h / 2)
|
||||
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(binding.titleTextView.right + hMargin, h / 2)
|
||||
path.lineTo(w, h / 2)
|
||||
path.close()
|
||||
c.drawPath(path, paint)
|
||||
|
@ -8,8 +8,8 @@ import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.DimenRes
|
||||
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.databinding.ViewProfilePictureBinding
|
||||
import org.session.libsession.avatars.ProfileContactPhoto
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.Address
|
||||
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
|
||||
|
||||
class ProfilePictureView : RelativeLayout {
|
||||
private lateinit var binding: ViewProfilePictureBinding
|
||||
lateinit var glide: GlideRequests
|
||||
var publicKey: 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() }
|
||||
|
||||
private fun initialize() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_profile_picture, null)
|
||||
addView(contentView)
|
||||
binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun update(recipient: Recipient, threadID: Long) {
|
||||
fun update(recipient: Recipient) {
|
||||
fun getUserDisplayName(publicKey: String): String {
|
||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||
@ -75,27 +74,27 @@ class ProfilePictureView : RelativeLayout {
|
||||
val publicKey = publicKey ?: return
|
||||
val additionalPublicKey = additionalPublicKey
|
||||
if (additionalPublicKey != null) {
|
||||
setProfilePictureIfNeeded(doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
|
||||
setProfilePictureIfNeeded(doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
|
||||
doubleModeImageViewContainer.visibility = View.VISIBLE
|
||||
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
|
||||
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
|
||||
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
|
||||
} else {
|
||||
glide.clear(doubleModeImageView1)
|
||||
glide.clear(doubleModeImageView2)
|
||||
doubleModeImageViewContainer.visibility = View.INVISIBLE
|
||||
glide.clear(binding.doubleModeImageView1)
|
||||
glide.clear(binding.doubleModeImageView2)
|
||||
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
|
||||
}
|
||||
if (additionalPublicKey == null && !isLarge) {
|
||||
setProfilePictureIfNeeded(singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size)
|
||||
singleModeImageView.visibility = View.VISIBLE
|
||||
setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size)
|
||||
binding.singleModeImageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
glide.clear(singleModeImageView)
|
||||
singleModeImageView.visibility = View.INVISIBLE
|
||||
glide.clear(binding.singleModeImageView)
|
||||
binding.singleModeImageView.visibility = View.INVISIBLE
|
||||
}
|
||||
if (additionalPublicKey == null && isLarge) {
|
||||
setProfilePictureIfNeeded(largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size)
|
||||
largeSingleModeImageView.visibility = View.VISIBLE
|
||||
setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size)
|
||||
binding.largeSingleModeImageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
glide.clear(largeSingleModeImageView)
|
||||
largeSingleModeImageView.visibility = View.INVISIBLE
|
||||
glide.clear(binding.largeSingleModeImageView)
|
||||
binding.largeSingleModeImageView.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,25 +66,18 @@ public class ContactAccessor {
|
||||
public List<String> getNumbersForThreadSearchFilter(Context context, String constraint) {
|
||||
LinkedList<String> numberList = new LinkedList<>();
|
||||
|
||||
GroupDatabase.Reader reader = null;
|
||||
GroupRecord record;
|
||||
|
||||
try {
|
||||
reader = DatabaseComponent.get(context).groupDatabase().getGroupsFilteredByTitle(constraint);
|
||||
|
||||
try (GroupDatabase.Reader reader = DatabaseComponent.get(context).groupDatabase().getGroupsFilteredByTitle(constraint)) {
|
||||
while ((record = reader.getNext()) != null) {
|
||||
numberList.add(record.getEncodedId());
|
||||
}
|
||||
} finally {
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
}
|
||||
|
||||
if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) &&
|
||||
!numberList.contains(TextSecurePreferences.getLocalNumber(context)))
|
||||
{
|
||||
numberList.add(TextSecurePreferences.getLocalNumber(context));
|
||||
}
|
||||
// if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) &&
|
||||
// !numberList.contains(TextSecurePreferences.getLocalNumber(context)))
|
||||
// {
|
||||
// numberList.add(TextSecurePreferences.getLocalNumber(context));
|
||||
// }
|
||||
|
||||
return numberList;
|
||||
}
|
||||
|
@ -1,15 +1,12 @@
|
||||
package org.thoughtcrime.securesms.contacts
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.contact_selection_list_divider.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.contacts.UserView
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import network.loki.messenger.databinding.ContactSelectionListDividerBinding
|
||||
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>() {
|
||||
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 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 {
|
||||
return items.size
|
||||
@ -41,8 +46,9 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
|
||||
return if (viewType == ViewType.Contact) {
|
||||
UserViewHolder(UserView(context))
|
||||
} else {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false)
|
||||
DividerViewHolder(view)
|
||||
DividerViewHolder(
|
||||
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,
|
||||
isSelected)
|
||||
} else if (viewHolder is DividerViewHolder) {
|
||||
item as ContactSelectionListItem.Header
|
||||
viewHolder.view.label.text = item.name
|
||||
viewHolder.bind(item as ContactSelectionListItem.Header)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,21 @@
|
||||
package org.thoughtcrime.securesms.contacts
|
||||
|
||||
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.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.contact_selection_list_fragment.*
|
||||
import network.loki.messenger.R
|
||||
import androidx.fragment.app.Fragment
|
||||
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.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 {
|
||||
private lateinit var binding: ContactSelectionListFragmentBinding
|
||||
private var cursorFilter: String? = null
|
||||
var onContactSelectedListener: OnContactSelectedListener? = null
|
||||
|
||||
@ -46,20 +44,21 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
||||
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() {
|
||||
super.onStart()
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.contact_selection_list_fragment, container, false)
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
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() {
|
||||
@ -74,15 +73,15 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
||||
|
||||
fun resetQueryFilter() {
|
||||
setQueryFilter(null)
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
fun setRefreshing(refreshing: Boolean) {
|
||||
swipeRefreshLayout.isRefreshing = refreshing
|
||||
binding.swipeRefreshLayout.isRefreshing = refreshing
|
||||
}
|
||||
|
||||
fun setOnRefreshListener(onRefreshListener: OnRefreshListener?) {
|
||||
swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
|
||||
binding.swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
|
||||
}
|
||||
|
||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> {
|
||||
@ -107,8 +106,8 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
||||
return
|
||||
}
|
||||
listAdapter.items = items
|
||||
mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
|
||||
emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
|
||||
binding.emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun onContactClick(contact: Recipient) {
|
||||
|
@ -9,16 +9,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
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.databinding.ActivitySelectContactsBinding
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
|
||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
||||
class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||
private lateinit var binding: ActivitySelectContactsBinding
|
||||
private var members = listOf<String>()
|
||||
set(value) { field = value; selectContactsAdapter.members = value }
|
||||
private lateinit var usersToExclude: Set<String>
|
||||
@ -36,18 +33,18 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
|
||||
setContentView(R.layout.activity_select_contacts)
|
||||
binding = ActivitySelectContactsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
|
||||
|
||||
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
|
||||
val emptyStateText = intent.getStringExtra(emptyStateTextKey)
|
||||
if (emptyStateText != null) {
|
||||
emptyStateMessageTextView.text = emptyStateText
|
||||
binding.emptyStateMessageTextView.text = emptyStateText
|
||||
}
|
||||
|
||||
recyclerView.adapter = selectContactsAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = selectContactsAdapter
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||
}
|
||||
@ -73,8 +70,8 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
||||
|
||||
private fun update(members: List<String>) {
|
||||
this.members = members
|
||||
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
// endregion
|
||||
|
@ -5,9 +5,8 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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.databinding.ViewUserBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
|
||||
@ -15,6 +14,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class UserView : LinearLayout {
|
||||
private lateinit var binding: ViewUserBinding
|
||||
var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly
|
||||
|
||||
enum class ActionIndicator {
|
||||
@ -41,9 +41,7 @@ class UserView : LinearLayout {
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_user, null)
|
||||
addView(contentView)
|
||||
binding = ViewUserBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -56,28 +54,32 @@ class UserView : LinearLayout {
|
||||
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
|
||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
|
||||
val address = user.address.serialize()
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.update(user, threadID)
|
||||
actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||
nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
||||
binding.profilePictureView.glide = glide
|
||||
binding.profilePictureView.update(user)
|
||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
||||
when (actionIndicator) {
|
||||
ActionIndicator.None -> {
|
||||
actionIndicatorImageView.visibility = View.GONE
|
||||
binding.actionIndicatorImageView.visibility = View.GONE
|
||||
}
|
||||
ActionIndicator.Menu -> {
|
||||
actionIndicatorImageView.visibility = View.VISIBLE
|
||||
actionIndicatorImageView.setImageResource(R.drawable.ic_more_horiz_white)
|
||||
binding.actionIndicatorImageView.visibility = View.VISIBLE
|
||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_more_horiz_white)
|
||||
}
|
||||
ActionIndicator.Tick -> {
|
||||
actionIndicatorImageView.visibility = View.VISIBLE
|
||||
actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle)
|
||||
binding.actionIndicatorImageView.visibility = View.VISIBLE
|
||||
binding.actionIndicatorImageView.setImageResource(
|
||||
if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleCheckbox(isSelected: Boolean = false) {
|
||||
actionIndicatorImageView.visibility = View.VISIBLE
|
||||
actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle)
|
||||
binding.actionIndicatorImageView.visibility = View.VISIBLE
|
||||
binding.actionIndicatorImageView.setImageResource(
|
||||
if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle
|
||||
)
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,9 +4,7 @@ import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
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.VisibleMessageContentViewDelegate
|
||||
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,
|
||||
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) {
|
||||
private val messageDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||
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 {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val viewType = ViewType.allValues[viewType]
|
||||
when (viewType) {
|
||||
ViewType.Visible -> {
|
||||
val view = VisibleMessageView(context)
|
||||
return VisibleMessageViewHolder(view)
|
||||
}
|
||||
ViewType.Control -> {
|
||||
val view = ControlMessageView(context)
|
||||
return ControlMessageViewHolder(view)
|
||||
}
|
||||
return when (viewType) {
|
||||
ViewType.Visible -> VisibleMessageViewHolder(VisibleMessageView(context))
|
||||
ViewType.Control -> ControlMessageViewHolder(ControlMessageView(context))
|
||||
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 isSelected = selectedItems.contains(message)
|
||||
view.snIsSelected = isSelected
|
||||
view.messageTimestampTextView.isVisible = isSelected
|
||||
view.indexInAdapter = position
|
||||
view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery)
|
||||
if (!message.isDeleted) {
|
||||
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
|
||||
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
||||
} else {
|
||||
view.onPress = null
|
||||
view.onSwipeToReply = null
|
||||
view.onLongPress = null
|
||||
}
|
||||
view.contentViewDelegate = visibleMessageContentViewDelegate
|
||||
}
|
||||
@ -111,6 +106,27 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
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) {
|
||||
if (selectedItems.contains(message)) selectedItems.remove(message) else selectedItems.add(message)
|
||||
notifyItemChanged(position)
|
||||
|
@ -2,16 +2,12 @@ package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.VelocityTracker
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
class ConversationRecyclerView : RecyclerView {
|
||||
private val maxLongPressVelocityY = toPx(10, resources)
|
||||
@ -37,10 +33,10 @@ class ConversationRecyclerView : RecyclerView {
|
||||
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
|
||||
// get passed on to the message view
|
||||
if (abs(vx) > abs(vy)) {
|
||||
return false
|
||||
return if (abs(vx) > abs(vy)) {
|
||||
false
|
||||
} else {
|
||||
return super.onInterceptTouchEvent(e)
|
||||
super.onInterceptTouchEvent(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,130 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import java.util.UUID
|
||||
|
||||
class ConversationViewModel(
|
||||
val threadId: Long,
|
||||
private val repository: ConversationRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(ConversationUiState())
|
||||
val uiState: StateFlow<ConversationUiState> = _uiState
|
||||
|
||||
val recipient: Recipient by lazy {
|
||||
repository.getRecipientForThreadId(threadId)
|
||||
}
|
||||
|
||||
init {
|
||||
_uiState.update {
|
||||
it.copy(isOxenHostedOpenGroup = repository.isOxenHostedOpenGroup(threadId))
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDraft(text: String) {
|
||||
repository.saveDraft(threadId, text)
|
||||
}
|
||||
|
||||
fun getDraft(): String? {
|
||||
return repository.getDraft(threadId)
|
||||
}
|
||||
|
||||
fun inviteContacts(contacts: List<Recipient>) {
|
||||
repository.inviteContacts(threadId, contacts)
|
||||
}
|
||||
|
||||
fun unblock() {
|
||||
if (recipient.isContactRecipient) {
|
||||
repository.unblock(recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteLocally(message: MessageRecord) {
|
||||
repository.deleteLocally(recipient, message)
|
||||
}
|
||||
|
||||
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
|
||||
repository.deleteForEveryone(threadId, recipient, message)
|
||||
.onFailure {
|
||||
showMessage("Couldn't delete message due to error: $it")
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMessagesWithoutUnsendRequest(messages: Set<MessageRecord>) = viewModelScope.launch {
|
||||
repository.deleteMessageWithoutUnsendRequest(threadId, messages)
|
||||
.onFailure {
|
||||
showMessage("Couldn't delete message due to error: $it")
|
||||
}
|
||||
}
|
||||
|
||||
fun banUser(recipient: Recipient) = viewModelScope.launch {
|
||||
repository.banUser(threadId, recipient)
|
||||
.onSuccess {
|
||||
showMessage("Successfully banned user")
|
||||
}
|
||||
.onFailure {
|
||||
showMessage("Couldn't ban user due to error: $it")
|
||||
}
|
||||
}
|
||||
|
||||
fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch {
|
||||
repository.banAndDeleteAll(threadId, recipient)
|
||||
.onSuccess {
|
||||
showMessage("Successfully banned user and deleted all their messages")
|
||||
}
|
||||
.onFailure {
|
||||
showMessage("Couldn't execute request due to error: $it")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMessage(message: String) {
|
||||
_uiState.update { currentUiState ->
|
||||
val messages = currentUiState.uiMessages + UiMessage(
|
||||
id = UUID.randomUUID().mostSignificantBits,
|
||||
message = message
|
||||
)
|
||||
currentUiState.copy(uiMessages = messages)
|
||||
}
|
||||
}
|
||||
|
||||
fun messageShown(messageId: Long) {
|
||||
_uiState.update { currentUiState ->
|
||||
val messages = currentUiState.uiMessages.filterNot { it.id == messageId }
|
||||
currentUiState.copy(uiMessages = messages)
|
||||
}
|
||||
}
|
||||
|
||||
@dagger.assisted.AssistedFactory
|
||||
interface AssistedFactory {
|
||||
fun create(threadId: Long): Factory
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class Factory @AssistedInject constructor(
|
||||
@Assisted private val threadId: Long,
|
||||
private val repository: ConversationRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ConversationViewModel(threadId, repository) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class UiMessage(val id: Long, val message: String)
|
||||
|
||||
data class ConversationUiState(
|
||||
val isOxenHostedOpenGroup: Boolean = false,
|
||||
val uiMessages: List<UiMessage> = emptyList()
|
||||
)
|
@ -7,8 +7,8 @@ import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.fragment_delete_message_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentDeleteMessageBottomSheetBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||
@ -22,6 +22,7 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
||||
lateinit var contactDatabase: SessionContactDatabase
|
||||
|
||||
lateinit var recipient: Recipient
|
||||
private lateinit var binding: FragmentDeleteMessageBottomSheetBinding
|
||||
val contact by lazy {
|
||||
val senderId = recipient.address.serialize()
|
||||
// this dialog won't show for open group contacts
|
||||
@ -37,15 +38,16 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_delete_message_bottom_sheet, container, false)
|
||||
): View {
|
||||
binding = FragmentDeleteMessageBottomSheetBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
when (v) {
|
||||
deleteForMeTextView -> onDeleteForMeTapped?.invoke()
|
||||
deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke()
|
||||
cancelTextView -> onCancelTapped?.invoke()
|
||||
binding.deleteForMeTextView -> onDeleteForMeTapped?.invoke()
|
||||
binding.deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke()
|
||||
binding.cancelTextView -> onCancelTapped?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,13 +57,13 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
||||
return dismiss()
|
||||
}
|
||||
if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) {
|
||||
deleteForEveryoneTextView.text =
|
||||
binding.deleteForEveryoneTextView.text =
|
||||
resources.getString(R.string.delete_message_for_me_and_recipient, contact)
|
||||
}
|
||||
deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
|
||||
deleteForMeTextView.setOnClickListener(this)
|
||||
deleteForEveryoneTextView.setOnClickListener(this)
|
||||
cancelTextView.setOnClickListener(this)
|
||||
binding.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
|
||||
binding.deleteForMeTextView.setOnClickListener(this)
|
||||
binding.deleteForEveryoneTextView.setOnClickListener(this)
|
||||
binding.cancelTextView.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.activity_message_detail.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityMessageDetailBinding
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
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.util.DateUtils
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMessageDetailBinding
|
||||
var messageRecord: MessageRecord? = null
|
||||
|
||||
// region Settings
|
||||
@ -29,7 +29,8 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
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)
|
||||
val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
||||
// We only show this screen for messages fail to send,
|
||||
@ -37,7 +38,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
||||
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
|
||||
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author)
|
||||
updateContent()
|
||||
resend_button.setOnClickListener {
|
||||
binding.resendButton.setOnClickListener {
|
||||
ResendMessageUtilities.resend(messageRecord!!)
|
||||
finish()
|
||||
}
|
||||
@ -46,20 +47,20 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
||||
fun updateContent() {
|
||||
val dateLocale = Locale.getDefault()
|
||||
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."
|
||||
error_message.text = errorMessage
|
||||
binding.errorMessage.text = errorMessage
|
||||
|
||||
if (messageRecord!!.getExpiresIn() <= 0 || messageRecord!!.getExpireStarted() <= 0) {
|
||||
expires_container.visibility = View.GONE
|
||||
if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
|
||||
binding.expiresContainer.visibility = View.GONE
|
||||
} else {
|
||||
expires_container.visibility = View.VISIBLE
|
||||
binding.expiresContainer.visibility = View.VISIBLE
|
||||
val elapsed = System.currentTimeMillis() - messageRecord!!.expireStarted
|
||||
val remaining = messageRecord!!.expiresIn - elapsed
|
||||
|
||||
val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
|
||||
expires_in.text = duration
|
||||
binding.expiresIn.text = duration
|
||||
}
|
||||
}
|
||||
}
|
@ -15,14 +15,16 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
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.databinding.FragmentModalUrlBottomSheetBinding
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
|
||||
class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_modal_url_bottom_sheet, container, false)
|
||||
private lateinit var binding: FragmentModalUrlBottomSheetBinding
|
||||
|
||||
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?) {
|
||||
@ -31,10 +33,10 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(url)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
openURLExplanationTextView.text = spannable
|
||||
cancelButton.setOnClickListener(this)
|
||||
copyButton.setOnClickListener(this)
|
||||
openURLButton.setOnClickListener(this)
|
||||
binding.openURLExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener(this)
|
||||
binding.copyButton.setOnClickListener(this)
|
||||
binding.openURLButton.setOnClickListener(this)
|
||||
}
|
||||
|
||||
private fun open() {
|
||||
@ -64,9 +66,9 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
when (v) {
|
||||
openURLButton -> open()
|
||||
copyButton -> copy()
|
||||
cancelButton -> dismiss()
|
||||
binding.openURLButton -> open()
|
||||
binding.copyButton -> copy()
|
||||
binding.cancelButton -> dismiss()
|
||||
}
|
||||
}
|
||||
}
|
@ -11,28 +11,25 @@ import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.album_thumbnail_view.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||
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.TextUtilities.getIntersectedModalSpans
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.longmessage.LongMessageActivity
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AlbumThumbnailView : FrameLayout {
|
||||
|
||||
private lateinit var binding: AlbumThumbnailViewBinding
|
||||
|
||||
companion object {
|
||||
const val MAX_ALBUM_DISPLAY_SIZE = 5
|
||||
}
|
||||
@ -55,7 +52,7 @@ class AlbumThumbnailView : FrameLayout {
|
||||
private var slideSize: Int = 0
|
||||
|
||||
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?) {
|
||||
@ -70,26 +67,9 @@ class AlbumThumbnailView : FrameLayout {
|
||||
val rawXInt = event.rawX.toInt()
|
||||
val rawYInt = event.rawY.toInt()
|
||||
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||
// Z-check in specific order
|
||||
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
|
||||
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)
|
||||
if (testRect.contains(eventRect)) {
|
||||
// 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,
|
||||
isStart: Boolean, isEnd: Boolean) {
|
||||
slides = message.slideDeck.thumbnailSlides
|
||||
@ -122,10 +107,10 @@ class AlbumThumbnailView : FrameLayout {
|
||||
|
||||
// recreate cell views if different size to what we have already (for recycling)
|
||||
if (slides.size != this.slideSize) {
|
||||
albumCellContainer.removeAllViews()
|
||||
LayoutInflater.from(context).inflate(layoutRes(slides.size), albumCellContainer)
|
||||
binding.albumCellContainer.removeAllViews()
|
||||
LayoutInflater.from(context).inflate(layoutRes(slides.size), binding.albumCellContainer)
|
||||
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.isVisible = overflowed // more than max album 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)
|
||||
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
|
||||
@ -165,11 +137,11 @@ class AlbumThumbnailView : FrameLayout {
|
||||
}
|
||||
|
||||
fun getThumbnailView(position: Int): KThumbnailView = when (position) {
|
||||
0 -> 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)
|
||||
2 -> 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)
|
||||
4 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
|
||||
0 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
|
||||
1 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
|
||||
2 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
|
||||
3 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -5,14 +5,14 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_link_preview_draft.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewLinkPreviewDraftBinding
|
||||
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.ImageSlide
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
|
||||
class LinkPreviewDraftView : LinearLayout {
|
||||
private lateinit var binding: ViewLinkPreviewDraftBinding
|
||||
var delegate: LinkPreviewDraftViewDelegate? = null
|
||||
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
@ -21,22 +21,22 @@ class LinkPreviewDraftView : LinearLayout {
|
||||
|
||||
private fun initialize() {
|
||||
// Start out with the loader showing and the content view hidden
|
||||
LayoutInflater.from(context).inflate(R.layout.view_link_preview_draft, this)
|
||||
linkPreviewDraftContainer.isVisible = false
|
||||
thumbnailImageView.clipToOutline = true
|
||||
linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
||||
binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.linkPreviewDraftContainer.isVisible = false
|
||||
binding.thumbnailImageView.clipToOutline = true
|
||||
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
||||
}
|
||||
|
||||
fun update(glide: GlideRequests, linkPreview: LinkPreview) {
|
||||
// Hide the loader and show the content view
|
||||
linkPreviewDraftContainer.isVisible = true
|
||||
linkPreviewDraftLoader.isVisible = false
|
||||
thumbnailImageView.radius = toPx(4, resources)
|
||||
binding.linkPreviewDraftContainer.isVisible = true
|
||||
binding.linkPreviewDraftLoader.isVisible = false
|
||||
binding.thumbnailImageView.radius = toPx(4, resources)
|
||||
if (linkPreview.getThumbnail().isPresent) {
|
||||
// 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() {
|
||||
|
@ -45,7 +45,7 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
||||
}
|
||||
|
||||
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)
|
||||
cell.glide = glide
|
||||
cell.mentionCandidate = mentionCandidate
|
||||
|
@ -4,32 +4,29 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewMentionCandidateBinding
|
||||
import org.session.libsession.messaging.mentions.Mention
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
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("", "")
|
||||
set(newValue) { field = newValue; update() }
|
||||
var glide: GlideRequests? = null
|
||||
var openGroupServer: String? = null
|
||||
var openGroupRoom: String? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
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 {
|
||||
|
||||
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView {
|
||||
return layoutInflater.inflate(R.layout.view_mention_candidate, parent, false) as MentionCandidateView
|
||||
}
|
||||
private fun initialize() {
|
||||
binding = ViewMentionCandidateBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
private fun update() = with(binding) {
|
||||
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
||||
profilePictureView.publicKey = mentionCandidate.publicKey
|
||||
profilePictureView.displayName = mentionCandidate.displayName
|
||||
|
@ -5,8 +5,7 @@ import android.content.Intent
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import kotlinx.android.synthetic.main.view_open_group_guidelines.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewOpenGroupGuidelinesBinding
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity
|
||||
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() }
|
||||
|
||||
private fun initialize() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null)
|
||||
addView(contentView)
|
||||
readButton.setOnClickListener {
|
||||
val activity = context as ConversationActivityV2
|
||||
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
|
||||
activity.push(intent)
|
||||
ViewOpenGroupGuidelinesBinding.inflate(LayoutInflater.from(context), this, true).apply {
|
||||
readButton.setOnClickListener {
|
||||
val activity = context as ConversationActivityV2
|
||||
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
|
||||
activity.push(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,22 +4,22 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_conversation_typing_container.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewConversationTypingContainerBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
|
||||
class TypingIndicatorViewContainer : LinearLayout {
|
||||
private lateinit var binding: ViewConversationTypingContainerBinding
|
||||
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
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>) {
|
||||
if (typists.isEmpty()) { typingIndicator.stopAnimation(); return }
|
||||
typingIndicator.startAnimation()
|
||||
if (typists.isEmpty()) { binding.typingIndicator.stopAnimation(); return }
|
||||
binding.typingIndicator.startAnimation()
|
||||
}
|
||||
}
|
@ -6,8 +6,8 @@ import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_blocked.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogBlockedBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
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() {
|
||||
|
||||
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 sessionID = recipient.address.toString()
|
||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
val title = resources.getString(R.string.dialog_blocked_title, name)
|
||||
contentView.blockedTitleTextView.text = title
|
||||
binding.blockedTitleTextView.text = title
|
||||
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
contentView.blockedExplanationTextView.text = spannable
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.unblockButton.setOnClickListener { unblock() }
|
||||
builder.setView(contentView)
|
||||
binding.blockedExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.unblockButton.setOnClickListener { unblock() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun unblock() {
|
||||
|
@ -7,8 +7,8 @@ import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.dialog_download.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogDownloadBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
@ -26,20 +26,20 @@ class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
|
||||
@Inject lateinit var contactDB: SessionContactDatabase
|
||||
|
||||
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 contact = contactDB.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
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 spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
contentView.downloadExplanationTextView.text = spannable
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.downloadButton.setOnClickListener { trust() }
|
||||
builder.setView(contentView)
|
||||
binding.downloadExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.downloadButton.setOnClickListener { trust() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun trust() {
|
||||
|
@ -7,8 +7,8 @@ import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogJoinOpenGroupBinding
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
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() {
|
||||
|
||||
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)
|
||||
contentView.joinOpenGroupTitleTextView.text = title
|
||||
binding.joinOpenGroupTitleTextView.text = title
|
||||
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
contentView.joinOpenGroupExplanationTextView.text = spannable
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.joinButton.setOnClickListener { join() }
|
||||
builder.setView(contentView)
|
||||
binding.joinOpenGroupExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.joinButton.setOnClickListener { join() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun join() {
|
||||
|
@ -2,8 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_link_preview.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogLinkPreviewBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
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() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_link_preview, null)
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.enableLinkPreviewsButton.setOnClickListener { enable() }
|
||||
builder.setView(contentView)
|
||||
val binding = DialogLinkPreviewBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.enableLinkPreviewsButton.setOnClickListener { enable() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun enable() {
|
||||
|
@ -2,18 +2,17 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_send_seed.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogSendSeedBinding
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
|
||||
/** Shown if the user is about to send their recovery phrase to someone. */
|
||||
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_send_seed, null)
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.sendSeedButton.setOnClickListener { send() }
|
||||
builder.setView(contentView)
|
||||
val binding = DialogSendSeedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.sendSeedButton.setOnClickListener { send() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun send() {
|
||||
|
@ -4,13 +4,14 @@ import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.text.InputType
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_input_bar.view.*
|
||||
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.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
@ -27,6 +28,7 @@ import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate {
|
||||
private lateinit var binding: ViewInputBarBinding
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private val vMargin by lazy { toDp(4, resources) }
|
||||
private val minHeight by lazy { toPx(56, resources) }
|
||||
@ -39,8 +41,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
set(value) { field = value; showOrHideInputIfNeeded() }
|
||||
|
||||
var text: String
|
||||
get() { return inputBarEditText.text?.toString() ?: "" }
|
||||
set(value) { inputBarEditText.setText(value) }
|
||||
get() { return binding.inputBarEditText.text?.toString() ?: "" }
|
||||
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 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() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_input_bar, this)
|
||||
binding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
// Attachments button
|
||||
attachmentsButtonContainer.addView(attachmentsButton)
|
||||
attachmentsButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
binding.attachmentsButtonContainer.addView(attachmentsButton)
|
||||
attachmentsButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
attachmentsButton.onPress = { toggleAttachmentOptions() }
|
||||
// Microphone button
|
||||
microphoneOrSendButtonContainer.addView(microphoneButton)
|
||||
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
binding.microphoneOrSendButtonContainer.addView(microphoneButton)
|
||||
microphoneButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
microphoneButton.onLongPress = { startRecordingVoiceMessage() }
|
||||
microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
|
||||
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
|
||||
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }
|
||||
// Send button
|
||||
microphoneOrSendButtonContainer.addView(sendButton)
|
||||
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
binding.microphoneOrSendButtonContainer.addView(sendButton)
|
||||
sendButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
sendButton.isVisible = false
|
||||
sendButton.onUp = { delegate?.sendMessage() }
|
||||
// Edit text
|
||||
val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0
|
||||
inputBarEditText.imeOptions = inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled
|
||||
inputBarEditText.inputType = inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
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)
|
||||
binding.inputBarEditText.imeOptions = binding.inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled
|
||||
binding.inputBarEditText.inputType = binding.inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
binding.inputBarEditText.delegate = this
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -94,8 +90,6 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
}
|
||||
|
||||
override fun inputBarEditTextHeightChanged(newValue: Int) {
|
||||
val newHeight = max(newValue + 2 * vMargin, minHeight) + inputBarAdditionalContentContainer.height
|
||||
setHeight(newHeight)
|
||||
}
|
||||
|
||||
override fun commitInputContent(contentUri: Uri) {
|
||||
@ -117,45 +111,31 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
quote = message
|
||||
linkPreview = null
|
||||
linkPreviewDraftView = null
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||
val quoteView = QuoteView(context, QuoteView.Mode.Draft)
|
||||
quoteView.delegate = this
|
||||
inputBarAdditionalContentContainer.addView(quoteView)
|
||||
binding.inputBarAdditionalContentContainer.addView(quoteView)
|
||||
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()
|
||||
quoteView.bind(sender, message.body, attachments,
|
||||
thread, true, maxContentWidth, 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
|
||||
// intrinsic height calculation.
|
||||
val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources)
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + quoteViewIntrinsicHeight
|
||||
additionalContentHeight = quoteViewIntrinsicHeight
|
||||
setHeight(newHeight)
|
||||
thread, true, message.isOpenGroupInvitation, message.threadId, false, glide)
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
override fun cancelQuoteDraft() {
|
||||
quote = null
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
|
||||
additionalContentHeight = 0
|
||||
setHeight(newHeight)
|
||||
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
fun draftLinkPreview() {
|
||||
quote = null
|
||||
val linkPreviewDraftHeight = toPx(88, resources)
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||
val linkPreviewDraftView = LinkPreviewDraftView(context)
|
||||
linkPreviewDraftView.delegate = this
|
||||
this.linkPreviewDraftView = linkPreviewDraftView
|
||||
inputBarAdditionalContentContainer.addView(linkPreviewDraftView)
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + linkPreviewDraftHeight
|
||||
additionalContentHeight = linkPreviewDraftHeight
|
||||
setHeight(newHeight)
|
||||
binding.inputBarAdditionalContentContainer.addView(linkPreviewDraftView)
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) {
|
||||
@ -167,24 +147,30 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
override fun cancelLinkPreviewDraft() {
|
||||
if (quote != null) { return }
|
||||
linkPreview = null
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
|
||||
additionalContentHeight = 0
|
||||
setHeight(newHeight)
|
||||
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun showOrHideInputIfNeeded() {
|
||||
if (showInput) {
|
||||
setOf( inputBarEditText, attachmentsButton ).forEach { it.isVisible = true }
|
||||
setOf( binding.inputBarEditText, attachmentsButton ).forEach { it.isVisible = true }
|
||||
microphoneButton.isVisible = text.isEmpty()
|
||||
sendButton.isVisible = text.isNotEmpty()
|
||||
} else {
|
||||
cancelQuoteDraft()
|
||||
cancelLinkPreviewDraft()
|
||||
val views = setOf( inputBarEditText, attachmentsButton, microphoneButton, sendButton )
|
||||
val views = setOf( binding.inputBarEditText, attachmentsButton, microphoneButton, sendButton )
|
||||
views.forEach { it.isVisible = false }
|
||||
}
|
||||
}
|
||||
|
||||
fun addTextChangedListener(textWatcher: TextWatcher) {
|
||||
binding.inputBarEditText.addTextChangedListener(textWatcher)
|
||||
}
|
||||
|
||||
fun setSelection(index: Int) {
|
||||
binding.inputBarEditText.setSelection(index)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
|
@ -45,8 +45,8 @@ class InputBarEditText : AppCompatEditText {
|
||||
delegate?.inputBarEditTextHeightChanged(constrainedHeight.roundToInt())
|
||||
}
|
||||
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||
val ic: InputConnection = super.onCreateInputConnection(editorInfo)
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? {
|
||||
val ic = super.onCreateInputConnection(editorInfo) ?: return null
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png", "image/gif", "image/jpg"))
|
||||
|
||||
val callback =
|
||||
|
@ -8,40 +8,56 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
|
||||
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.disableClipping
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
class InputBarRecordingView : RelativeLayout {
|
||||
private lateinit var binding: ViewInputBarRecordingBinding
|
||||
private var startTimestamp = 0L
|
||||
private val snHandler = Handler(Looper.getMainLooper())
|
||||
private var dotViewAnimation: ValueAnimator? = null
|
||||
private var pulseAnimation: ValueAnimator? = 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, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_input_bar_recording, this)
|
||||
inputBarMiddleContentContainer.disableClipping()
|
||||
inputBarCancelButton.setOnClickListener { hide() }
|
||||
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.inputBarMiddleContentContainer.disableClipping()
|
||||
binding.inputBarCancelButton.setOnClickListener { hide() }
|
||||
}
|
||||
|
||||
fun show() {
|
||||
startTimestamp = Date().time
|
||||
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
||||
inputBarCancelButton.alpha = 0.0f
|
||||
inputBarMiddleContentContainer.alpha = 1.0f
|
||||
lockView.alpha = 1.0f
|
||||
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
||||
binding.inputBarCancelButton.alpha = 0.0f
|
||||
binding.inputBarMiddleContentContainer.alpha = 1.0f
|
||||
binding.lockView.alpha = 1.0f
|
||||
isVisible = true
|
||||
alpha = 0.0f
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||
@ -77,7 +93,7 @@ class InputBarRecordingView : RelativeLayout {
|
||||
dotViewAnimation = animation
|
||||
animation.duration = 500L
|
||||
animation.addUpdateListener { animator ->
|
||||
dotView.alpha = animator.animatedValue as Float
|
||||
binding.dotView.alpha = animator.animatedValue as Float
|
||||
}
|
||||
animation.repeatCount = ValueAnimator.INFINITE
|
||||
animation.repeatMode = ValueAnimator.REVERSE
|
||||
@ -87,12 +103,12 @@ class InputBarRecordingView : RelativeLayout {
|
||||
private fun pulse() {
|
||||
val collapsedSize = toPx(80.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)
|
||||
pulseAnimation = animation
|
||||
animation.duration = 1000L
|
||||
animation.addUpdateListener { animator ->
|
||||
pulseView.alpha = animator.animatedValue as Float
|
||||
binding.pulseView.alpha = animator.animatedValue as Float
|
||||
if (animator.animatedFraction == 1.0f && isVisible) { pulse() }
|
||||
}
|
||||
animation.start()
|
||||
@ -101,21 +117,21 @@ class InputBarRecordingView : RelativeLayout {
|
||||
private fun animateLockViewUp() {
|
||||
val startMarginBottom = toPx(32, resources)
|
||||
val endMarginBottom = toPx(72, resources)
|
||||
val layoutParams = lockView.layoutParams as LayoutParams
|
||||
val layoutParams = binding.lockView.layoutParams as LayoutParams
|
||||
layoutParams.bottomMargin = startMarginBottom
|
||||
lockView.layoutParams = layoutParams
|
||||
binding.lockView.layoutParams = layoutParams
|
||||
val animation = ValueAnimator.ofObject(IntEvaluator(), startMarginBottom, endMarginBottom)
|
||||
animation.duration = 250L
|
||||
animation.addUpdateListener { animator ->
|
||||
layoutParams.bottomMargin = animator.animatedValue as Int
|
||||
lockView.layoutParams = layoutParams
|
||||
binding.lockView.layoutParams = layoutParams
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
private fun updateTimer() {
|
||||
val duration = (Date().time - startTimestamp) / 1000L
|
||||
recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||
snHandler.postDelayed({ updateTimer() }, 500)
|
||||
}
|
||||
|
||||
@ -123,19 +139,19 @@ class InputBarRecordingView : RelativeLayout {
|
||||
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||
fadeOutAnimation.duration = 250L
|
||||
fadeOutAnimation.addUpdateListener { animator ->
|
||||
inputBarMiddleContentContainer.alpha = animator.animatedValue as Float
|
||||
lockView.alpha = animator.animatedValue as Float
|
||||
binding.inputBarMiddleContentContainer.alpha = animator.animatedValue as Float
|
||||
binding.lockView.alpha = animator.animatedValue as Float
|
||||
}
|
||||
fadeOutAnimation.start()
|
||||
val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||
fadeInAnimation.duration = 250L
|
||||
fadeInAnimation.addUpdateListener { animator ->
|
||||
inputBarCancelButton.alpha = animator.animatedValue as Float
|
||||
binding.inputBarCancelButton.alpha = animator.animatedValue as Float
|
||||
}
|
||||
fadeInAnimation.start()
|
||||
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
|
||||
recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
|
||||
inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }
|
||||
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
|
||||
binding.recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
|
||||
binding.inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,33 +4,29 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewMentionCandidateV2Binding
|
||||
import org.session.libsession.messaging.mentions.Mention
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
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("", "")
|
||||
set(newValue) { field = newValue; update() }
|
||||
var glide: GlideRequests? = null
|
||||
var openGroupServer: String? = null
|
||||
var openGroupRoom: String? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
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 {
|
||||
|
||||
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView {
|
||||
return layoutInflater.inflate(R.layout.view_mention_candidate_v2, parent, false) as MentionCandidateView
|
||||
}
|
||||
private fun initialize() {
|
||||
binding = ViewMentionCandidateV2Binding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
private fun update() = with(binding) {
|
||||
mentionCandidateNameTextView.text = candidate.displayName
|
||||
profilePictureView.publicKey = candidate.publicKey
|
||||
profilePictureView.displayName = candidate.displayName
|
||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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 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)
|
||||
cell.glide = glide
|
||||
cell.candidate = mentionCandidate
|
||||
|
@ -12,7 +12,6 @@ import android.os.AsyncTask
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
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.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
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.libsignal.utilities.guava.Optional
|
||||
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.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
|
||||
@ -101,15 +104,12 @@ object ConversationMenuHelper {
|
||||
val searchViewItem = menu.findItem(R.id.menu_search)
|
||||
(context as ConversationActivityV2).searchViewItem = searchViewItem
|
||||
val searchView = searchViewItem.actionView as SearchView
|
||||
val searchViewModel = context.searchViewModel!!
|
||||
val queryListener = object : OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
searchViewModel.onQueryUpdated(query, threadId)
|
||||
context.searchBottomBar.showLoading()
|
||||
context.onSearchQueryUpdated(query)
|
||||
return true
|
||||
}
|
||||
@ -117,10 +117,7 @@ object ConversationMenuHelper {
|
||||
searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
searchView.setOnQueryTextListener(queryListener)
|
||||
searchViewModel.onSearchOpened()
|
||||
context.searchBottomBar.visibility = View.VISIBLE
|
||||
context.searchBottomBar.setData(0, 0)
|
||||
context.inputBar.visibility = View.GONE
|
||||
context.onSearchOpened()
|
||||
for (i in 0 until menu.size()) {
|
||||
if (menu.getItem(i) != searchViewItem) {
|
||||
menu.getItem(i).isVisible = false
|
||||
@ -131,11 +128,7 @@ object ConversationMenuHelper {
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
searchView.setOnQueryTextListener(null)
|
||||
searchViewModel.onSearchClosed()
|
||||
context.searchBottomBar.visibility = View.GONE
|
||||
context.inputBar.visibility = View.VISIBLE
|
||||
context.onSearchQueryUpdated(null)
|
||||
context.invalidateOptionsMenu()
|
||||
context.onSearchClosed()
|
||||
return true
|
||||
}
|
||||
})
|
||||
@ -169,7 +162,7 @@ object ConversationMenuHelper {
|
||||
}
|
||||
|
||||
private fun search(context: Context) {
|
||||
val searchViewModel = (context as ConversationActivityV2).searchViewModel!!
|
||||
val searchViewModel = (context as ConversationActivityV2).searchViewModel
|
||||
searchViewModel.onSearchOpened()
|
||||
}
|
||||
|
||||
|
@ -7,35 +7,41 @@ import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.view_control_message.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewControlMessageBinding
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
class ControlMessageView : LinearLayout {
|
||||
|
||||
private lateinit var binding: ViewControlMessageBinding
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
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)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, previous: MessageRecord?) {
|
||||
dateBreakTextView.showDateBreak(message, previous)
|
||||
iconImageView.visibility = View.GONE
|
||||
binding.dateBreakTextView.showDateBreak(message, previous)
|
||||
binding.iconImageView.visibility = View.GONE
|
||||
if (message.isExpirationTimerUpdate) {
|
||||
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme))
|
||||
iconImageView.visibility = View.VISIBLE
|
||||
binding.iconImageView.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)
|
||||
)
|
||||
binding.iconImageView.visibility = View.VISIBLE
|
||||
} else if (message.isMediaSavedNotification) {
|
||||
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme))
|
||||
iconImageView.visibility = View.VISIBLE
|
||||
binding.iconImageView.setImageDrawable(
|
||||
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() {
|
||||
|
@ -6,32 +6,28 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
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.databinding.ViewDeletedMessageBinding
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import java.util.*
|
||||
|
||||
class DeletedMessageView : LinearLayout {
|
||||
|
||||
private lateinit var binding: ViewDeletedMessageBinding
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_deleted_message, this)
|
||||
binding = ViewDeletedMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||
assert(message.isDeleted)
|
||||
deleteTitleTextView.text = context.getString(R.string.deleted_message)
|
||||
deleteTitleTextView.setTextColor(textColor)
|
||||
deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
binding.deleteTitleTextView.text = context.getString(R.string.deleted_message)
|
||||
binding.deleteTitleTextView.setTextColor(textColor)
|
||||
binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -6,29 +6,27 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import kotlinx.android.synthetic.main.view_document.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import network.loki.messenger.databinding.ViewDocumentBinding
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
|
||||
class DocumentView : LinearLayout {
|
||||
|
||||
private lateinit var binding: ViewDocumentBinding
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_document, this)
|
||||
binding = ViewDocumentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MmsMessageRecord, @ColorInt textColor: Int) {
|
||||
val document = message.slideDeck.documentSlide!!
|
||||
documentTitleTextView.text = document.fileName.or("Untitled File")
|
||||
documentTitleTextView.setTextColor(textColor)
|
||||
documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
binding.documentTitleTextView.text = document.fileName.or("Untitled File")
|
||||
binding.documentTitleTextView.setTextColor(textColor)
|
||||
binding.documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -11,8 +11,8 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_link_preview.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewLinkPreviewBinding
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
|
||||
class LinkPreviewView : LinearLayout {
|
||||
private lateinit var binding: ViewLinkPreviewBinding
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var url: String? = null
|
||||
lateinit var bodyTextView: TextView
|
||||
@ -33,31 +34,35 @@ class LinkPreviewView : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_link_preview, this)
|
||||
binding = ViewLinkPreviewBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// 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()
|
||||
url = linkPreview.url
|
||||
// Thumbnail
|
||||
if (linkPreview.getThumbnail().isPresent) {
|
||||
// This internally fetches the thumbnail
|
||||
thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
||||
thumbnailImageView.loadIndicator.isVisible = false
|
||||
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
||||
binding.thumbnailImageView.loadIndicator.isVisible = false
|
||||
}
|
||||
// Title
|
||||
titleTextView.text = linkPreview.title
|
||||
binding.titleTextView.text = linkPreview.title
|
||||
val textColorID = if (message.isOutgoing && UiModeUtilities.isDayUiMode(context)) {
|
||||
R.color.white
|
||||
} else {
|
||||
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
|
||||
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
mainLinkPreviewContainer.addView(bodyTextView)
|
||||
binding.titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
|
||||
// Corner radii
|
||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||
@ -78,14 +83,14 @@ class LinkPreviewView : LinearLayout {
|
||||
val rawYInt = event.rawY.toInt()
|
||||
val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||
val previewRect = Rect()
|
||||
mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
|
||||
binding.mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
|
||||
if (previewRect.contains(hitRect)) {
|
||||
openURL()
|
||||
return
|
||||
}
|
||||
// intersectedModalSpans should only be a list of one item
|
||||
val hitSpans = bodyTextView.getIntersectedModalSpans(hitRect)
|
||||
hitSpans.forEach { span ->
|
||||
hitSpans.iterator().forEach { span ->
|
||||
span.onClick(bodyTextView)
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,15 @@ import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.android.synthetic.main.view_open_group_invitation.view.*
|
||||
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.utilities.OpenGroupUrlParser
|
||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
class OpenGroupInvitationView : LinearLayout {
|
||||
private lateinit var binding: ViewOpenGroupInvitationBinding
|
||||
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
|
||||
|
||||
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() }
|
||||
|
||||
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) {
|
||||
@ -31,12 +31,14 @@ class OpenGroupInvitationView : LinearLayout {
|
||||
val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation
|
||||
this.data = data
|
||||
val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus
|
||||
openGroupInvitationIconImageView.setImageResource(iconID)
|
||||
openGroupTitleTextView.text = data.groupName
|
||||
openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl)
|
||||
openGroupTitleTextView.setTextColor(textColor)
|
||||
openGroupJoinMessageTextView.setTextColor(textColor)
|
||||
openGroupURLTextView.setTextColor(textColor)
|
||||
with(binding){
|
||||
openGroupInvitationIconImageView.setImageResource(iconID)
|
||||
openGroupTitleTextView.text = data.groupName
|
||||
openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl)
|
||||
openGroupTitleTextView.setTextColor(textColor)
|
||||
openGroupJoinMessageTextView.setTextColor(textColor)
|
||||
openGroupURLTextView.setTextColor(textColor)
|
||||
}
|
||||
}
|
||||
|
||||
fun joinOpenGroup() {
|
||||
|
@ -2,22 +2,24 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.text.StaticLayout
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.view_quote.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewQuoteBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
|
||||
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.SlideDeck
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
@ -26,7 +28,6 @@ import org.thoughtcrime.securesms.util.toPx
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
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
|
||||
// if you don't need to. If you do then test:
|
||||
@ -39,6 +40,7 @@ class QuoteView : LinearLayout {
|
||||
|
||||
@Inject lateinit var contactDb: SessionContactDatabase
|
||||
|
||||
private lateinit var binding: ViewQuoteBinding
|
||||
private lateinit var mode: Mode
|
||||
private val vPadding by lazy { toPx(6, resources) }
|
||||
var delegate: QuoteViewDelegate? = null
|
||||
@ -46,25 +48,20 @@ class QuoteView : LinearLayout {
|
||||
enum class Mode { Regular, Draft }
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { throw IllegalAccessError("Use QuoteView(context:mode:) instead.") }
|
||||
constructor(context: Context) : this(context, Mode.Regular)
|
||||
constructor(context: Context, attrs: AttributeSet) : this(context, Mode.Regular, attrs)
|
||||
|
||||
constructor(context: Context, mode: Mode) : super(context) {
|
||||
constructor(context: Context, mode: Mode, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
this.mode = mode
|
||||
LayoutInflater.from(context).inflate(R.layout.view_quote, this)
|
||||
// Add padding here (not on mainQuoteViewContainer) to get a bit of a top inset while avoiding
|
||||
binding = ViewQuoteBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
// Add padding here (not on binding.mainQuoteViewContainer) to get a bit of a top inset while avoiding
|
||||
// the clipping issue described in getIntrinsicHeight(maxContentWidth:).
|
||||
setPadding(0, toPx(6, resources), 0, 0)
|
||||
when (mode) {
|
||||
Mode.Draft -> quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
|
||||
Mode.Draft -> binding.quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
|
||||
Mode.Regular -> {
|
||||
quoteViewCancelButton.isVisible = false
|
||||
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
|
||||
binding.quoteViewCancelButton.isVisible = false
|
||||
binding.mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,19 +70,19 @@ class QuoteView : LinearLayout {
|
||||
// region General
|
||||
fun getIntrinsicContentHeight(maxContentWidth: Int): Int {
|
||||
// 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 authorTextViewIntrinsicHeight = 0
|
||||
if (quoteViewAuthorTextView.isVisible) {
|
||||
val author = quoteViewAuthorTextView.text
|
||||
authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, quoteViewAuthorTextView.paint, maxContentWidth)
|
||||
val authorTextViewIntrinsicHeight: Int
|
||||
if (binding.quoteViewAuthorTextView.isVisible) {
|
||||
val author = binding.quoteViewAuthorTextView.text
|
||||
authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, binding.quoteViewAuthorTextView.paint, maxContentWidth)
|
||||
result += authorTextViewIntrinsicHeight
|
||||
}
|
||||
val body = quoteViewBodyTextView.text
|
||||
val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, quoteViewBodyTextView.paint, maxContentWidth)
|
||||
val staticLayout = TextUtilities.getIntrinsicLayout(body, quoteViewBodyTextView.paint, maxContentWidth)
|
||||
val body = binding.quoteViewBodyTextView.text
|
||||
val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
|
||||
val staticLayout = TextUtilities.getIntrinsicLayout(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
|
||||
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.
|
||||
// Height from intrinsic layout is the height of the text before truncation so we shorten
|
||||
// proportionally to our max lines setting.
|
||||
@ -110,89 +107,114 @@ class QuoteView : LinearLayout {
|
||||
|
||||
// region Updating
|
||||
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) {
|
||||
// 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
|
||||
// to get too big.
|
||||
quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3
|
||||
binding.quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3
|
||||
// Author
|
||||
if (thread.isGroupRecipient) {
|
||||
val author = contactDb.getContactWithSessionID(authorPublicKey)
|
||||
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
|
||||
quoteViewAuthorTextView.text = authorDisplayName
|
||||
quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
binding.quoteViewAuthorTextView.text = authorDisplayName
|
||||
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
}
|
||||
quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
||||
binding.quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
||||
// Body
|
||||
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.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context)
|
||||
binding.quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
// Accent line / attachment preview
|
||||
val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing
|
||||
quoteViewAccentLine.isVisible = !hasAttachments
|
||||
quoteViewAttachmentPreviewContainer.isVisible = hasAttachments
|
||||
binding.quoteViewAccentLine.isVisible = !hasAttachments
|
||||
binding.quoteViewAttachmentPreviewContainer.isVisible = hasAttachments
|
||||
if (!hasAttachments) {
|
||||
val accentLineLayoutParams = quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams
|
||||
accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height
|
||||
quoteViewAccentLine.layoutParams = accentLineLayoutParams
|
||||
quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage))
|
||||
binding.quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage))
|
||||
} 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 backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme)
|
||||
quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
||||
quoteViewAttachmentPreviewImageView.isVisible = false
|
||||
quoteViewAttachmentThumbnailImageView.isVisible = false
|
||||
if (attachments.audioSlide != null) {
|
||||
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||
quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
|
||||
} else if (attachments.documentSlide != null) {
|
||||
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
||||
quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
quoteViewBodyTextView.text = resources.getString(R.string.document)
|
||||
} else if (attachments.thumbnailSlide != null) {
|
||||
val slide = attachments.thumbnailSlide!!
|
||||
// This internally fetches the thumbnail
|
||||
quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources)
|
||||
quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false)
|
||||
quoteViewAttachmentThumbnailImageView.isVisible = true
|
||||
quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
|
||||
binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
||||
binding.quoteViewAttachmentPreviewImageView.isVisible = false
|
||||
binding.quoteViewAttachmentThumbnailImageView.isVisible = false
|
||||
when {
|
||||
attachments.audioSlide != null -> {
|
||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
binding.quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
|
||||
}
|
||||
attachments.documentSlide != null -> {
|
||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
||||
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
binding.quoteViewBodyTextView.text = resources.getString(R.string.document)
|
||||
}
|
||||
attachments.thumbnailSlide != null -> {
|
||||
val slide = attachments.thumbnailSlide!!
|
||||
// This internally fetches the thumbnail
|
||||
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
|
||||
|
||||
// region Convenience
|
||||
@ColorInt private fun getLineColor(isOutgoingMessage: Boolean): Int {
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||
if ((mode == Mode.Regular && isLightMode) || (mode == Mode.Draft && isLightMode)) {
|
||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
} else if (mode == Mode.Regular && !isLightMode) {
|
||||
if (isOutgoingMessage) {
|
||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
} else {
|
||||
return ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
||||
return when {
|
||||
mode == Mode.Regular && isLightMode || mode == Mode.Draft && isLightMode -> {
|
||||
ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
}
|
||||
mode == Mode.Regular && !isLightMode -> {
|
||||
if (isOutgoingMessage) {
|
||||
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 {
|
||||
if (mode == Mode.Draft) { return ResourcesCompat.getColor(resources, R.color.text, context.theme) }
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||
if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) {
|
||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
return if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) {
|
||||
ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
} 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
|
||||
}
|
||||
|
||||
|
@ -6,15 +6,15 @@ import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.android.synthetic.main.view_untrusted_attachment.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
|
||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
class UntrustedAttachmentView: LinearLayout {
|
||||
|
||||
private lateinit var binding: ViewUntrustedAttachmentBinding
|
||||
enum class AttachmentType {
|
||||
AUDIO,
|
||||
DOCUMENT,
|
||||
@ -27,7 +27,7 @@ class UntrustedAttachmentView: LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_untrusted_attachment, this)
|
||||
binding = ViewUntrustedAttachmentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -42,8 +42,8 @@ class UntrustedAttachmentView: LinearLayout {
|
||||
iconDrawable.mutate().setTint(textColor)
|
||||
val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT))
|
||||
|
||||
untrustedAttachmentIcon.setImageDrawable(iconDrawable)
|
||||
untrustedAttachmentTitle.text = text
|
||||
binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable)
|
||||
binding.untrustedAttachmentTitle.text = text
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
@ -5,16 +5,17 @@ import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Spannable
|
||||
import android.text.StaticLayout
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.text.util.Linkify
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@ -23,24 +24,22 @@ import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.text.getSpans
|
||||
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.databinding.ViewVisibleMessageContentBinding
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
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.ModalUrlBottomSheet
|
||||
import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.SearchUtil
|
||||
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.util.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
@ -48,7 +47,8 @@ import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
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 delegate: VisibleMessageContentViewDelegate? = null
|
||||
var indexInAdapter: Int = -1
|
||||
@ -59,7 +59,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_visible_message_content, this)
|
||||
binding = ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -73,23 +73,42 @@ class VisibleMessageContentView : LinearLayout {
|
||||
val filter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
|
||||
background.colorFilter = filter
|
||||
setBackground(background)
|
||||
// Body
|
||||
mainContainer.removeAllViews()
|
||||
onContentClick = null
|
||||
|
||||
val onlyBodyMessage = message is SmsMessageRecord
|
||||
val mediaThumbnailMessage = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.thumbnailSlide != null
|
||||
|
||||
// reset visibilities / containers
|
||||
onContentClick.clear()
|
||||
binding.albumThumbnailView.clearViews()
|
||||
onContentDoubleTap = null
|
||||
|
||||
if (message.isDeleted) {
|
||||
val deletedMessageView = DeletedMessageView(context)
|
||||
deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message))
|
||||
mainContainer.addView(deletedMessageView)
|
||||
} else if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
||||
val linkPreviewView = LinkPreviewView(context)
|
||||
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
|
||||
mainContainer.addView(linkPreviewView)
|
||||
onContentClick = { event -> linkPreviewView.calculateHit(event) }
|
||||
// Body text view is inside the link preview for layout convenience
|
||||
} else if (message is MmsMessageRecord && message.quote != null) {
|
||||
binding.deletedMessageView.isVisible = true
|
||||
binding.deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message))
|
||||
return
|
||||
} else {
|
||||
binding.deletedMessageView.isVisible = false
|
||||
}
|
||||
|
||||
binding.quoteView.isVisible = 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 quoteView = QuoteView(context, QuoteView.Mode.Regular)
|
||||
// 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
|
||||
// here to get the layout right.
|
||||
@ -99,136 +118,161 @@ class VisibleMessageContentView : LinearLayout {
|
||||
} else {
|
||||
quote.text
|
||||
}
|
||||
quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
|
||||
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId,
|
||||
binding.quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
|
||||
message.isOutgoing, message.isOpenGroupInvitation, message.threadId,
|
||||
quote.isOriginalMissing, glide)
|
||||
mainContainer.addView(quoteView)
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
ViewUtil.setPaddingTop(bodyTextView, 0)
|
||||
mainContainer.addView(bodyTextView)
|
||||
onContentClick = { event ->
|
||||
onContentClick.add { event ->
|
||||
val r = Rect()
|
||||
quoteView.getGlobalVisibleRect(r)
|
||||
binding.quoteView.getGlobalVisibleRect(r)
|
||||
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
|
||||
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) {
|
||||
hideBody = true
|
||||
// Audio attachment
|
||||
if (contactIsTrusted || message.isOutgoing) {
|
||||
val voiceMessageView = VoiceMessageView(context)
|
||||
voiceMessageView.indexInAdapter = indexInAdapter
|
||||
voiceMessageView.delegate = context as? ConversationActivityV2
|
||||
voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
mainContainer.addView(voiceMessageView)
|
||||
binding.voiceMessageView.indexInAdapter = indexInAdapter
|
||||
binding.voiceMessageView.delegate = context as? ConversationActivityV2
|
||||
binding.voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
// 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.
|
||||
onContentClick = { voiceMessageView.togglePlayback() }
|
||||
onContentDoubleTap = { voiceMessageView.handleDoubleTap() }
|
||||
onContentClick.add { binding.voiceMessageView.togglePlayback() }
|
||||
onContentDoubleTap = { binding.voiceMessageView.handleDoubleTap() }
|
||||
} else {
|
||||
val untrustedView = UntrustedAttachmentView(context)
|
||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
|
||||
mainContainer.addView(untrustedView)
|
||||
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
// TODO: move this out to its own area
|
||||
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
|
||||
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
|
||||
hideBody = true
|
||||
// Document attachment
|
||||
if (contactIsTrusted || message.isOutgoing) {
|
||||
val documentView = DocumentView(context)
|
||||
documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
mainContainer.addView(documentView)
|
||||
binding.documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
} else {
|
||||
val untrustedView = UntrustedAttachmentView(context)
|
||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
|
||||
mainContainer.addView(untrustedView)
|
||||
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
|
||||
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
|
||||
// Images/Video attachment
|
||||
/*
|
||||
* Images / Video attachment
|
||||
*/
|
||||
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
|
||||
// bind after add view because views are inflated and calculated during bind
|
||||
albumThumbnailView.bind(
|
||||
binding.albumThumbnailView.bind(
|
||||
glideRequests = glide,
|
||||
message = message,
|
||||
isStart = isStartOfMessageCluster,
|
||||
isEnd = isEndOfMessageCluster
|
||||
)
|
||||
onContentClick = { event ->
|
||||
albumThumbnailView.calculateHitObject(event, message, thread)
|
||||
onContentClick.add { event ->
|
||||
binding.albumThumbnailView.calculateHitObject(event, message, thread)
|
||||
}
|
||||
} else {
|
||||
val untrustedView = UntrustedAttachmentView(context)
|
||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
||||
mainContainer.addView(untrustedView)
|
||||
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
hideBody = true
|
||||
binding.albumThumbnailView.clearViews()
|
||||
binding.untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
||||
onContentClick.add { binding.untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
} else if (message.isOpenGroupInvitation) {
|
||||
val openGroupInvitationView = OpenGroupInvitationView(context)
|
||||
openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
mainContainer.addView(openGroupInvitationView)
|
||||
onContentClick = { openGroupInvitationView.joinOpenGroup() }
|
||||
} else {
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
mainContainer.addView(bodyTextView)
|
||||
onContentClick = { event ->
|
||||
// intersectedModalSpans should only be a list of one item
|
||||
bodyTextView.getIntersectedModalSpans(event).forEach { span ->
|
||||
span.onClick(bodyTextView)
|
||||
hideBody = true
|
||||
binding.openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
onContentClick.add { binding.openGroupInvitationView.joinOpenGroup() }
|
||||
}
|
||||
|
||||
binding.bodyTextView.isVisible = message.body.isNotEmpty() && !hideBody
|
||||
|
||||
// set it to use constraints if not only a text message, otherwise wrap content to whatever width it wants
|
||||
val params = binding.bodyTextView.layoutParams
|
||||
params.width = if (onlyBodyMessage || binding.barrierViewsGone()) ViewGroup.LayoutParams.WRAP_CONTENT else 0
|
||||
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 {
|
||||
val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster)
|
||||
@DrawableRes val backgroundID: Int
|
||||
if (isSingleMessage) {
|
||||
backgroundID = 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
|
||||
} else if (isEndOfMessageCluster) {
|
||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end
|
||||
} else {
|
||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle
|
||||
@DrawableRes val backgroundID = when {
|
||||
isSingleMessage -> {
|
||||
if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
|
||||
}
|
||||
isStartOfMessageCluster -> {
|
||||
if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start
|
||||
}
|
||||
isEndOfMessageCluster -> {
|
||||
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)!!
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// region Convenience
|
||||
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 {
|
||||
var body = message.body.toSpannable()
|
||||
|
||||
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(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
|
||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(),
|
||||
{ BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
|
||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(),
|
||||
{ ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
|
||||
|
||||
Linkify.addLinks(body, Linkify.WEB_URLS)
|
||||
|
||||
|
@ -5,39 +5,51 @@ import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
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.RelativeLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.view_visible_message.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
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.home.UserDetailsBottomSheet
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.*
|
||||
import java.util.*
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
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 kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class VisibleMessageView : LinearLayout {
|
||||
|
||||
@ -48,6 +60,7 @@ class VisibleMessageView : LinearLayout {
|
||||
@Inject lateinit var smsDb: SmsDatabase
|
||||
@Inject lateinit var mmsDb: MmsDatabase
|
||||
|
||||
private lateinit var binding: ViewVisibleMessageBinding
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
||||
private val swipeToReplyIconRect = Rect()
|
||||
@ -60,7 +73,11 @@ class VisibleMessageView : LinearLayout {
|
||||
private var onDoubleTap: (() -> Unit)? = null
|
||||
var indexInAdapter: Int = -1
|
||||
var snIsSelected = false
|
||||
set(value) { field = value; handleIsSelectedChanged()}
|
||||
set(value) {
|
||||
field = value
|
||||
binding.messageTimestampTextView.isVisible = isSelected
|
||||
handleIsSelectedChanged()
|
||||
}
|
||||
var onPress: ((event: MotionEvent) -> Unit)? = null
|
||||
var onSwipeToReply: (() -> Unit)? = null
|
||||
var onLongPress: (() -> Unit)? = null
|
||||
@ -68,7 +85,7 @@ class VisibleMessageView : LinearLayout {
|
||||
|
||||
companion object {
|
||||
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 maxDoubleTapInterval = 200L
|
||||
}
|
||||
@ -79,12 +96,12 @@ class VisibleMessageView : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 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)
|
||||
isHapticFeedbackEnabled = true
|
||||
setWillNotDraw(false)
|
||||
expirationTimerViewContainer.disableClipping()
|
||||
messageContentContainer.disableClipping()
|
||||
binding.expirationTimerViewContainer.disableClipping()
|
||||
binding.messageContentContainer.disableClipping()
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -101,47 +118,46 @@ class VisibleMessageView : LinearLayout {
|
||||
// Show profile picture and sender name if this is a group thread AND
|
||||
// the message is incoming
|
||||
if (isGroupThread && !message.isOutgoing) {
|
||||
profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
|
||||
profilePictureView.publicKey = senderSessionID
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.update(message.individualRecipient, threadID)
|
||||
profilePictureView.setOnClickListener {
|
||||
binding.profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
|
||||
binding.profilePictureView.publicKey = senderSessionID
|
||||
binding.profilePictureView.glide = glide
|
||||
binding.profilePictureView.update(message.individualRecipient)
|
||||
binding.profilePictureView.setOnClickListener {
|
||||
showUserDetails(senderSessionID, threadID)
|
||||
}
|
||||
if (thread.isOpenGroupRecipient) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||
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 {
|
||||
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
|
||||
senderNameTextView.text = contact?.displayName(context) ?: senderSessionID
|
||||
binding.senderNameTextView.text = contact?.displayName(context) ?: senderSessionID
|
||||
} else {
|
||||
profilePictureContainer.visibility = View.GONE
|
||||
senderNameTextView.visibility = View.GONE
|
||||
binding.profilePictureContainer.visibility = View.GONE
|
||||
binding.senderNameTextView.visibility = View.GONE
|
||||
}
|
||||
// Date break
|
||||
dateBreakTextView.showDateBreak(message, previous)
|
||||
binding.dateBreakTextView.showDateBreak(message, previous)
|
||||
// Timestamp
|
||||
messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
|
||||
binding.messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
|
||||
// Margins
|
||||
val startPadding: Int
|
||||
if (isGroupThread) {
|
||||
startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() else 0
|
||||
val startPadding = if (isGroupThread) {
|
||||
if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.very_large_spacing) else toPx(50,resources)
|
||||
} else {
|
||||
startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt()
|
||||
else resources.getDimension(R.dimen.medium_spacing).toInt()
|
||||
if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.very_large_spacing)
|
||||
else resources.getDimensionPixelSize(R.dimen.medium_spacing)
|
||||
}
|
||||
val endPadding = if (message.isOutgoing) resources.getDimension(R.dimen.medium_spacing).toInt()
|
||||
else resources.getDimension(R.dimen.very_large_spacing).toInt()
|
||||
messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0)
|
||||
val endPadding = if (message.isOutgoing) resources.getDimensionPixelSize(R.dimen.medium_spacing)
|
||||
else resources.getDimensionPixelSize(R.dimen.very_large_spacing)
|
||||
binding.messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0)
|
||||
// Set inter-message spacing
|
||||
setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
// Gravity
|
||||
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
|
||||
val (iconID, iconColor) = getMessageStatusImage(message)
|
||||
if (iconID != null) {
|
||||
@ -149,24 +165,24 @@ class VisibleMessageView : LinearLayout {
|
||||
if (iconColor != null) {
|
||||
drawable?.setTint(iconColor)
|
||||
}
|
||||
messageStatusImageView.setImageDrawable(drawable)
|
||||
binding.messageStatusImageView.setImageDrawable(drawable)
|
||||
}
|
||||
if (message.isOutgoing) {
|
||||
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
||||
messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
|
||||
binding.messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
|
||||
} else {
|
||||
messageStatusImageView.isVisible = false
|
||||
binding.messageStatusImageView.isVisible = false
|
||||
}
|
||||
// Expiration timer
|
||||
updateExpirationTimer(message)
|
||||
// Calculate max message bubble width
|
||||
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
|
||||
messageContentView.indexInAdapter = indexInAdapter
|
||||
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false))
|
||||
messageContentView.delegate = contentViewDelegate
|
||||
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
|
||||
binding.messageContentView.indexInAdapter = indexInAdapter
|
||||
binding.messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false))
|
||||
binding.messageContentView.delegate = contentViewDelegate
|
||||
onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() }
|
||||
}
|
||||
|
||||
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||
@ -207,29 +223,31 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
private fun updateExpirationTimer(message: MessageRecord) {
|
||||
val expirationTimerViewLayoutParams = expirationTimerView.layoutParams as RelativeLayout.LayoutParams
|
||||
val ruleToAdd = if (message.isOutgoing) RelativeLayout.ALIGN_START else RelativeLayout.ALIGN_END
|
||||
val ruleToRemove = if (message.isOutgoing) RelativeLayout.ALIGN_END else RelativeLayout.ALIGN_START
|
||||
expirationTimerViewLayoutParams.removeRule(ruleToRemove)
|
||||
expirationTimerViewLayoutParams.addRule(ruleToAdd, R.id.messageContentView)
|
||||
val expirationTimerViewLayoutParams = binding.expirationTimerView.layoutParams as MarginLayoutParams
|
||||
val container = binding.expirationTimerViewContainer
|
||||
val content = binding.messageContentView
|
||||
val expiration = binding.expirationTimerView
|
||||
container.removeAllViewsInLayout()
|
||||
container.addView(if (message.isOutgoing) expiration else content)
|
||||
container.addView(if (message.isOutgoing) content else expiration)
|
||||
val expirationTimerViewSize = toPx(12, resources)
|
||||
val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt()
|
||||
expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0
|
||||
expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize)
|
||||
expirationTimerView.layoutParams = expirationTimerViewLayoutParams
|
||||
binding.expirationTimerView.layoutParams = expirationTimerViewLayoutParams
|
||||
if (message.expiresIn > 0 && !message.isPending) {
|
||||
expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
|
||||
expirationTimerView.isVisible = true
|
||||
expirationTimerView.setPercentComplete(0.0f)
|
||||
binding.expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
|
||||
binding.expirationTimerView.isVisible = true
|
||||
binding.expirationTimerView.setPercentComplete(0.0f)
|
||||
if (message.expireStarted > 0) {
|
||||
expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
||||
expirationTimerView.startAnimation()
|
||||
binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
||||
binding.expirationTimerView.startAnimation()
|
||||
if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) {
|
||||
ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule()
|
||||
}
|
||||
} else if (!message.isMediaPending) {
|
||||
expirationTimerView.setPercentComplete(0.0f)
|
||||
expirationTimerView.stopAnimation()
|
||||
binding.expirationTimerView.setPercentComplete(0.0f)
|
||||
binding.expirationTimerView.stopAnimation()
|
||||
ThreadUtils.queue {
|
||||
val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager
|
||||
val id = message.getId()
|
||||
@ -238,12 +256,13 @@ class VisibleMessageView : LinearLayout {
|
||||
expirationManager.scheduleDeletion(id, mms, message.expiresIn)
|
||||
}
|
||||
} else {
|
||||
expirationTimerView.stopAnimation()
|
||||
expirationTimerView.setPercentComplete(0.0f)
|
||||
binding.expirationTimerView.stopAnimation()
|
||||
binding.expirationTimerView.setPercentComplete(0.0f)
|
||||
}
|
||||
} else {
|
||||
expirationTimerView.isVisible = false
|
||||
binding.expirationTimerView.isVisible = false
|
||||
}
|
||||
container.requestLayout()
|
||||
}
|
||||
|
||||
private fun handleIsSelectedChanged() {
|
||||
@ -255,14 +274,14 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
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 threshold = VisibleMessageView.swipeToReplyThreshold
|
||||
val threshold = swipeToReplyThreshold
|
||||
val iconSize = toPx(24, context.resources)
|
||||
val bottomVOffset = paddingBottom + messageStatusImageView.height + (messageContentView.height - iconSize) / 2
|
||||
swipeToReplyIconRect.left = messageContentContainer.right - messageContentContainer.paddingEnd + spacing
|
||||
val bottomVOffset = paddingBottom + binding.messageStatusImageView.height + (binding.messageContentView.height - iconSize) / 2
|
||||
swipeToReplyIconRect.left = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + spacing
|
||||
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
|
||||
swipeToReplyIcon.bounds = swipeToReplyIconRect
|
||||
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
|
||||
@ -274,8 +293,8 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
profilePictureView.recycle()
|
||||
messageContentView.recycle()
|
||||
binding.profilePictureView.recycle()
|
||||
binding.messageContentView.recycle()
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -296,13 +315,13 @@ class VisibleMessageView : LinearLayout {
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
val newLongPressCallback = Runnable { onLongPress() }
|
||||
this.longPressCallback = newLongPressCallback
|
||||
gestureHandler.postDelayed(newLongPressCallback, VisibleMessageView.longPressDurationThreshold)
|
||||
gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold)
|
||||
onDownTimestamp = Date().time
|
||||
}
|
||||
|
||||
private fun onMove(event: MotionEvent) {
|
||||
val translationX = toDp(event.rawX + dx, context.resources)
|
||||
if (abs(translationX) < VisibleMessageView.longPressMovementTreshold || snIsSelected) {
|
||||
if (abs(translationX) < longPressMovementThreshold || snIsSelected) {
|
||||
return
|
||||
} else {
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
@ -313,20 +332,16 @@ class VisibleMessageView : LinearLayout {
|
||||
val sign = -1.0f
|
||||
val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign
|
||||
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
|
||||
if (abs(x) > VisibleMessageView.swipeToReplyThreshold && abs(previousTranslationX) < VisibleMessageView.swipeToReplyThreshold) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||
} else {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
}
|
||||
if (abs(x) > swipeToReplyThreshold && abs(previousTranslationX) < swipeToReplyThreshold) {
|
||||
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||
}
|
||||
previousTranslationX = x
|
||||
}
|
||||
|
||||
private fun onCancel(event: MotionEvent) {
|
||||
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) {
|
||||
if (abs(translationX) > swipeToReplyThreshold) {
|
||||
onSwipeToReply?.invoke()
|
||||
}
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
@ -334,9 +349,9 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
private fun onUp(event: MotionEvent) {
|
||||
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) {
|
||||
if (abs(translationX) > swipeToReplyThreshold) {
|
||||
onSwipeToReply?.invoke()
|
||||
} else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) {
|
||||
} else if ((Date().time - onDownTimestamp) < longPressDurationThreshold) {
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
val pressCallback = this.pressCallback
|
||||
if (pressCallback != null) {
|
||||
@ -363,7 +378,7 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
.start()
|
||||
// Bit of a hack to keep the date break text view from moving
|
||||
dateBreakTextView.animate()
|
||||
binding.dateBreakTextView.animate()
|
||||
.translationX(0.0f)
|
||||
.setDuration(150)
|
||||
.start()
|
||||
@ -375,7 +390,7 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
fun onContentClick(event: MotionEvent) {
|
||||
messageContentView.onContentClick?.invoke(event)
|
||||
binding.messageContentView.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
|
||||
}
|
||||
|
||||
private fun onPress(event: MotionEvent) {
|
||||
@ -393,5 +408,9 @@ class VisibleMessageView : LinearLayout {
|
||||
val activity = context as AppCompatActivity
|
||||
userDetailsBottomSheet.show(activity.supportFragmentManager, userDetailsBottomSheet.tag)
|
||||
}
|
||||
|
||||
fun playVoiceMessage() {
|
||||
binding.messageContentView.playVoiceMessage()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.view_voice_message.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVoiceMessageBinding
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
@ -26,6 +26,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
|
||||
@Inject lateinit var attachmentDb: AttachmentDatabase
|
||||
|
||||
private lateinit var binding: ViewVoiceMessageBinding
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var isPlaying = false
|
||||
set(value) {
|
||||
@ -44,8 +45,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_voice_message, this)
|
||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
binding = ViewVoiceMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(0),
|
||||
TimeUnit.MILLISECONDS.toSeconds(0))
|
||||
}
|
||||
@ -54,7 +55,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
// region Updating
|
||||
fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||
val audio = message.slideDeck.audioSlide!!
|
||||
voiceMessageViewLoader.isVisible = audio.isInProgress
|
||||
binding.voiceMessageViewLoader.isVisible = audio.isInProgress
|
||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||
cornerMask.setTopRightRadius(cornerRadii[1])
|
||||
@ -74,8 +75,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras ->
|
||||
if (audioExtras.durationMs > 0) {
|
||||
duration = audioExtras.durationMs
|
||||
voiceMessageViewDurationTextView.visibility = View.VISIBLE
|
||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
binding.voiceMessageViewDurationTextView.visibility = View.VISIBLE
|
||||
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
|
||||
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs))
|
||||
}
|
||||
@ -99,12 +100,12 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
|
||||
private fun handleProgressChanged(progress: Double) {
|
||||
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.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()
|
||||
progressView.layoutParams = layoutParams
|
||||
binding.progressView.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
override fun onPlayerStop(player: AudioSlidePlayer) {
|
||||
@ -118,7 +119,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
|
||||
private fun renderIcon() {
|
||||
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
|
||||
voiceMessagePlaybackImageView.setImageResource(iconID)
|
||||
binding.voiceMessagePlaybackImageView.setImageResource(iconID)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
@ -5,11 +5,12 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_search_bottom_bar.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewSearchBottomBarBinding
|
||||
|
||||
|
||||
class SearchBottomBar : LinearLayout {
|
||||
private lateinit var binding: ViewSearchBottomBarBinding
|
||||
private var eventListener: EventListener? = null
|
||||
|
||||
// region Lifecycle
|
||||
@ -18,10 +19,10 @@ class SearchBottomBar : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 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
|
||||
searchUp.setOnClickListener { v: View? ->
|
||||
if (eventListener != null) {
|
||||
@ -43,7 +44,7 @@ class SearchBottomBar : LinearLayout {
|
||||
}
|
||||
|
||||
fun showLoading() {
|
||||
searchProgressWheel.visibility = VISIBLE
|
||||
binding.searchProgressWheel.visibility = VISIBLE
|
||||
}
|
||||
|
||||
private fun setViewEnabled(view: View, enabled: Boolean) {
|
||||
|
@ -11,6 +11,7 @@ import org.session.libsession.utilities.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor
|
||||
import org.thoughtcrime.securesms.database.CursorList
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase
|
||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.search.SearchRepository
|
||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||
@ -20,14 +21,11 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SearchViewModel @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
searchDb: SearchDatabase,
|
||||
threadDb: ThreadDatabase
|
||||
private val searchRepository: SearchRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val searchRepository: SearchRepository
|
||||
private val result: CloseableLiveData<SearchResult>
|
||||
private val debouncer: Debouncer
|
||||
private val result: CloseableLiveData<SearchResult> = CloseableLiveData()
|
||||
private val debouncer: Debouncer = Debouncer(500)
|
||||
private var firstSearch = false
|
||||
private var searchOpen = false
|
||||
private var activeQuery: String? = null
|
||||
@ -107,13 +105,4 @@ class SearchViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
result = CloseableLiveData()
|
||||
debouncer = Debouncer(500)
|
||||
searchRepository = SearchRepository(context,
|
||||
searchDb,
|
||||
threadDb,
|
||||
ContactAccessor.getInstance(),
|
||||
SignalExecutors.SERIAL)
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
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.drawable.DrawableTransitionOptions
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import kotlinx.android.synthetic.main.thumbnail_view.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ThumbnailViewBinding
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.utilities.Util.equals
|
||||
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.GlideDrawableListeningTarget
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.*
|
||||
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 {
|
||||
|
||||
private lateinit var binding: ThumbnailViewBinding
|
||||
companion object {
|
||||
private const val WIDTH = 0
|
||||
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, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
||||
|
||||
private val image by lazy { thumbnail_image }
|
||||
private val playOverlay by lazy { play_overlay }
|
||||
val loadIndicator: View by lazy { thumbnail_load_indicator }
|
||||
val downloadIndicator: View by lazy { thumbnail_download_icon }
|
||||
private val image by lazy { binding.thumbnailImage }
|
||||
private val playOverlay by lazy { binding.playOverlay }
|
||||
val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
|
||||
val downloadIndicator: View by lazy { binding.thumbnailDownloadIcon }
|
||||
|
||||
private val dimensDelegate = ThumbnailDimensDelegate()
|
||||
|
||||
@ -48,7 +51,7 @@ open class KThumbnailView: FrameLayout {
|
||||
private var radius: Int = 0
|
||||
|
||||
private fun initialize(attrs: AttributeSet?) {
|
||||
inflate(context, R.layout.thumbnail_view, this)
|
||||
binding = ThumbnailViewBinding.inflate(LayoutInflater.from(context), this)
|
||||
if (attrs != null) {
|
||||
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities;
|
||||
|
||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
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.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
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.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.GlideDrawableListeningTarget;
|
||||
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.SlideClickListener;
|
||||
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.Locale;
|
||||
|
||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ThumbnailView extends FrameLayout {
|
||||
|
||||
@ -287,7 +286,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
} else if (slide.hasPlaceholder()) {
|
||||
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(image, result));
|
||||
} else {
|
||||
glideRequests.clear(image);
|
||||
glideRequests.load(R.drawable.ic_image_white_24dp).centerInside().into(image);
|
||||
result.set(false);
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ import org.session.libsignal.database.LokiOpenGroupDatabaseProtocol;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
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);
|
||||
return Optional.fromNullable(reader.getCurrent());
|
||||
}
|
||||
@ -146,6 +147,29 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
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) {
|
||||
List<Address> members = getCurrentMembers(groupId, false);
|
||||
List<Recipient> recipients = new LinkedList<>();
|
||||
|
@ -450,7 +450,7 @@ private inline fun <reified T> wrap(x: T): Array<T> {
|
||||
|
||||
private fun wrap(x: Map<String, String>): ContentValues {
|
||||
val result = ContentValues(x.size)
|
||||
x.forEach { result.put(it.key, it.value) }
|
||||
x.iterator().forEach { result.put(it.key, it.value) }
|
||||
return result
|
||||
}
|
||||
// endregion
|
@ -139,7 +139,7 @@ public class MmsSmsDatabase extends Database {
|
||||
|
||||
try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) {
|
||||
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 {
|
||||
return cursor != null ? cursor.getCount() : 0;
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();;
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@ -8,8 +9,8 @@ import com.annimon.stream.Stream;
|
||||
import net.sqlcipher.Cursor;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
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 + " " +
|
||||
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " +
|
||||
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
|
||||
"LIMIT 500";
|
||||
"LIMIT ?";
|
||||
|
||||
private static final String MESSAGES_FOR_THREAD_QUERY =
|
||||
"SELECT " +
|
||||
@ -115,7 +116,9 @@ public class SearchDatabase extends Database {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
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);
|
||||
return cursor;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import androidx.core.database.getStringOrNull
|
||||
import net.sqlcipher.Cursor
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsignal.utilities.Base64
|
||||
@ -73,7 +74,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
||||
notifyConversationListListeners()
|
||||
}
|
||||
|
||||
private fun contactFromCursor(cursor: Cursor): Contact {
|
||||
fun contactFromCursor(cursor: Cursor): Contact {
|
||||
val sessionID = cursor.getString(sessionID)
|
||||
val contact = Contact(sessionID)
|
||||
contact.name = cursor.getStringOrNull(name)
|
||||
@ -87,4 +88,29 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
||||
contact.isTrusted = cursor.getInt(isTrusted) != 0
|
||||
return contact
|
||||
}
|
||||
|
||||
fun contactFromCursor(cursor: android.database.Cursor): Contact {
|
||||
val sessionID = cursor.getString(cursor.getColumnIndexOrThrow(sessionID))
|
||||
val contact = Contact(sessionID)
|
||||
contact.name = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(name))
|
||||
contact.nickname = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(nickname))
|
||||
contact.profilePictureURL = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureURL))
|
||||
contact.profilePictureFileName = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureFileName))
|
||||
cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureEncryptionKey))?.let {
|
||||
contact.profilePictureEncryptionKey = Base64.decode(it)
|
||||
}
|
||||
contact.threadID = cursor.getLong(cursor.getColumnIndexOrThrow(threadID))
|
||||
contact.isTrusted = cursor.getInt(cursor.getColumnIndexOrThrow(isTrusted)) != 0
|
||||
return contact
|
||||
}
|
||||
|
||||
fun queryContactsByName(constraint: String): Cursor {
|
||||
return databaseHelper.readableDatabase.query(
|
||||
sessionContactTable, null, " $name LIKE ? OR $nickname LIKE ?", arrayOf(
|
||||
"%$constraint%",
|
||||
"%$constraint%"
|
||||
),
|
||||
null, null, null
|
||||
)
|
||||
}
|
||||
}
|
@ -45,6 +45,7 @@ import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsignal.utilities.Pair;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
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.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
|
||||
|
||||
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) {
|
||||
if (filter == null || filter.size() == 0)
|
||||
return null;
|
||||
@ -593,6 +608,18 @@ public class ThreadDatabase extends Database {
|
||||
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) {
|
||||
Recipient threadRecipient = getRecipientForThreadId(threadId);
|
||||
return threadRecipient != null && !threadRecipient.isOpenGroupRecipient();
|
||||
@ -692,14 +719,14 @@ public class ThreadDatabase extends Database {
|
||||
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
|
||||
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT));
|
||||
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 deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DELIVERY_RECEIPT_COUNT));
|
||||
int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ_RECEIPT_COUNT));
|
||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN));
|
||||
long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN));
|
||||
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)) {
|
||||
readReceiptCount = 0;
|
||||
|
@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.session.libsession.utilities.AppTextSecurePreferences
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import org.thoughtcrime.securesms.repository.DefaultConversationRepository
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class AppModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindTextSecurePreferences(preferences: AppTextSecurePreferences): TextSecurePreferences
|
||||
|
||||
@Binds
|
||||
abstract fun bindConversationRepository(repository: DefaultConversationRepository): ConversationRepository
|
||||
|
||||
}
|
@ -9,16 +9,18 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
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.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
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.databinding.ActivityCreatePrivateChatBinding
|
||||
import network.loki.messenger.databinding.FragmentEnterPublicKeyBinding
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
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.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
|
||||
class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityCreatePrivateChatBinding
|
||||
private val adapter = CreatePrivateChatActivityAdapter(this)
|
||||
private var isKeyboardShowing = false
|
||||
set(value) {
|
||||
@ -47,37 +49,36 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityCreatePrivateChatBinding.inflate(layoutInflater)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_create_private_chat)
|
||||
setContentView(binding.root)
|
||||
// Set title
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_create_private_chat_title)
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
rootLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
val diff = rootLayout.rootView.height - rootLayout.height
|
||||
val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics
|
||||
val estimatedKeyboardHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics)
|
||||
this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight)
|
||||
}
|
||||
})
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
binding.rootLayout.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val diff = binding.rootLayout.rootView.height - binding.rootLayout.height
|
||||
val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics
|
||||
val estimatedKeyboardHeight =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics)
|
||||
this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun showLoader() {
|
||||
loader.visibility = View.VISIBLE
|
||||
loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
binding.loader.visibility = View.VISIBLE
|
||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
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?) {
|
||||
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
|
||||
class EnterPublicKeyFragment : Fragment() {
|
||||
private lateinit var binding: FragmentEnterPublicKeyBinding
|
||||
|
||||
var isKeyboardShowing = false
|
||||
set(value) { field = value; handleIsKeyboardShowingChanged() }
|
||||
|
||||
@ -165,32 +168,34 @@ class EnterPublicKeyFragment : Fragment() {
|
||||
}
|
||||
|
||||
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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
createPrivateChatIfPossible()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
with(binding) {
|
||||
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
createPrivateChatIfPossible()
|
||||
true
|
||||
} 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() {
|
||||
val optionalContentContainer = optionalContentContainer ?: return
|
||||
optionalContentContainer.isVisible = !isKeyboardShowing
|
||||
binding.optionalContentContainer.isVisible = !isKeyboardShowing
|
||||
}
|
||||
|
||||
private fun copyPublicKey() {
|
||||
@ -209,7 +214,7 @@ class EnterPublicKeyFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun createPrivateChatIfPossible() {
|
||||
val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString()
|
||||
val hexEncodedPublicKey = binding.publicKeyEditText.text?.trim().toString()
|
||||
val activity = requireActivity() as CreatePrivateChatActivity
|
||||
activity.createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
|
@ -1,22 +1,23 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.fragment_closed_group_edit_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import network.loki.messenger.databinding.FragmentClosedGroupEditBottomSheetBinding
|
||||
|
||||
public class ClosedGroupEditingOptionsBottomSheet : BottomSheetDialogFragment() {
|
||||
class ClosedGroupEditingOptionsBottomSheet : BottomSheetDialogFragment() {
|
||||
private lateinit var binding: FragmentClosedGroupEditBottomSheetBinding
|
||||
var onRemoveTapped: (() -> Unit)? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_closed_group_edit_bottom_sheet, container, false)
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentClosedGroupEditBottomSheetBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
removeFromGroup.setOnClickListener { onRemoveTapped?.invoke() }
|
||||
binding.removeFromGroup.setOnClickListener { onRemoveTapped?.invoke() }
|
||||
}
|
||||
}
|
@ -10,8 +10,8 @@ import android.widget.Toast
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.activity_create_closed_group.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityCreateClosedGroupBinding
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
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.fadeOut
|
||||
|
||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
||||
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||
private lateinit var binding: ActivityCreateClosedGroupBinding
|
||||
private var isLoading = false
|
||||
set(newValue) { field = newValue; invalidateOptionsMenu() }
|
||||
private var members = listOf<String>()
|
||||
@ -50,11 +50,12 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
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)
|
||||
recyclerView.adapter = this.selectContactsAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
binding.recyclerView.adapter = this.selectContactsAdapter
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||
}
|
||||
|
||||
@ -80,8 +81,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
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
|
||||
this.members = members.minus(publicKey)
|
||||
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
// endregion
|
||||
@ -95,12 +96,12 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
}
|
||||
|
||||
private fun createNewPrivateChat() {
|
||||
setResult(Companion.closedGroupCreatedResultCode)
|
||||
setResult(closedGroupCreatedResultCode)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun createClosedGroup() {
|
||||
val name = nameEditText.text.trim()
|
||||
val name = binding.nameEditText.text.trim()
|
||||
if (name.isEmpty()) {
|
||||
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)!!
|
||||
isLoading = true
|
||||
loaderContainer.fadeIn()
|
||||
binding.loaderContainer.fadeIn()
|
||||
MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
loaderContainer.fadeOut()
|
||||
binding.loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
val threadID = DatabaseComponent.get(this).threadDatabase().getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
if (!isFinishing) {
|
||||
@ -126,7 +127,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
finish()
|
||||
}
|
||||
}.failUi {
|
||||
loaderContainer.fadeOut()
|
||||
binding.loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
@ -8,12 +8,14 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
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.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.activity_settings.*
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.task
|
||||
|
@ -13,62 +13,63 @@ import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||
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 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.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
|
||||
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.groups.DefaultGroupsViewModel
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.State
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityJoinPublicChatBinding
|
||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityJoinPublicChatBinding.inflate(layoutInflater)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_join_public_chat)
|
||||
setContentView(binding.root)
|
||||
// Set title
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_join_public_chat_title)
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun showLoader() {
|
||||
loader.visibility = View.VISIBLE
|
||||
loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
binding.loader.visibility = View.VISIBLE
|
||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
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?) {
|
||||
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
|
||||
class EnterChatURLFragment : Fragment() {
|
||||
private lateinit var binding: FragmentEnterChatUrlBinding
|
||||
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
||||
|
||||
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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||
binding.chatURLEditText.imeOptions = binding.chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
binding.joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
||||
defaultRoomsContainer.isVisible = state is State.Success
|
||||
defaultRoomsLoaderContainer.isVisible = state is State.Loading
|
||||
defaultRoomsLoader.isVisible = state is State.Loading
|
||||
binding.defaultRoomsContainer.isVisible = state is State.Success
|
||||
binding.defaultRoomsLoaderContainer.isVisible = state is State.Loading
|
||||
binding.defaultRoomsLoader.isVisible = state is State.Loading
|
||||
when (state) {
|
||||
State.Loading -> {
|
||||
// TODO: Show a loader
|
||||
// TODO: Show a binding.loader
|
||||
}
|
||||
is State.Error -> {
|
||||
// TODO: Hide the loader
|
||||
// TODO: Hide the binding.loader
|
||||
}
|
||||
is State.Success -> {
|
||||
populateDefaultGroups(state.value)
|
||||
@ -195,10 +198,10 @@ class EnterChatURLFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
|
||||
defaultRoomsGridLayout.removeAllViews()
|
||||
defaultRoomsGridLayout.useDefaultMargins = false
|
||||
groups.forEach { defaultGroup ->
|
||||
val chip = layoutInflater.inflate(R.layout.default_group_chip, defaultRoomsGridLayout, false) as Chip
|
||||
binding.defaultRoomsGridLayout.removeAllViews()
|
||||
binding.defaultRoomsGridLayout.useDefaultMargins = false
|
||||
groups.iterator().forEach { defaultGroup ->
|
||||
val chip = layoutInflater.inflate(R.layout.default_group_chip, binding.defaultRoomsGridLayout, false) as Chip
|
||||
val drawable = defaultGroup.image?.let { bytes ->
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size)
|
||||
RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
|
||||
@ -210,18 +213,18 @@ class EnterChatURLFragment : Fragment() {
|
||||
chip.setOnClickListener {
|
||||
(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
|
||||
layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout)
|
||||
layoutInflater.inflate(R.layout.grid_layout_filler, binding.defaultRoomsGridLayout)
|
||||
}
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
private fun joinPublicChatIfPossible() {
|
||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
|
||||
val chatURL = chatURLEditText.text.trim().toString().toLowerCase(Locale.US)
|
||||
inputMethodManager.hideSoftInputFromWindow(binding.chatURLEditText.windowToken, 0)
|
||||
val chatURL = binding.chatURLEditText.text.trim().toString().toLowerCase(Locale.US)
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
||||
}
|
||||
// endregion
|
||||
|
@ -1,16 +1,16 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.os.Bundle
|
||||
import kotlinx.android.synthetic.main.activity_open_group_guidelines.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityOpenGroupGuidelinesBinding
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
|
||||
class OpenGroupGuidelinesActivity : BaseActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_open_group_guidelines)
|
||||
communityGuidelinesTextView.text = """
|
||||
val binding = ActivityOpenGroupGuidelinesBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.communityGuidelinesTextView.text = """
|
||||
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.
|
||||
|
@ -6,13 +6,12 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
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
|
||||
// is not the best idea. It doesn't survive configuration change.
|
||||
// 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 onUnblockTapped: (() -> Unit)? = null
|
||||
var onDeleteTapped: (() -> Unit)? = null
|
||||
var onMarkAllAsReadTapped: (() -> Unit)? = null
|
||||
var onNotificationTapped: (() -> Unit)? = null
|
||||
var onSetMuteTapped: ((Boolean) -> Unit)? = null
|
||||
|
||||
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?) {
|
||||
when (v) {
|
||||
detailsTextView -> onViewDetailsTapped?.invoke()
|
||||
pinTextView -> onPinTapped?.invoke()
|
||||
unpinTextView -> onUnpinTapped?.invoke()
|
||||
blockTextView -> onBlockTapped?.invoke()
|
||||
unblockTextView -> onUnblockTapped?.invoke()
|
||||
deleteTextView -> onDeleteTapped?.invoke()
|
||||
notificationsTextView -> onNotificationTapped?.invoke()
|
||||
unMuteNotificationsTextView -> onSetMuteTapped?.invoke(false)
|
||||
muteNotificationsTextView -> onSetMuteTapped?.invoke(true)
|
||||
binding.detailsTextView -> onViewDetailsTapped?.invoke()
|
||||
binding.pinTextView -> onPinTapped?.invoke()
|
||||
binding.unpinTextView -> onUnpinTapped?.invoke()
|
||||
binding.blockTextView -> onBlockTapped?.invoke()
|
||||
binding.unblockTextView -> onUnblockTapped?.invoke()
|
||||
binding.deleteTextView -> onDeleteTapped?.invoke()
|
||||
binding.markAllAsReadTextView -> onMarkAllAsReadTapped?.invoke()
|
||||
binding.notificationsTextView -> onNotificationTapped?.invoke()
|
||||
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() }
|
||||
val recipient = thread.recipient
|
||||
if (!recipient.isGroupRecipient && !recipient.isLocalNumber) {
|
||||
detailsTextView.visibility = View.VISIBLE
|
||||
unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE
|
||||
blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE
|
||||
detailsTextView.setOnClickListener(this)
|
||||
blockTextView.setOnClickListener(this)
|
||||
unblockTextView.setOnClickListener(this)
|
||||
binding.detailsTextView.visibility = View.VISIBLE
|
||||
binding.unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE
|
||||
binding.blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE
|
||||
binding.detailsTextView.setOnClickListener(this)
|
||||
binding.blockTextView.setOnClickListener(this)
|
||||
binding.unblockTextView.setOnClickListener(this)
|
||||
} else {
|
||||
detailsTextView.visibility = View.GONE
|
||||
binding.detailsTextView.visibility = View.GONE
|
||||
}
|
||||
unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
||||
muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
||||
unMuteNotificationsTextView.setOnClickListener(this)
|
||||
muteNotificationsTextView.setOnClickListener(this)
|
||||
notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
|
||||
notificationsTextView.setOnClickListener(this)
|
||||
deleteTextView.setOnClickListener(this)
|
||||
pinTextView.isVisible = !thread.isPinned
|
||||
unpinTextView.isVisible = thread.isPinned
|
||||
pinTextView.setOnClickListener(this)
|
||||
unpinTextView.setOnClickListener(this)
|
||||
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
||||
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
||||
binding.unMuteNotificationsTextView.setOnClickListener(this)
|
||||
binding.muteNotificationsTextView.setOnClickListener(this)
|
||||
binding.notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
|
||||
binding.notificationsTextView.setOnClickListener(this)
|
||||
binding.deleteTextView.setOnClickListener(this)
|
||||
binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0
|
||||
binding.markAllAsReadTextView.setOnClickListener(this)
|
||||
binding.pinTextView.isVisible = !thread.isPinned
|
||||
binding.unpinTextView.isVisible = thread.isPinned
|
||||
binding.pinTextView.setOnClickListener(this)
|
||||
binding.unpinTextView.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -11,8 +11,8 @@ import android.widget.LinearLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.view_conversation.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewConversationBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
|
||||
class ConversationView : LinearLayout {
|
||||
private lateinit var binding: ViewConversationBinding
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
var thread: ThreadRecord? = null
|
||||
|
||||
@ -31,7 +32,7 @@ class ConversationView : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 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)
|
||||
}
|
||||
// endregion
|
||||
@ -39,84 +40,84 @@ class ConversationView : LinearLayout {
|
||||
// region Updating
|
||||
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
|
||||
this.thread = thread
|
||||
if (thread.isPinned) {
|
||||
conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0)
|
||||
background = ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background)
|
||||
background = if (thread.isPinned) {
|
||||
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0)
|
||||
ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background)
|
||||
} else {
|
||||
conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
background = ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
||||
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
||||
}
|
||||
profilePictureView.glide = glide
|
||||
binding.profilePictureView.glide = glide
|
||||
val unreadCount = thread.unreadCount
|
||||
if (thread.recipient.isBlocked) {
|
||||
accentView.setBackgroundResource(R.color.destructive)
|
||||
accentView.visibility = View.VISIBLE
|
||||
binding.accentView.setBackgroundResource(R.color.destructive)
|
||||
binding.accentView.visibility = View.VISIBLE
|
||||
} 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
|
||||
// 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) {
|
||||
null
|
||||
} else {
|
||||
if (unreadCount < 100) unreadCount.toString() else "99+"
|
||||
if (unreadCount < 10000) unreadCount.toString() else "9999+"
|
||||
}
|
||||
unreadCountTextView.text = formattedUnreadCount
|
||||
val textSize = if (unreadCount < 100) 12.0f else 9.0f
|
||||
unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
||||
unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL)
|
||||
unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead)
|
||||
binding.unreadCountTextView.text = formattedUnreadCount
|
||||
val textSize = if (unreadCount < 10000) 12.0f else 9.0f
|
||||
binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
||||
binding.unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL)
|
||||
binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead)
|
||||
val senderDisplayName = getUserDisplayName(thread.recipient)
|
||||
?: thread.recipient.address.toString()
|
||||
conversationViewDisplayNameTextView.text = senderDisplayName
|
||||
timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||
binding.conversationViewDisplayNameTextView.text = senderDisplayName
|
||||
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||
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) {
|
||||
R.drawable.ic_outline_notifications_off_24
|
||||
} else {
|
||||
R.drawable.ic_notifications_mentions
|
||||
}
|
||||
muteIndicatorImageView.setImageResource(drawableRes)
|
||||
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
||||
val rawSnippet = thread.getDisplayBody(context)
|
||||
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
|
||||
snippetTextView.text = snippet
|
||||
snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||
binding.snippetTextView.text = snippet
|
||||
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||
if (isTyping) {
|
||||
typingIndicatorView.startAnimation()
|
||||
binding.typingIndicatorView.startAnimation()
|
||||
} else {
|
||||
typingIndicatorView.stopAnimation()
|
||||
binding.typingIndicatorView.stopAnimation()
|
||||
}
|
||||
typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE
|
||||
statusIndicatorImageView.visibility = View.VISIBLE
|
||||
binding.typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE
|
||||
binding.statusIndicatorImageView.visibility = View.VISIBLE
|
||||
when {
|
||||
!thread.isOutgoing -> statusIndicatorImageView.visibility = View.GONE
|
||||
!thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE
|
||||
thread.isFailed -> {
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_error)?.mutate()
|
||||
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.isRead -> statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||
else -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
||||
thread.isPending -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot)
|
||||
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
||||
}
|
||||
post {
|
||||
profilePictureView.update(thread.recipient, thread.threadId)
|
||||
binding.profilePictureView.update(thread.recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
profilePictureView.recycle()
|
||||
binding.profilePictureView.recycle()
|
||||
}
|
||||
|
||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
||||
if (recipient.isLocalNumber) {
|
||||
return context.getString(R.string.note_to_self)
|
||||
return if (recipient.isLocalNumber) {
|
||||
context.getString(R.string.note_to_self)
|
||||
} else {
|
||||
return recipient.name // Internally uses the Contact API
|
||||
recipient.name // Internally uses the Contact API
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,9 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.database.Cursor
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
@ -19,23 +17,26 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.flow.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityHomeBinding
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.utilities.*
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsession.utilities.Address
|
||||
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.toHexString
|
||||
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.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
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.JoinPublicChatActivity
|
||||
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.GlideRequests
|
||||
import org.thoughtcrime.securesms.onboarding.SeedActivity
|
||||
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
|
||||
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 javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener,
|
||||
SeedReminderViewDelegate, NewConversationButtonSetViewDelegate, LoaderManager.LoaderCallbacks<Cursor> {
|
||||
class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
ConversationClickListener,
|
||||
SeedReminderViewDelegate,
|
||||
NewConversationButtonSetViewDelegate,
|
||||
LoaderManager.LoaderCallbacks<Cursor>,
|
||||
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
||||
|
||||
private lateinit var binding: ActivityHomeBinding
|
||||
private lateinit var glide: GlideRequests
|
||||
private var broadcastReceiver: BroadcastReceiver? = null
|
||||
|
||||
@Inject lateinit var threadDb: ThreadDatabase
|
||||
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
||||
@Inject lateinit var recipientDatabase: RecipientDatabase
|
||||
@Inject lateinit var groupDatabase: GroupDatabase
|
||||
@Inject lateinit var textSecurePreferences: TextSecurePreferences
|
||||
|
||||
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
|
||||
|
||||
private val publicKey: String
|
||||
get() = TextSecurePreferences.getLocalNumber(this)!!
|
||||
|
||||
private val homeAdapter:HomeAdapter by lazy {
|
||||
HomeAdapter(this, threadDb.conversationList)
|
||||
private val homeAdapter: HomeAdapter by lazy {
|
||||
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
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_home)
|
||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
// Set custom toolbar
|
||||
setSupportActionBar(toolbar)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
// Set up Glide
|
||||
glide = GlideApp.with(this)
|
||||
// Set up toolbar buttons
|
||||
profileButton.glide = glide
|
||||
profileButton.setOnClickListener { openSettings() }
|
||||
pathStatusViewContainer.disableClipping()
|
||||
pathStatusViewContainer.setOnClickListener { showPath() }
|
||||
binding.profileButton.glide = glide
|
||||
binding.profileButton.setOnClickListener { openSettings() }
|
||||
binding.searchViewContainer.setOnClickListener {
|
||||
binding.globalSearchInputLayout.requestFocus()
|
||||
}
|
||||
binding.sessionToolbar.disableClipping()
|
||||
// Set up seed reminder view
|
||||
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
||||
if (!hasViewedSeed) {
|
||||
seedReminderStub.inflate().apply {
|
||||
val seedReminderView = this.seedReminderView
|
||||
val seedReminderViewTitle = SpannableString("You're almost finished! 80%") // Intentionally not yet translated
|
||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
|
||||
seedReminderView.setProgress(80, false)
|
||||
seedReminderView.delegate = this@HomeActivity
|
||||
}
|
||||
binding.seedReminderView.isVisible = true
|
||||
binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated
|
||||
binding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
|
||||
binding.seedReminderView.setProgress(80, false)
|
||||
binding.seedReminderView.delegate = this@HomeActivity
|
||||
} else {
|
||||
seedReminderStub.isVisible = false
|
||||
binding.seedReminderView.isVisible = false
|
||||
}
|
||||
setupHeaderImage()
|
||||
// Set up recycler view
|
||||
binding.globalSearchInputLayout.listener = this
|
||||
homeAdapter.setHasStableIds(true)
|
||||
homeAdapter.glide = glide
|
||||
homeAdapter.conversationClickListener = this
|
||||
recyclerView.adapter = homeAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = homeAdapter
|
||||
binding.globalSearchRecycler.adapter = globalSearchAdapter
|
||||
// Set up empty state view
|
||||
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||
// 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)
|
||||
// Set up new conversation button set
|
||||
newConversationButtonSet.delegate = this
|
||||
binding.newConversationButtonSet.delegate = this
|
||||
// Observe blocked contacts changed events
|
||||
val broadcastReceiver = object : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
this.broadcastReceiver = broadcastReceiver
|
||||
@ -138,7 +197,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
// Set up typing observer
|
||||
withContext(Dispatchers.Main) {
|
||||
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()
|
||||
})
|
||||
updateProfileButton()
|
||||
@ -155,10 +214,85 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
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)
|
||||
}
|
||||
|
||||
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> {
|
||||
return HomeLoader(this@HomeActivity)
|
||||
}
|
||||
@ -177,11 +311,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
|
||||
if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared
|
||||
IdentityKeyUtil.checkUpdate(this)
|
||||
profileButton.recycle() // clear cached image before update tje profilePictureView
|
||||
profileButton.update()
|
||||
binding.profileButton.recycle() // clear cached image before update tje profilePictureView
|
||||
binding.profileButton.update()
|
||||
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
||||
if (hasViewedSeed) {
|
||||
seedReminderView?.isVisible = false
|
||||
binding.seedReminderView.isVisible = false
|
||||
}
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(this)) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
@ -214,8 +348,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
|
||||
// region Updating
|
||||
private fun updateEmptyState() {
|
||||
val threadCount = (recyclerView.adapter as HomeAdapter).itemCount
|
||||
emptyStateContainer.visibility = if (threadCount == 0) View.VISIBLE else View.GONE
|
||||
val threadCount = (binding.recyclerView.adapter as HomeAdapter).itemCount
|
||||
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@ -226,26 +360,34 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
}
|
||||
|
||||
private fun updateProfileButton() {
|
||||
profileButton.publicKey = publicKey
|
||||
profileButton.displayName = TextSecurePreferences.getProfileName(this)
|
||||
profileButton.recycle()
|
||||
profileButton.update()
|
||||
binding.profileButton.publicKey = publicKey
|
||||
binding.profileButton.displayName = TextSecurePreferences.getProfileName(this)
|
||||
binding.profileButton.recycle()
|
||||
binding.profileButton.update()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun onBackPressed() {
|
||||
if (binding.globalSearchRecycler.isVisible) {
|
||||
binding.globalSearchInputLayout.clearSearch(true)
|
||||
return
|
||||
}
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun handleSeedReminderViewContinueButtonTapped() {
|
||||
val intent = Intent(this, SeedActivity::class.java)
|
||||
show(intent)
|
||||
}
|
||||
|
||||
override fun onConversationClick(view: ConversationView) {
|
||||
val thread = view.thread ?: return
|
||||
openConversation(thread)
|
||||
override fun onConversationClick(thread: ThreadRecord) {
|
||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
override fun onLongConversationClick(view: ConversationView) {
|
||||
val thread = view.thread ?: return
|
||||
override fun onLongConversationClick(thread: ThreadRecord) {
|
||||
val bottomSheet = ConversationOptionsBottomSheet()
|
||||
bottomSheet.thread = thread
|
||||
bottomSheet.onViewDetailsTapped = {
|
||||
@ -286,15 +428,15 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
}
|
||||
bottomSheet.onPinTapped = {
|
||||
bottomSheet.dismiss()
|
||||
if (!thread.isPinned) {
|
||||
pinConversation(thread)
|
||||
}
|
||||
setConversationPinned(thread.threadId, true)
|
||||
}
|
||||
bottomSheet.onUnpinTapped = {
|
||||
bottomSheet.dismiss()
|
||||
if (thread.isPinned) {
|
||||
unpinConversation(thread)
|
||||
}
|
||||
setConversationPinned(thread.threadId, false)
|
||||
}
|
||||
bottomSheet.onMarkAllAsReadTapped = {
|
||||
bottomSheet.dismiss()
|
||||
markAllAsRead(thread)
|
||||
}
|
||||
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)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setBlocked(thread.recipient, true)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
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)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setBlocked(thread.recipient, false)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
@ -333,18 +475,18 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
|
||||
private fun setConversationMuted(thread: ThreadRecord, isMuted: Boolean) {
|
||||
if (!isMuted) {
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setMuted(thread.recipient, 0)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MuteDialog.show(this) { until: Long ->
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setMuted(thread.recipient, until)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -352,45 +494,41 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
}
|
||||
|
||||
private fun setNotifyType(thread: ThreadRecord, newNotifyType: Int) {
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setNotifyType(thread.recipient, newNotifyType)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pinConversation(thread: ThreadRecord) {
|
||||
ThreadUtils.queue {
|
||||
threadDb.setPinned(thread.threadId, true)
|
||||
Util.runOnMain {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
threadDb.setPinned(threadId, pinned)
|
||||
withContext(Dispatchers.Main) {
|
||||
LoaderManager.getInstance(this@HomeActivity).restartLoader(0, null, this@HomeActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun unpinConversation(thread: ThreadRecord) {
|
||||
private fun markAllAsRead(thread: ThreadRecord) {
|
||||
ThreadUtils.queue {
|
||||
threadDb.setPinned(thread.threadId, false)
|
||||
Util.runOnMain {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
}
|
||||
threadDb.markAllAsRead(thread.threadId, thread.recipient.isOpenGroupRecipient)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteConversation(thread: ThreadRecord) {
|
||||
val threadID = thread.threadId
|
||||
val recipient = thread.recipient
|
||||
val message: String
|
||||
if (recipient.isGroupRecipient) {
|
||||
val message = if (recipient.isGroupRecipient) {
|
||||
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
|
||||
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 {
|
||||
message = resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||
resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||
}
|
||||
} 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)
|
||||
dialog.setMessage(message)
|
||||
@ -419,7 +557,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
if (v2OpenGroup != null) {
|
||||
OpenGroupManager.delete(v2OpenGroup.server, v2OpenGroup.room, this@HomeActivity)
|
||||
} else {
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
threadDb.deleteConversation(threadID)
|
||||
}
|
||||
}
|
||||
@ -436,12 +574,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
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() {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
show(intent, isForResult = true)
|
||||
|
@ -9,20 +9,23 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.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()
|
||||
lateinit var glide: GlideRequests
|
||||
var typingThreadIDs = setOf<Long>()
|
||||
set(value) { field = value; notifyDataSetChanged() }
|
||||
var conversationClickListener: ConversationClickListener? = null
|
||||
|
||||
class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = ConversationView(context)
|
||||
view.setOnClickListener { conversationClickListener?.onConversationClick(view) }
|
||||
view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } }
|
||||
view.setOnLongClickListener {
|
||||
conversationClickListener?.onLongConversationClick(view)
|
||||
view.thread?.let { listener.onLongConversationClick(it) }
|
||||
true
|
||||
}
|
||||
return ViewHolder(view)
|
||||
@ -45,6 +48,6 @@ class HomeAdapter(context: Context, cursor: Cursor?) : CursorRecyclerViewAdapter
|
||||
}
|
||||
|
||||
interface ConversationClickListener {
|
||||
fun onConversationClick(view: ConversationView)
|
||||
fun onLongConversationClick(view: ConversationView)
|
||||
fun onConversationClick(thread: ThreadRecord)
|
||||
fun onLongConversationClick(thread: ThreadRecord)
|
||||
}
|
@ -17,26 +17,33 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.android.synthetic.main.activity_path.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityPathBinding
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.utilities.Snode
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.util.*
|
||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.util.IP2Country
|
||||
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() {
|
||||
private lateinit var binding: ActivityPathBinding
|
||||
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
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)
|
||||
pathRowsContainer.disableClipping()
|
||||
learnMoreButton.setOnClickListener { learnMore() }
|
||||
binding.pathRowsContainer.disableClipping()
|
||||
binding.learnMoreButton.setOnClickListener { learnMore() }
|
||||
update(false)
|
||||
registerObservers()
|
||||
}
|
||||
@ -82,7 +89,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
private fun handleOnionRequestPathCountriesLoaded() { update(false) }
|
||||
|
||||
private fun update(isAnimated: Boolean) {
|
||||
pathRowsContainer.removeAllViews()
|
||||
binding.pathRowsContainer.removeAllViews()
|
||||
if (OnionRequestAPI.paths.isNotEmpty()) {
|
||||
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
|
||||
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 rows = listOf( youRow ) + pathRows + listOf( destinationRow )
|
||||
for (row in rows) {
|
||||
pathRowsContainer.addView(row)
|
||||
binding.pathRowsContainer.addView(row)
|
||||
}
|
||||
if (isAnimated) {
|
||||
spinner.fadeOut()
|
||||
binding.spinner.fadeOut()
|
||||
} else {
|
||||
spinner.alpha = 0.0f
|
||||
binding.spinner.alpha = 0.0f
|
||||
}
|
||||
} else {
|
||||
if (isAnimated) {
|
||||
spinner.fadeIn()
|
||||
binding.spinner.fadeIn()
|
||||
} else {
|
||||
spinner.alpha = 1.0f
|
||||
binding.spinner.alpha = 1.0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,10 +14,9 @@ import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.fragment_user_details_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentUserDetailsBottomSheetBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.Address
|
||||
@ -34,13 +33,15 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
@Inject lateinit var threadDb: ThreadDatabase
|
||||
|
||||
private lateinit var binding: FragmentUserDetailsBottomSheetBinding
|
||||
companion object {
|
||||
const val ARGUMENT_PUBLIC_KEY = "publicKey"
|
||||
const val ARGUMENT_THREAD_ID = "threadId"
|
||||
}
|
||||
|
||||
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?) {
|
||||
@ -49,58 +50,62 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
||||
val threadID = arguments?.getLong(ARGUMENT_THREAD_ID) ?: return dismiss()
|
||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
|
||||
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
||||
profilePictureView.publicKey = publicKey
|
||||
profilePictureView.glide = GlideApp.with(this)
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update(recipient, -1)
|
||||
nameTextViewContainer.visibility = View.VISIBLE
|
||||
nameTextViewContainer.setOnClickListener {
|
||||
nameTextViewContainer.visibility = View.INVISIBLE
|
||||
nameEditTextContainer.visibility = View.VISIBLE
|
||||
nicknameEditText.text = null
|
||||
nicknameEditText.requestFocus()
|
||||
showSoftKeyboard()
|
||||
}
|
||||
cancelNicknameEditingButton.setOnClickListener {
|
||||
nicknameEditText.clearFocus()
|
||||
hideSoftKeyboard()
|
||||
with(binding) {
|
||||
profilePictureView.publicKey = publicKey
|
||||
profilePictureView.glide = GlideApp.with(this@UserDetailsBottomSheet)
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update(recipient)
|
||||
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
|
||||
nameTextViewContainer.setOnClickListener {
|
||||
nameTextViewContainer.visibility = View.INVISIBLE
|
||||
nameEditTextContainer.visibility = View.VISIBLE
|
||||
nicknameEditText.text = null
|
||||
nicknameEditText.requestFocus()
|
||||
showSoftKeyboard()
|
||||
}
|
||||
}
|
||||
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
|
||||
cancelNicknameEditingButton.setOnClickListener {
|
||||
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
|
||||
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient
|
||||
publicKeyTextView.text = publicKey
|
||||
publicKeyTextView.setOnLongClickListener {
|
||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", publicKey)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
messageButton.setOnClickListener {
|
||||
val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient)
|
||||
val intent = Intent(
|
||||
context,
|
||||
ConversationActivityV2::class.java
|
||||
)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1)
|
||||
startActivity(intent)
|
||||
dismiss()
|
||||
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
|
||||
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient
|
||||
publicKeyTextView.text = publicKey
|
||||
publicKeyTextView.setOnLongClickListener {
|
||||
val clipboard =
|
||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", publicKey)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
true
|
||||
}
|
||||
messageButton.setOnClickListener {
|
||||
val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient)
|
||||
val intent = Intent(
|
||||
context,
|
||||
ConversationActivityV2::class.java
|
||||
)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
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)
|
||||
}
|
||||
|
||||
fun saveNickName(recipient: Recipient) {
|
||||
fun saveNickName(recipient: Recipient) = with(binding) {
|
||||
nicknameEditText.clearFocus()
|
||||
hideSoftKeyboard()
|
||||
nameTextViewContainer.visibility = View.VISIBLE
|
||||
@ -131,11 +136,11 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
||||
@SuppressLint("ServiceCast")
|
||||
fun showSoftKeyboard() {
|
||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(nicknameEditText, 0)
|
||||
imm?.showSoftInput(binding.nicknameEditText, 0)
|
||||
}
|
||||
|
||||
fun hideSoftKeyboard() {
|
||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.hideSoftInputFromWindow(nicknameEditText.windowToken, 0)
|
||||
imm?.hideSoftInputFromWindow(binding.nicknameEditText.windowToken, 0)
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package org.thoughtcrime.securesms.home.search
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
|
||||
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||
import java.security.InvalidParameterException
|
||||
import org.session.libsession.messaging.contacts.Contact as ContactModel
|
||||
|
||||
class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
companion object {
|
||||
const val HEADER_VIEW_TYPE = 0
|
||||
const val CONTENT_VIEW_TYPE = 1
|
||||
}
|
||||
|
||||
private var data: List<Model> = listOf()
|
||||
private var query: String? = null
|
||||
|
||||
fun setNewData(query: String, newData: List<Model>) {
|
||||
val diffResult = DiffUtil.calculateDiff(GlobalSearchDiff(this.query, query, data, newData))
|
||||
this.query = query
|
||||
data = newData
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int =
|
||||
if (data[position] is Model.Header) HEADER_VIEW_TYPE else CONTENT_VIEW_TYPE
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
||||
if (viewType == HEADER_VIEW_TYPE) {
|
||||
HeaderView(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.view_global_search_header, parent, false)
|
||||
)
|
||||
} else {
|
||||
ContentView(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.view_global_search_result, parent, false)
|
||||
, modelCallback)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: RecyclerView.ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
val newUpdateQuery: String? = payloads.firstOrNull { it is String } as String?
|
||||
if (newUpdateQuery != null && holder is ContentView) {
|
||||
holder.bindPayload(newUpdateQuery, data[position])
|
||||
return
|
||||
}
|
||||
if (holder is HeaderView) {
|
||||
holder.bind(data[position] as Model.Header)
|
||||
} else if (holder is ContentView) {
|
||||
holder.bind(query.orEmpty(), data[position])
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
onBindViewHolder(holder,position, mutableListOf())
|
||||
}
|
||||
|
||||
class HeaderView(view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
val binding = ViewGlobalSearchHeaderBinding.bind(view)
|
||||
|
||||
fun bind(header: Model.Header) {
|
||||
binding.searchHeader.setText(header.title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is ContentView) {
|
||||
holder.binding.searchResultProfilePicture.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
val binding = ViewGlobalSearchResultBinding.bind(view).apply {
|
||||
searchResultProfilePicture.glide = GlideApp.with(root)
|
||||
}
|
||||
|
||||
fun bindPayload(newQuery: String, model: Model) {
|
||||
bindQuery(newQuery, model)
|
||||
}
|
||||
|
||||
fun bind(query: String, model: Model) {
|
||||
binding.searchResultProfilePicture.recycle()
|
||||
when (model) {
|
||||
is Model.GroupConversation -> bindModel(query, model)
|
||||
is Model.Contact -> bindModel(query, model)
|
||||
is Model.Message -> bindModel(query, model)
|
||||
is Model.SavedMessages -> bindModel(model)
|
||||
is Model.Header -> throw InvalidParameterException("Can't display Model.Header as ContentView")
|
||||
}
|
||||
binding.root.setOnClickListener { modelCallback(model) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class MessageModel(
|
||||
val threadRecipient: Recipient,
|
||||
val messageRecipient: Recipient,
|
||||
val messageSnippet: String
|
||||
)
|
||||
|
||||
sealed class Model {
|
||||
data class Header(@StringRes val title: Int) : Model()
|
||||
data class SavedMessages(val currentUserPublicKey: String): Model()
|
||||
data class Contact(val contact: ContactModel) : Model()
|
||||
data class GroupConversation(val groupRecord: GroupRecord) : Model()
|
||||
data class Message(val messageResult: MessageResult, val unread: Int) : Model()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
package org.thoughtcrime.securesms.home.search
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.TypedValue
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.SearchUtil
|
||||
import java.util.Locale
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel
|
||||
|
||||
|
||||
class GlobalSearchDiff(
|
||||
private val oldQuery: String?,
|
||||
private val newQuery: String?,
|
||||
private val oldData: List<GlobalSearchAdapter.Model>,
|
||||
private val newData: List<GlobalSearchAdapter.Model>
|
||||
) : DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int = oldData.size
|
||||
override fun getNewListSize(): Int = newData.size
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
oldData[oldItemPosition] == newData[newItemPosition]
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
oldQuery == newQuery && oldData[oldItemPosition] == newData[newItemPosition]
|
||||
|
||||
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? =
|
||||
if (oldQuery != newQuery) newQuery
|
||||
else null
|
||||
}
|
||||
|
||||
private val BoldStyleFactory = { StyleSpan(Typeface.BOLD) }
|
||||
|
||||
fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) {
|
||||
when (model) {
|
||||
is ContactModel -> {
|
||||
binding.searchResultTitle.text = getHighlight(
|
||||
query,
|
||||
model.contact.getSearchName()
|
||||
)
|
||||
}
|
||||
is Message -> {
|
||||
val textSpannable = SpannableStringBuilder()
|
||||
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
||||
// group chat, bind
|
||||
val text = "${model.messageResult.messageRecipient.getSearchName()}: "
|
||||
textSpannable.append(text)
|
||||
}
|
||||
textSpannable.append(getHighlight(
|
||||
query,
|
||||
model.messageResult.bodySnippet
|
||||
))
|
||||
binding.searchResultSubtitle.text = textSpannable
|
||||
binding.searchResultSubtitle.isVisible = true
|
||||
binding.searchResultTitle.text = model.messageResult.conversationRecipient.toShortString()
|
||||
}
|
||||
is GroupConversation -> {
|
||||
binding.searchResultTitle.text = getHighlight(
|
||||
query,
|
||||
model.groupRecord.title
|
||||
)
|
||||
|
||||
val membersString = model.groupRecord.members.joinToString { address ->
|
||||
val recipient = Recipient.from(binding.root.context, address, false)
|
||||
recipient.name ?: "${address.serialize().take(4)}...${address.serialize().takeLast(4)}"
|
||||
}
|
||||
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHighlight(query: String?, toSearch: String): Spannable? {
|
||||
return SearchUtil.getHighlightedSpan(Locale.getDefault(), BoldStyleFactory, toSearch, query)
|
||||
}
|
||||
|
||||
fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
||||
binding.searchResultProfilePicture.isVisible = true
|
||||
binding.searchResultSavedMessages.isVisible = false
|
||||
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
|
||||
binding.searchResultTimestamp.isVisible = false
|
||||
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false)
|
||||
binding.searchResultProfilePicture.update(threadRecipient)
|
||||
val nameString = model.groupRecord.title
|
||||
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||
|
||||
val groupRecipients = model.groupRecord.members.map { Recipient.from(binding.root.context, it, false) }
|
||||
|
||||
val membersString = groupRecipients.joinToString {
|
||||
val address = it.address.serialize()
|
||||
it.name ?: "${address.take(4)}...${address.takeLast(4)}"
|
||||
}
|
||||
if (model.groupRecord.isClosedGroup) {
|
||||
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
||||
}
|
||||
}
|
||||
|
||||
fun ContentView.bindModel(query: String?, model: ContactModel) {
|
||||
binding.searchResultProfilePicture.isVisible = true
|
||||
binding.searchResultSavedMessages.isVisible = false
|
||||
binding.searchResultSubtitle.isVisible = false
|
||||
binding.searchResultTimestamp.isVisible = false
|
||||
binding.searchResultSubtitle.text = null
|
||||
val recipient =
|
||||
Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false)
|
||||
binding.searchResultProfilePicture.update(recipient)
|
||||
val nameString = model.contact.getSearchName()
|
||||
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||
}
|
||||
|
||||
fun ContentView.bindModel(model: SavedMessages) {
|
||||
binding.searchResultSubtitle.isVisible = false
|
||||
binding.searchResultTimestamp.isVisible = false
|
||||
binding.searchResultTitle.setText(R.string.note_to_self)
|
||||
binding.searchResultProfilePicture.isVisible = false
|
||||
binding.searchResultSavedMessages.isVisible = true
|
||||
}
|
||||
|
||||
fun ContentView.bindModel(query: String?, model: Message) {
|
||||
binding.searchResultProfilePicture.isVisible = true
|
||||
binding.searchResultSavedMessages.isVisible = false
|
||||
binding.searchResultTimestamp.isVisible = true
|
||||
// val hasUnreads = model.unread > 0
|
||||
// binding.unreadCountIndicator.isVisible = hasUnreads
|
||||
// if (hasUnreads) {
|
||||
// binding.unreadCountTextView.text = model.unread.toString()
|
||||
// }
|
||||
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.receivedTimestampMs)
|
||||
binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient)
|
||||
val textSpannable = SpannableStringBuilder()
|
||||
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
||||
// group chat, bind
|
||||
val text = "${model.messageResult.messageRecipient.getSearchName()}: "
|
||||
textSpannable.append(text)
|
||||
}
|
||||
textSpannable.append(getHighlight(
|
||||
query,
|
||||
model.messageResult.bodySnippet
|
||||
))
|
||||
binding.searchResultSubtitle.text = textSpannable
|
||||
binding.searchResultTitle.text = model.messageResult.conversationRecipient.toShortString()
|
||||
binding.searchResultSubtitle.isVisible = true
|
||||
}
|
||||
|
||||
fun Recipient.getSearchName(): String = name ?: address.serialize().let { address -> "${address.take(4)}...${address.takeLast(4)}" }
|
||||
|
||||
fun Contact.getSearchName(): String =
|
||||
if (nickname.isNullOrEmpty()) name ?: "${sessionID.take(4)}...${sessionID.takeLast(4)}"
|
||||
else "${name ?: "${sessionID.take(4)}...${sessionID.takeLast(4)}"} ($nickname)"
|
@ -0,0 +1,88 @@
|
||||
package org.thoughtcrime.securesms.home.search
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import network.loki.messenger.databinding.ViewGlobalSearchInputBinding
|
||||
|
||||
class GlobalSearchInputLayout @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs),
|
||||
View.OnFocusChangeListener,
|
||||
View.OnClickListener,
|
||||
TextWatcher, TextView.OnEditorActionListener {
|
||||
|
||||
var binding: ViewGlobalSearchInputBinding = ViewGlobalSearchInputBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
var listener: GlobalSearchInputLayoutListener? = null
|
||||
|
||||
private val _query = MutableStateFlow<CharSequence?>(null)
|
||||
val query: StateFlow<CharSequence?> = _query
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
binding.searchInput.onFocusChangeListener = this
|
||||
binding.searchInput.addTextChangedListener(this)
|
||||
binding.searchInput.setOnEditorActionListener(this)
|
||||
binding.searchCancel.setOnClickListener(this)
|
||||
binding.searchClear.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onFocusChange(v: View?, hasFocus: Boolean) {
|
||||
if (v === binding.searchInput) {
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(windowToken, 0)
|
||||
listener?.onInputFocusChanged(hasFocus)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||
if (v === binding.searchInput && actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
binding.searchInput.clearFocus()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
if (v === binding.searchCancel) {
|
||||
clearSearch(true)
|
||||
} else if (v === binding.searchClear) {
|
||||
clearSearch(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSearch(clearFocus: Boolean) {
|
||||
binding.searchInput.text = null
|
||||
if (clearFocus) {
|
||||
binding.searchInput.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
_query.value = s?.toString()
|
||||
}
|
||||
|
||||
interface GlobalSearchInputLayoutListener {
|
||||
fun onInputFocusChanged(hasFocus: Boolean)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package org.thoughtcrime.securesms.home.search
|
||||
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||
import org.thoughtcrime.securesms.search.model.SearchResult
|
||||
|
||||
data class GlobalSearchResult(
|
||||
val query: String,
|
||||
val contacts: List<Contact>,
|
||||
val threads: List<GroupRecord>,
|
||||
val messages: List<MessageResult>
|
||||
) {
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = contacts.isEmpty() && threads.isEmpty() && messages.isEmpty()
|
||||
|
||||
companion object {
|
||||
|
||||
val EMPTY = GlobalSearchResult("", emptyList(), emptyList(), emptyList())
|
||||
const val SEARCH_LIMIT = 5
|
||||
|
||||
fun from(searchResult: SearchResult): GlobalSearchResult {
|
||||
val query = searchResult.query
|
||||
val contactList = searchResult.contacts.toList()
|
||||
val threads = searchResult.conversations.toList()
|
||||
val messages = searchResult.messages.toList()
|
||||
searchResult.close()
|
||||
return GlobalSearchResult(query, contactList, threads, messages)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package org.thoughtcrime.securesms.home.search
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.plus
|
||||
import org.session.libsignal.utilities.SettableFuture
|
||||
import org.thoughtcrime.securesms.search.SearchRepository
|
||||
import org.thoughtcrime.securesms.search.model.SearchResult
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class GlobalSearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() {
|
||||
|
||||
private val executor = viewModelScope + SupervisorJob()
|
||||
|
||||
private val _result: MutableStateFlow<GlobalSearchResult> =
|
||||
MutableStateFlow(GlobalSearchResult.EMPTY)
|
||||
|
||||
val result: StateFlow<GlobalSearchResult> = _result
|
||||
|
||||
private val _queryText: MutableStateFlow<CharSequence> = MutableStateFlow("")
|
||||
|
||||
fun postQuery(charSequence: CharSequence?) {
|
||||
charSequence ?: return
|
||||
_queryText.value = charSequence
|
||||
}
|
||||
|
||||
init {
|
||||
//
|
||||
_queryText
|
||||
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.mapLatest { query ->
|
||||
if (query.trim().length < 2) {
|
||||
SearchResult.EMPTY
|
||||
} else {
|
||||
// user input delay here in case we get a new query within a few hundred ms
|
||||
// this coroutine will be cancelled and expensive query will not be run if typing quickly
|
||||
// first query of 2 characters will be instant however
|
||||
delay(300)
|
||||
val settableFuture = SettableFuture<SearchResult>()
|
||||
searchRepository.query(query.toString(), settableFuture::set)
|
||||
try {
|
||||
// search repository doesn't play nicely with suspend functions (yet)
|
||||
settableFuture.get(10_000, TimeUnit.MILLISECONDS)
|
||||
} catch (e: Exception) {
|
||||
SearchResult.EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
.onEach { result ->
|
||||
// update the latest _result value
|
||||
_result.value = GlobalSearchResult.from(result)
|
||||
}
|
||||
.launchIn(executor)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
@ -80,7 +80,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
|
||||
controller = (Controller) getActivity();
|
||||
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
|
||||
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
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
@ -66,7 +66,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
||||
bucketId = getArguments().getString(KEY_BUCKET_ID);
|
||||
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
|
||||
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
|
||||
@ -105,7 +105,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
||||
onMediaSelectionChanged(new ArrayList<>(viewModel.getSelectedMedia().getValue()));
|
||||
}
|
||||
|
||||
viewModel.getMediaInBucket(requireContext(), bucketId).observe(this, adapter::setMedia);
|
||||
viewModel.getMediaInBucket(requireContext(), bucketId).observe(getViewLifecycleOwner(), adapter::setMedia);
|
||||
|
||||
initMediaObserver(viewModel);
|
||||
}
|
||||
@ -178,7 +178,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
||||
}
|
||||
|
||||
private void initMediaObserver(@NonNull MediaSendViewModel viewModel) {
|
||||
viewModel.getCountButtonState().observe(this, media -> {
|
||||
viewModel.getCountButtonState().observe(getViewLifecycleOwner(), media -> {
|
||||
requireActivity().invalidateOptionsMenu();
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Rect;
|
||||
@ -313,7 +313,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
||||
}
|
||||
|
||||
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 -> {
|
||||
if (Util.isEmpty(media)) {
|
||||
|
@ -60,7 +60,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
||||
val closedGroupPoller = ClosedGroupPollerV2() // Intentionally don't use shared
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
allGroupPublicKeys.forEach { closedGroupPoller.poll(it) }
|
||||
allGroupPublicKeys.iterator().forEach { closedGroupPoller.poll(it) }
|
||||
|
||||
// Open Groups
|
||||
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
||||
|
@ -57,7 +57,7 @@ object LokiPushNotificationManager {
|
||||
// Unsubscribe from all closed groups
|
||||
val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys()
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
allClosedGroupPublicKeys.forEach { closedGroup ->
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||
}
|
||||
}
|
||||
@ -87,7 +87,7 @@ object LokiPushNotificationManager {
|
||||
}
|
||||
// Subscribe to all closed groups
|
||||
val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys()
|
||||
allClosedGroupPublicKeys.forEach { closedGroup ->
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
||||
}
|
||||
}
|
||||
|
@ -70,12 +70,13 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
||||
|
||||
public static void process(@NonNull Context context, @NonNull List<MarkedMessageInfo> markedReadMessages) {
|
||||
if (markedReadMessages.isEmpty()) return;
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) return;
|
||||
|
||||
for (MarkedMessageInfo messageInfo : markedReadMessages) {
|
||||
scheduleDeletion(context, messageInfo.getExpirationInfo());
|
||||
}
|
||||
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) return;
|
||||
|
||||
Map<Address, List<SyncMessageId>> addressMap = Stream.of(markedReadMessages)
|
||||
.map(MarkedMessageInfo::getSyncMessageId)
|
||||
.collect(Collectors.groupingBy(SyncMessageId::getAddress));
|
||||
|
@ -7,8 +7,8 @@ import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_display_name.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityDisplayNameBinding
|
||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
@ -16,28 +16,32 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
||||
class DisplayNameActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityDisplayNameBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo()
|
||||
setContentView(R.layout.activity_display_name)
|
||||
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
displayNameEditText.setOnEditorActionListener(
|
||||
OnEditorActionListener { _, actionID, event ->
|
||||
if (actionID == EditorInfo.IME_ACTION_SEARCH ||
|
||||
actionID == EditorInfo.IME_ACTION_DONE ||
|
||||
(event.action == KeyEvent.ACTION_DOWN &&
|
||||
event.keyCode == KeyEvent.KEYCODE_ENTER)) {
|
||||
this.register()
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
registerButton.setOnClickListener { register() }
|
||||
binding = ActivityDisplayNameBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
with(binding) {
|
||||
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
displayNameEditText.setOnEditorActionListener(
|
||||
OnEditorActionListener { _, actionID, event ->
|
||||
if (actionID == EditorInfo.IME_ACTION_SEARCH ||
|
||||
actionID == EditorInfo.IME_ACTION_DONE ||
|
||||
(event.action == KeyEvent.ACTION_DOWN &&
|
||||
event.keyCode == KeyEvent.KEYCODE_ENTER)) {
|
||||
register()
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
registerButton.setOnClickListener { register() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun register() {
|
||||
val displayName = displayNameEditText.text.toString().trim()
|
||||
val displayName = binding.displayNameEditText.text.toString().trim()
|
||||
if (displayName.isEmpty()) {
|
||||
return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0)
|
||||
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
||||
TextSecurePreferences.setProfileName(this, displayName)
|
||||
val intent = Intent(this, PNModeActivity::class.java)
|
||||
push(intent)
|
||||
|
@ -3,19 +3,17 @@ package org.thoughtcrime.securesms.onboarding
|
||||
import android.animation.FloatEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.Context.LAYOUT_INFLATER_SERVICE
|
||||
import android.os.Handler
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import kotlinx.android.synthetic.main.view_fake_chat.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewFakeChatBinding
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
|
||||
class FakeChatView : ScrollView {
|
||||
|
||||
private lateinit var binding: ViewFakeChatBinding
|
||||
// region Settings
|
||||
private val spacing = context.resources.getDimension(R.dimen.medium_spacing)
|
||||
private val startDelay: Long = 1000
|
||||
@ -41,17 +39,15 @@ class FakeChatView : ScrollView {
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_fake_chat, null) as LinearLayout
|
||||
contentView.disableClipping()
|
||||
addView(contentView)
|
||||
binding = ViewFakeChatBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.root.disableClipping()
|
||||
isVerticalScrollBarEnabled = false
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Animation
|
||||
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) {
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||
animation.duration = animationDuration
|
||||
@ -61,18 +57,18 @@ class FakeChatView : ScrollView {
|
||||
animation.start()
|
||||
}
|
||||
Handler().postDelayed({
|
||||
show(bubble1)
|
||||
show(binding.bubble1)
|
||||
Handler().postDelayed({
|
||||
show(bubble2)
|
||||
show(binding.bubble2)
|
||||
Handler().postDelayed({
|
||||
show(bubble3)
|
||||
smoothScrollTo(0, (bubble1.height + spacing).toInt())
|
||||
show(binding.bubble3)
|
||||
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt())
|
||||
Handler().postDelayed({
|
||||
show(bubble4)
|
||||
smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt())
|
||||
show(binding.bubble4)
|
||||
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt())
|
||||
Handler().postDelayed({
|
||||
show(bubble5)
|
||||
smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt() + (bubble3.height + spacing).toInt())
|
||||
show(binding.bubble5)
|
||||
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt() + (binding.bubble3.height + spacing).toInt())
|
||||
}, delayBetweenMessages)
|
||||
}, delayBetweenMessages)
|
||||
}, delayBetweenMessages)
|
||||
|
@ -2,25 +2,27 @@ package org.thoughtcrime.securesms.onboarding
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityLandingBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
|
||||
class LandingActivity : BaseActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_landing)
|
||||
val binding = ActivityLandingBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setUpActionBarSessionLogo(true)
|
||||
findViewById<FakeChatView>(R.id.fakeChatView).startAnimating()
|
||||
findViewById<View>(R.id.registerButton).setOnClickListener { register() }
|
||||
findViewById<View>(R.id.restoreButton).setOnClickListener { restore() }
|
||||
findViewById<View>(R.id.linkButton).setOnClickListener { link() }
|
||||
with(binding) {
|
||||
fakeChatView.startAnimating()
|
||||
registerButton.setOnClickListener { register() }
|
||||
restoreButton.setOnClickListener { restore() }
|
||||
linkButton.setOnClickListener { link() }
|
||||
}
|
||||
IdentityKeyUtil.generateIdentityKeyPair(this)
|
||||
TextSecurePreferences.setPasswordDisabled(this, true)
|
||||
// AC: This is a temporary workaround to trick the old code that the screen is unlocked.
|
||||
|
@ -4,7 +4,9 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
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.InputMethodManager
|
||||
import android.widget.Toast
|
||||
@ -13,14 +15,13 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
|
||||
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
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.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityLinkDeviceBinding
|
||||
private val adapter = LinkDeviceActivityAdapter(this)
|
||||
private var restoreJob: Job? = null
|
||||
|
||||
@ -55,9 +57,10 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
||||
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
||||
setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
|
||||
}
|
||||
setContentView(R.layout.activity_link_device)
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
binding = ActivityLinkDeviceBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -107,8 +110,8 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
||||
TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
||||
TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true)
|
||||
|
||||
loader.isVisible = true
|
||||
val snackBar = Snackbar.make(containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE)
|
||||
binding.loader.isVisible = true
|
||||
val snackBar = Snackbar.make(binding.containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.registration_activity__skip) { register(true) }
|
||||
|
||||
val skipJob = launch {
|
||||
@ -127,13 +130,13 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
||||
register(false)
|
||||
}
|
||||
|
||||
loader.isVisible = false
|
||||
binding.loader.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun register(skipped: Boolean) {
|
||||
restoreJob?.cancel()
|
||||
loader.isVisible = false
|
||||
binding.loader.isVisible = false
|
||||
TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis())
|
||||
val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
@ -175,30 +178,34 @@ private class LinkDeviceActivityAdapter(private val activity: LinkDeviceActivity
|
||||
|
||||
// region Recovery Phrase Fragment
|
||||
class RecoveryPhraseFragment : Fragment() {
|
||||
private lateinit var binding: FragmentRecoveryPhraseBinding
|
||||
|
||||
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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
handleContinueButtonTapped()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
with(binding) {
|
||||
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
handleContinueButtonTapped()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
continueButton.setOnClickListener { handleContinueButtonTapped() }
|
||||
}
|
||||
continueButton.setOnClickListener { handleContinueButtonTapped() }
|
||||
}
|
||||
|
||||
private fun handleContinueButtonTapped() {
|
||||
val mnemonic = mnemonicEditText.text?.trim().toString()
|
||||
val mnemonic = binding.mnemonicEditText.text?.trim().toString()
|
||||
(requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic)
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,8 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
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.databinding.ActivityPnModeBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
@ -28,6 +27,7 @@ import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.util.PNModeView
|
||||
|
||||
class PNModeActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityPnModeBinding
|
||||
private var selectedOptionView: PNModeView? = null
|
||||
|
||||
// region Lifecycle
|
||||
@ -35,15 +35,18 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo(true)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||
setContentView(R.layout.activity_pn_mode)
|
||||
contentView.disableClipping()
|
||||
fcmOptionView.setOnClickListener { toggleFCM() }
|
||||
fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
||||
fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
||||
backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() }
|
||||
backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
||||
backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
||||
registerButton.setOnClickListener { register() }
|
||||
binding = ActivityPnModeBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
with(binding) {
|
||||
contentView.disableClipping()
|
||||
fcmOptionView.setOnClickListener { toggleFCM() }
|
||||
fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
||||
fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
||||
backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() }
|
||||
backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
||||
backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
||||
registerButton.setOnClickListener { register() }
|
||||
}
|
||||
toggleFCM()
|
||||
}
|
||||
|
||||
@ -63,8 +66,7 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
|
||||
// region Interaction
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val id = item.itemId
|
||||
when(id) {
|
||||
when(item.itemId) {
|
||||
R.id.learnMoreButton -> learnMore()
|
||||
else -> { /* Do nothing */ }
|
||||
}
|
||||
@ -81,52 +83,52 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleFCM() {
|
||||
private fun toggleFCM() = with(binding) {
|
||||
when (selectedOptionView) {
|
||||
null -> {
|
||||
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)
|
||||
selectedOptionView = fcmOptionView
|
||||
}
|
||||
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)
|
||||
selectedOptionView = null
|
||||
}
|
||||
backgroundPollingOptionView -> {
|
||||
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)
|
||||
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)
|
||||
selectedOptionView = fcmOptionView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleBackgroundPolling() {
|
||||
private fun toggleBackgroundPolling() = with(binding) {
|
||||
when (selectedOptionView) {
|
||||
null -> {
|
||||
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)
|
||||
selectedOptionView = backgroundPollingOptionView
|
||||
}
|
||||
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)
|
||||
selectedOptionView = null
|
||||
}
|
||||
fcmOptionView -> {
|
||||
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)
|
||||
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)
|
||||
selectedOptionView = backgroundPollingOptionView
|
||||
}
|
||||
@ -153,7 +155,7 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
dialog.create().show()
|
||||
return
|
||||
}
|
||||
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView))
|
||||
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView))
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.startPollingIfNeeded()
|
||||
application.registerForFCMIfNeeded(true)
|
||||
|
@ -11,8 +11,8 @@ import android.text.style.ClickableSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_recovery_phrase_restore.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.utilities.Hex
|
||||
@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
|
||||
private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -36,9 +36,10 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
||||
setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
||||
}
|
||||
setContentView(R.layout.activity_recovery_phrase_restore)
|
||||
mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
restoreButton.setOnClickListener { restore() }
|
||||
binding = ActivityRecoveryPhraseRestoreBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
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")
|
||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsExplanation.setSpan(object : ClickableSpan() {
|
||||
@ -54,14 +55,14 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
openURL("https://getsession.org/privacy-policy/")
|
||||
}
|
||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
termsTextView.text = termsExplanation
|
||||
binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.termsTextView.text = termsExplanation
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
private fun restore() {
|
||||
val mnemonic = mnemonicEditText.text.toString()
|
||||
val mnemonic = binding.mnemonicEditText.text.toString()
|
||||
try {
|
||||
val loadFileContents: (String) -> String = { fileName ->
|
||||
MnemonicUtilities.loadFileContents(this, fileName)
|
||||
|
@ -16,8 +16,8 @@ import android.text.style.StyleSpan
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import kotlinx.android.synthetic.main.activity_register.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityRegisterBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
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.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
import java.util.*
|
||||
|
||||
class RegisterActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityRegisterBinding
|
||||
private var seed: ByteArray? = null
|
||||
private var ed25519KeyPair: KeyPair? = null
|
||||
private var x25519KeyPair: ECKeyPair? = null
|
||||
@ -37,7 +37,8 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_register)
|
||||
binding = ActivityRegisterBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setUpActionBarSessionLogo()
|
||||
TextSecurePreferences.apply {
|
||||
setHasViewedSeed(this@RegisterActivity, false)
|
||||
@ -45,8 +46,8 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
setRestorationTime(this@RegisterActivity, 0)
|
||||
setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis())
|
||||
}
|
||||
registerButton.setOnClickListener { register() }
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
binding.registerButton.setOnClickListener { register() }
|
||||
binding.copyButton.setOnClickListener { copyPublicKey() }
|
||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
|
||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsExplanation.setSpan(object : ClickableSpan() {
|
||||
@ -62,8 +63,8 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
openURL("https://getsession.org/privacy-policy/")
|
||||
}
|
||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
termsTextView.text = termsExplanation
|
||||
binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.termsTextView.text = termsExplanation
|
||||
updateKeyPair()
|
||||
}
|
||||
// endregion
|
||||
@ -94,12 +95,12 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
}
|
||||
count += 1
|
||||
if (count < limit) {
|
||||
publicKeyTextView.text = mangledHexEncodedPublicKey
|
||||
binding.publicKeyTextView.text = mangledHexEncodedPublicKey
|
||||
Handler().postDelayed({
|
||||
animate()
|
||||
}, 32)
|
||||
} else {
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
binding.publicKeyTextView.text = hexEncodedPublicKey
|
||||
}
|
||||
}
|
||||
animate()
|
||||
|
@ -9,8 +9,8 @@ import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_seed.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivitySeedBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
||||
@ -21,6 +21,8 @@ import org.thoughtcrime.securesms.util.getColorWithID
|
||||
|
||||
class SeedActivity : BaseActionBarActivity() {
|
||||
|
||||
private lateinit var binding: ActivitySeedBinding
|
||||
|
||||
private val seed by lazy {
|
||||
var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED)
|
||||
if (hexEncodedSeed == null) {
|
||||
@ -35,27 +37,30 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_seed)
|
||||
binding = ActivitySeedBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_seed_title)
|
||||
val seedReminderViewTitle = SpannableString("You're almost finished! 90%") // Intentionally not yet translated
|
||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2)
|
||||
seedReminderView.setProgress(90, false)
|
||||
seedReminderView.hideContinueButton()
|
||||
var redactedSeed = seed
|
||||
var index = 0
|
||||
for (character in seed) {
|
||||
if (character.isLetter()) {
|
||||
redactedSeed = redactedSeed.replaceRange(index, index + 1, "▆")
|
||||
with(binding) {
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2)
|
||||
seedReminderView.setProgress(90, false)
|
||||
seedReminderView.hideContinueButton()
|
||||
var redactedSeed = seed
|
||||
var index = 0
|
||||
for (character in seed) {
|
||||
if (character.isLetter()) {
|
||||
redactedSeed = redactedSeed.replaceRange(index, index + 1, "▆")
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
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
|
||||
|
||||
@ -63,14 +68,16 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
private fun revealSeed() {
|
||||
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)
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3)
|
||||
seedReminderView.setProgress(100, true)
|
||||
val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams
|
||||
seedTextViewLayoutParams.height = seedTextView.height
|
||||
seedTextView.layoutParams = seedTextViewLayoutParams
|
||||
seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
|
||||
seedTextView.text = seed
|
||||
with(binding) {
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3)
|
||||
seedReminderView.setProgress(100, true)
|
||||
val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams
|
||||
seedTextViewLayoutParams.height = seedTextView.height
|
||||
seedTextView.layoutParams = seedTextViewLayoutParams
|
||||
seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
|
||||
seedTextView.text = seed
|
||||
}
|
||||
TextSecurePreferences.setHasViewedSeed(this, true)
|
||||
}
|
||||
// endregion
|
||||
|
@ -6,16 +6,17 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import kotlinx.android.synthetic.main.view_seed_reminder.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewSeedReminderBinding
|
||||
|
||||
class SeedReminderView : FrameLayout {
|
||||
private lateinit var binding: ViewSeedReminderBinding
|
||||
|
||||
var title: CharSequence
|
||||
get() = titleTextView.text
|
||||
set(value) { titleTextView.text = value }
|
||||
get() = binding.titleTextView.text
|
||||
set(value) { binding.titleTextView.text = value }
|
||||
var subtitle: CharSequence
|
||||
get() = subtitleTextView.text
|
||||
set(value) { subtitleTextView.text = value }
|
||||
get() = binding.subtitleTextView.text
|
||||
set(value) { binding.subtitleTextView.text = value }
|
||||
var delegate: SeedReminderViewDelegate? = null
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
@ -35,22 +36,20 @@ class SeedReminderView : FrameLayout {
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_seed_reminder, null)
|
||||
addView(contentView)
|
||||
button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() }
|
||||
binding = ViewSeedReminderBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() }
|
||||
}
|
||||
|
||||
fun setProgress(progress: Int, isAnimated: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
progressBar.setProgress(progress, isAnimated)
|
||||
binding.progressBar.setProgress(progress, isAnimated)
|
||||
} else {
|
||||
progressBar.progress = progress
|
||||
binding.progressBar.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
fun hideContinueButton() {
|
||||
button.visibility = View.GONE
|
||||
binding.button.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,12 @@ import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.android.synthetic.main.dialog_clear_all_data.*
|
||||
import kotlinx.android.synthetic.main.dialog_clear_all_data.view.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogClearAllDataBinding
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
@ -15,6 +17,7 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
|
||||
class ClearAllDataDialog : BaseDialog() {
|
||||
private lateinit var binding: DialogClearAllDataBinding
|
||||
|
||||
enum class Steps {
|
||||
INFO_PROMPT,
|
||||
@ -34,15 +37,15 @@ class ClearAllDataDialog : BaseDialog() {
|
||||
}
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null)
|
||||
contentView.cancelButton.setOnClickListener {
|
||||
binding = DialogClearAllDataBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener {
|
||||
if (step == Steps.NETWORK_PROMPT) {
|
||||
clearAllData(false)
|
||||
} else if (step != Steps.DELETING) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
contentView.clearAllDataButton.setOnClickListener {
|
||||
binding.clearAllDataButton.setOnClickListener {
|
||||
when(step) {
|
||||
Steps.INFO_PROMPT -> step = Steps.NETWORK_PROMPT
|
||||
Steps.NETWORK_PROMPT -> {
|
||||
@ -51,36 +54,33 @@ class ClearAllDataDialog : BaseDialog() {
|
||||
Steps.DELETING -> { /* do nothing intentionally */ }
|
||||
}
|
||||
}
|
||||
builder.setView(contentView)
|
||||
builder.setView(binding.root)
|
||||
builder.setCancelable(false)
|
||||
}
|
||||
|
||||
private fun updateUI() {
|
||||
|
||||
dialog?.let { view ->
|
||||
|
||||
dialog?.let {
|
||||
val isLoading = step == Steps.DELETING
|
||||
|
||||
when (step) {
|
||||
Steps.INFO_PROMPT -> {
|
||||
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation)
|
||||
view.cancelButton.setText(R.string.cancel)
|
||||
view.clearAllDataButton.setText(R.string.delete)
|
||||
binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation)
|
||||
binding.cancelButton.setText(R.string.cancel)
|
||||
binding.clearAllDataButton.setText(R.string.delete)
|
||||
}
|
||||
else -> {
|
||||
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation)
|
||||
view.cancelButton.setText(R.string.dialog_clear_all_data_local_only)
|
||||
view.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network)
|
||||
binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation)
|
||||
binding.cancelButton.setText(R.string.dialog_clear_all_data_local_only)
|
||||
binding.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network)
|
||||
}
|
||||
}
|
||||
|
||||
view.cancelButton.isVisible = !isLoading
|
||||
view.clearAllDataButton.isVisible = !isLoading
|
||||
view.progressBar.isVisible = isLoading
|
||||
binding.cancelButton.isVisible = !isLoading
|
||||
binding.clearAllDataButton.isVisible = !isLoading
|
||||
binding.progressBar.isVisible = isLoading
|
||||
|
||||
view.setCanceledOnTouchOutside(!isLoading)
|
||||
it.setCanceledOnTouchOutside(!isLoading)
|
||||
isCancelable = !isLoading
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,9 +54,9 @@ public abstract class CorrectedPreferenceFragment extends PreferenceFragmentComp
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("RestrictedApi")
|
||||
protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
|
||||
return new PreferenceGroupAdapter(preferenceScreen) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
|
||||
super.onBindViewHolder(holder, position);
|
||||
|
@ -10,9 +10,9 @@ import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
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.databinding.ActivityQrCodeBinding
|
||||
import network.loki.messenger.databinding.FragmentViewMyQrCodeBinding
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
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.conversation.v2.ConversationActivityV2
|
||||
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.FileOutputStream
|
||||
|
||||
class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityQrCodeBinding
|
||||
private val adapter = QRCodeActivityAdapter(this)
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityQrCodeBinding.inflate(layoutInflater)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_qr_code)
|
||||
setContentView(binding.root)
|
||||
// Set title
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title)
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -91,6 +97,7 @@ private class QRCodeActivityAdapter(val activity: QRCodeActivity) : FragmentPage
|
||||
|
||||
// region View My QR Code Fragment
|
||||
class ViewMyQRCodeFragment : Fragment() {
|
||||
private lateinit var binding: FragmentViewMyQrCodeBinding
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
get() {
|
||||
@ -98,18 +105,19 @@ class ViewMyQRCodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val size = toPx(280, resources)
|
||||
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.")
|
||||
// explanation.setSpan(StyleSpan(Typeface.BOLD), 8, 34, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation)
|
||||
shareButton.setOnClickListener { shareQRCode() }
|
||||
binding.explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation)
|
||||
binding.shareButton.setOnClickListener { shareQRCode() }
|
||||
}
|
||||
|
||||
private fun shareQRCode() {
|
||||
|
@ -6,8 +6,8 @@ import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_seed.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogSeedBinding
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
@ -28,11 +28,11 @@ class SeedDialog : BaseDialog() {
|
||||
}
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_seed, null)
|
||||
contentView.seedTextView.text = seed
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.copyButton.setOnClickListener { copySeed() }
|
||||
builder.setView(contentView)
|
||||
val binding = DialogSeedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.seedTextView.text = seed
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.copyButton.setOnClickListener { copySeed() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun copySeed() {
|
||||
|
@ -7,7 +7,11 @@ import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.Menu
|
||||
import android.view.MenuItem
|
||||
@ -15,9 +19,9 @@ import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.activity_settings.*
|
||||
import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivitySettingsBinding
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.all
|
||||
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.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||
import org.thoughtcrime.securesms.home.PathActivity
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
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.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
private lateinit var binding: ActivitySettingsBinding
|
||||
private var displayNameEditActionMode: ActionMode? = null
|
||||
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
|
||||
private lateinit var glide: GlideRequests
|
||||
@ -59,33 +71,38 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
setContentView(R.layout.activity_settings)
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey
|
||||
glide = GlideApp.with(this)
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.publicKey = hexEncodedPublicKey
|
||||
profilePictureView.displayName = displayName
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update()
|
||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||
btnGroupNameDisplay.text = displayName
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
shareButton.setOnClickListener { sharePublicKey() }
|
||||
privacyButton.setOnClickListener { showPrivacySettings() }
|
||||
notificationsButton.setOnClickListener { showNotificationSettings() }
|
||||
chatsButton.setOnClickListener { showChatSettings() }
|
||||
sendInvitationButton.setOnClickListener { sendInvitation() }
|
||||
faqButton.setOnClickListener { showFAQ() }
|
||||
surveyButton.setOnClickListener { showSurvey() }
|
||||
helpTranslateButton.setOnClickListener { helpTranslate() }
|
||||
seedButton.setOnClickListener { showSeed() }
|
||||
clearAllDataButton.setOnClickListener { clearAllData() }
|
||||
debugLogButton.setOnClickListener { shareLogs() }
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(this)
|
||||
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})")
|
||||
with(binding) {
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.publicKey = hexEncodedPublicKey
|
||||
profilePictureView.displayName = displayName
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update()
|
||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||
btnGroupNameDisplay.text = displayName
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
shareButton.setOnClickListener { sharePublicKey() }
|
||||
pathButton.setOnClickListener { showPath() }
|
||||
pathContainer.disableClipping()
|
||||
privacyButton.setOnClickListener { showPrivacySettings() }
|
||||
notificationsButton.setOnClickListener { showNotificationSettings() }
|
||||
chatsButton.setOnClickListener { showChatSettings() }
|
||||
sendInvitationButton.setOnClickListener { sendInvitation() }
|
||||
faqButton.setOnClickListener { showFAQ() }
|
||||
surveyButton.setOnClickListener { showSurvey() }
|
||||
helpTranslateButton.setOnClickListener { helpTranslate() }
|
||||
seedButton.setOnClickListener { showSeed() }
|
||||
clearAllDataButton.setOnClickListener { clearAllData() }
|
||||
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 {
|
||||
@ -152,22 +169,22 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
private fun handleDisplayNameEditActionModeChanged() {
|
||||
val isEditingDisplayName = this.displayNameEditActionMode !== null
|
||||
|
||||
btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
|
||||
displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
|
||||
binding.btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
|
||||
binding.displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
if (isEditingDisplayName) {
|
||||
displayNameEditText.setText(btnGroupNameDisplay.text)
|
||||
displayNameEditText.selectAll()
|
||||
displayNameEditText.requestFocus()
|
||||
inputMethodManager.showSoftInput(displayNameEditText, 0)
|
||||
binding.displayNameEditText.setText(binding.btnGroupNameDisplay.text)
|
||||
binding.displayNameEditText.selectAll()
|
||||
binding.displayNameEditText.requestFocus()
|
||||
inputMethodManager.showSoftInput(binding.displayNameEditText, 0)
|
||||
} else {
|
||||
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0)
|
||||
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfile(isUpdatingProfilePicture: Boolean) {
|
||||
loader.isVisible = true
|
||||
binding.loader.isVisible = true
|
||||
val promises = mutableListOf<Promise<*, Exception>>()
|
||||
val displayName = displayNameToBeUploaded
|
||||
if (displayName != null) {
|
||||
@ -192,15 +209,15 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
compoundPromise.alwaysUi {
|
||||
if (displayName != null) {
|
||||
btnGroupNameDisplay.text = displayName
|
||||
binding.btnGroupNameDisplay.text = displayName
|
||||
}
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
profilePictureView.recycle() // Clear the cached image before updating
|
||||
profilePictureView.update()
|
||||
binding.profilePictureView.recycle() // Clear the cached image before updating
|
||||
binding.profilePictureView.update()
|
||||
}
|
||||
displayNameToBeUploaded = null
|
||||
profilePictureToBeUploaded = null
|
||||
loader.isVisible = false
|
||||
binding.loader.isVisible = false
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
@ -211,7 +228,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
* @return true if the update was successful.
|
||||
*/
|
||||
private fun saveDisplayName(): Boolean {
|
||||
val displayName = displayNameEditText.text.toString().trim()
|
||||
val displayName = binding.displayNameEditText.text.toString().trim()
|
||||
if (displayName.isEmpty()) {
|
||||
Toast.makeText(this, R.string.activity_settings_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
@ -291,6 +308,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPath() {
|
||||
val intent = Intent(this, PathActivity::class.java)
|
||||
show(intent)
|
||||
}
|
||||
|
||||
private fun showSurvey() {
|
||||
try {
|
||||
val url = "https://getsession.org/survey"
|
||||
|
@ -13,12 +13,12 @@ import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.android.synthetic.main.dialog_share_logs.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogShareLogsBinding
|
||||
import org.session.libsignal.utilities.ExternalStorageUtil
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
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.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.Objects
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ShareLogsDialog : BaseDialog() {
|
||||
@ -34,16 +34,15 @@ class ShareLogsDialog : BaseDialog() {
|
||||
private var shareJob: Job? = null
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView =
|
||||
LayoutInflater.from(requireContext()).inflate(R.layout.dialog_share_logs, null)
|
||||
contentView.cancelButton.setOnClickListener {
|
||||
val binding = DialogShareLogsBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
contentView.shareButton.setOnClickListener {
|
||||
binding.shareButton.setOnClickListener {
|
||||
// start the export and share
|
||||
shareLogs()
|
||||
}
|
||||
builder.setView(contentView)
|
||||
builder.setView(binding.root)
|
||||
builder.setCancelable(false)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,229 @@
|
||||
package org.thoughtcrime.securesms.repository
|
||||
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
interface ConversationRepository {
|
||||
fun isOxenHostedOpenGroup(threadId: Long): Boolean
|
||||
fun getRecipientForThreadId(threadId: Long): Recipient
|
||||
fun saveDraft(threadId: Long, text: String)
|
||||
fun getDraft(threadId: Long): String?
|
||||
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
||||
fun unblock(recipient: Recipient)
|
||||
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
||||
|
||||
suspend fun deleteForEveryone(
|
||||
threadId: Long,
|
||||
recipient: Recipient,
|
||||
message: MessageRecord
|
||||
): ResultOf<Unit>
|
||||
|
||||
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
|
||||
|
||||
suspend fun deleteMessageWithoutUnsendRequest(
|
||||
threadId: Long,
|
||||
messages: Set<MessageRecord>
|
||||
): ResultOf<Unit>
|
||||
|
||||
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||
|
||||
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||
}
|
||||
|
||||
class DefaultConversationRepository @Inject constructor(
|
||||
private val textSecurePreferences: TextSecurePreferences,
|
||||
private val messageDataProvider: MessageDataProvider,
|
||||
private val threadDb: ThreadDatabase,
|
||||
private val draftDb: DraftDatabase,
|
||||
private val lokiThreadDb: LokiThreadDatabase,
|
||||
private val smsDb: SmsDatabase,
|
||||
private val mmsDb: MmsDatabase,
|
||||
private val recipientDb: RecipientDatabase,
|
||||
private val lokiMessageDb: LokiMessageDatabase
|
||||
) : ConversationRepository {
|
||||
|
||||
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||
return openGroup?.room == "session" || openGroup?.room == "oxen"
|
||||
|| openGroup?.room == "lokinet" || openGroup?.room == "crypto"
|
||||
}
|
||||
|
||||
override fun getRecipientForThreadId(threadId: Long): Recipient {
|
||||
return threadDb.getRecipientForThreadId(threadId)!!
|
||||
}
|
||||
|
||||
override fun saveDraft(threadId: Long, text: String) {
|
||||
if (text.isEmpty()) return
|
||||
val drafts = DraftDatabase.Drafts()
|
||||
drafts.add(DraftDatabase.Draft(DraftDatabase.Draft.TEXT, text))
|
||||
draftDb.insertDrafts(threadId, drafts)
|
||||
}
|
||||
|
||||
override fun getDraft(threadId: Long): String? {
|
||||
val drafts = draftDb.getDrafts(threadId)
|
||||
draftDb.clearDrafts(threadId)
|
||||
return drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value
|
||||
}
|
||||
|
||||
override fun inviteContacts(threadId: Long, contacts: List<Recipient>) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
|
||||
for (contact in contacts) {
|
||||
val message = VisibleMessage()
|
||||
message.sentTimestamp = System.currentTimeMillis()
|
||||
val openGroupInvitation = OpenGroupInvitation()
|
||||
openGroupInvitation.name = openGroup.name
|
||||
openGroupInvitation.url = openGroup.joinURL
|
||||
message.openGroupInvitation = openGroupInvitation
|
||||
val outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation(
|
||||
openGroupInvitation,
|
||||
contact,
|
||||
message.sentTimestamp
|
||||
)
|
||||
smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!)
|
||||
MessageSender.send(message, contact.address)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unblock(recipient: Recipient) {
|
||||
recipientDb.setBlocked(recipient, false)
|
||||
}
|
||||
|
||||
override fun deleteLocally(recipient: Recipient, message: MessageRecord) {
|
||||
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||
textSecurePreferences.getLocalNumber()?.let {
|
||||
MessageSender.send(unsendRequest, Address.fromSerialized(it))
|
||||
}
|
||||
}
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
}
|
||||
|
||||
override suspend fun deleteForEveryone(
|
||||
threadId: Long,
|
||||
recipient: Recipient,
|
||||
message: MessageRecord
|
||||
): ResultOf<Unit> = suspendCoroutine { continuation ->
|
||||
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||
MessageSender.send(unsendRequest, recipient.address)
|
||||
}
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||
if (openGroup != null) {
|
||||
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
|
||||
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||
.success {
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
}.fail { error ->
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
messageDataProvider.getServerHashForMessage(message.id)?.let { serverHash ->
|
||||
var publicKey = recipient.address.serialize()
|
||||
if (recipient.isClosedGroupRecipient) {
|
||||
publicKey = GroupUtil.doubleDecodeGroupID(publicKey).toHexString()
|
||||
}
|
||||
SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
|
||||
.success {
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
}.fail { error ->
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? {
|
||||
if (recipient.isOpenGroupRecipient) return null
|
||||
messageDataProvider.getServerHashForMessage(message.id) ?: return null
|
||||
val unsendRequest = UnsendRequest()
|
||||
if (message.isOutgoing) {
|
||||
unsendRequest.author = textSecurePreferences.getLocalNumber()
|
||||
} else {
|
||||
unsendRequest.author = message.individualRecipient.address.contactIdentifier()
|
||||
}
|
||||
unsendRequest.timestamp = message.timestamp
|
||||
|
||||
return unsendRequest
|
||||
}
|
||||
|
||||
override suspend fun deleteMessageWithoutUnsendRequest(
|
||||
threadId: Long,
|
||||
messages: Set<MessageRecord>
|
||||
): ResultOf<Unit> = suspendCoroutine { continuation ->
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||
if (openGroup != null) {
|
||||
val messageServerIDs = mutableMapOf<Long, MessageRecord>()
|
||||
for (message in messages) {
|
||||
val messageServerID =
|
||||
lokiMessageDb.getServerID(message.id, !message.isMms) ?: continue
|
||||
messageServerIDs[messageServerID] = message
|
||||
}
|
||||
for ((messageServerID, message) in messageServerIDs) {
|
||||
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||
.success {
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
}.fail { error ->
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (message in messages) {
|
||||
if (message.isMms) {
|
||||
mmsDb.deleteMessage(message.id)
|
||||
} else {
|
||||
smsDb.deleteMessage(message.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
}
|
||||
|
||||
override suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit> =
|
||||
suspendCoroutine { continuation ->
|
||||
val sessionID = recipient.address.toString()
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||
OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server)
|
||||
.success {
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
}.fail { error ->
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> =
|
||||
suspendCoroutine { continuation ->
|
||||
val sessionID = recipient.address.toString()
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||
OpenGroupAPIV2.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
|
||||
.success {
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
}.fail { error ->
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.repository
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
sealed class ResultOf<out T> {
|
||||
|
||||
data class Success<out R>(val value: R) : ResultOf<R>()
|
||||
|
||||
data class Failure(val throwable: Throwable) : ResultOf<Nothing>()
|
||||
|
||||
inline fun onFailure(block: (throwable: Throwable) -> Unit) = this.also {
|
||||
if (this is Failure) {
|
||||
block(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun onSuccess(block: (value: T) -> Unit) = this.also {
|
||||
if (this is Success) {
|
||||
block(value)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <R> flatMap(mapper: (T) -> R): ResultOf<R> = when (this) {
|
||||
is Success -> wrap { mapper(value) }
|
||||
is Failure -> Failure(throwable)
|
||||
}
|
||||
|
||||
fun getOrThrow(): T = when (this) {
|
||||
is Success -> value
|
||||
is Failure -> throw throwable
|
||||
}
|
||||
|
||||
companion object {
|
||||
inline fun <T> wrap(block: () -> T): ResultOf<T> =
|
||||
try {
|
||||
Success(block())
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Failure(e)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.search
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import org.session.libsession.utilities.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase
|
||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
object SearchModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSearchRepository(@ApplicationContext context: Context,
|
||||
searchDatabase: SearchDatabase,
|
||||
threadDatabase: ThreadDatabase,
|
||||
groupDatabase: GroupDatabase,
|
||||
contactDatabase: SessionContactDatabase) =
|
||||
SearchRepository(context, searchDatabase, threadDatabase, groupDatabase, contactDatabase, ContactAccessor.getInstance(), SignalExecutors.SERIAL)
|
||||
|
||||
|
||||
}
|
@ -3,29 +3,39 @@ package org.thoughtcrime.securesms.search;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.database.MergeCursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
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.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.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionContactDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
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.SearchResult;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import kotlin.Pair;
|
||||
|
||||
/**
|
||||
* Manages data retrieval for search.
|
||||
*/
|
||||
@ -50,21 +60,27 @@ public class SearchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final SearchDatabase searchDatabase;
|
||||
private final ThreadDatabase threadDatabase;
|
||||
private final ContactAccessor contactAccessor;
|
||||
private final Executor executor;
|
||||
private final Context context;
|
||||
private final SearchDatabase searchDatabase;
|
||||
private final ThreadDatabase threadDatabase;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final SessionContactDatabase contactDatabase;
|
||||
private final ContactAccessor contactAccessor;
|
||||
private final Executor executor;
|
||||
|
||||
public SearchRepository(@NonNull Context context,
|
||||
@NonNull SearchDatabase searchDatabase,
|
||||
@NonNull ThreadDatabase threadDatabase,
|
||||
@NonNull GroupDatabase groupDatabase,
|
||||
@NonNull SessionContactDatabase contactDatabase,
|
||||
@NonNull ContactAccessor contactAccessor,
|
||||
@NonNull Executor executor)
|
||||
{
|
||||
this.context = context.getApplicationContext();
|
||||
this.searchDatabase = searchDatabase;
|
||||
this.threadDatabase = threadDatabase;
|
||||
this.groupDatabase = groupDatabase;
|
||||
this.contactDatabase = contactDatabase;
|
||||
this.contactAccessor = contactAccessor;
|
||||
this.executor = executor;
|
||||
}
|
||||
@ -81,10 +97,10 @@ public class SearchRepository {
|
||||
String cleanQuery = sanitizeQuery(query);
|
||||
timer.split("clean");
|
||||
|
||||
CursorList<Recipient> contacts = queryContacts(cleanQuery);
|
||||
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
|
||||
timer.split("contacts");
|
||||
|
||||
CursorList<ThreadRecord> conversations = queryConversations(cleanQuery);
|
||||
CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond());
|
||||
timer.split("conversations");
|
||||
|
||||
CursorList<MessageResult> messages = queryMessages(cleanQuery);
|
||||
@ -92,7 +108,7 @@ public class SearchRepository {
|
||||
|
||||
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) {
|
||||
return CursorList.emptyList();
|
||||
/* Loki - We don't need contacts permission
|
||||
if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
|
||||
return CursorList.emptyList();
|
||||
private Pair<CursorList<Contact>, List<String>> queryContacts(String query) {
|
||||
|
||||
Cursor contacts = contactDatabase.queryContactsByName(query);
|
||||
List<Address> contactList = new ArrayList<>();
|
||||
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);
|
||||
Cursor systemContacts = contactsDatabase.querySystemContacts(query);
|
||||
MergeCursor contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
|
||||
contacts.close();
|
||||
|
||||
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<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);
|
||||
return conversations != null ? new CursorList<>(conversations, new ThreadModelBuilder(threadDatabase))
|
||||
: CursorList.emptyList();
|
||||
Cursor membersGroupList = groupDatabase.getGroupsFilteredByMembers(matchingAddresses);
|
||||
if (membersGroupList != null) {
|
||||
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) {
|
||||
@ -169,6 +219,28 @@ public class SearchRepository {
|
||||
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 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 final ThreadDatabase threadDatabase;
|
||||
@ -208,7 +296,7 @@ public class SearchRepository {
|
||||
|
||||
@Override
|
||||
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)));
|
||||
Recipient conversationRecipient = Recipient.from(context, conversationAddress, false);
|
||||
Recipient messageRecipient = Recipient.from(context, messageAddress, false);
|
||||
|
@ -4,9 +4,10 @@ import android.database.ContentObserver;
|
||||
|
||||
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.model.ThreadRecord;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -19,13 +20,13 @@ public class SearchResult {
|
||||
public static final SearchResult EMPTY = new SearchResult("", CursorList.emptyList(), CursorList.emptyList(), CursorList.emptyList());
|
||||
|
||||
private final String query;
|
||||
private final CursorList<Recipient> contacts;
|
||||
private final CursorList<ThreadRecord> conversations;
|
||||
private final CursorList<Contact> contacts;
|
||||
private final CursorList<GroupRecord> conversations;
|
||||
private final CursorList<MessageResult> messages;
|
||||
|
||||
public SearchResult(@NonNull String query,
|
||||
@NonNull CursorList<Recipient> contacts,
|
||||
@NonNull CursorList<ThreadRecord> conversations,
|
||||
@NonNull CursorList<Contact> contacts,
|
||||
@NonNull CursorList<GroupRecord> conversations,
|
||||
@NonNull CursorList<MessageResult> messages)
|
||||
{
|
||||
this.query = query;
|
||||
@ -34,11 +35,11 @@ public class SearchResult {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public List<Recipient> getContacts() {
|
||||
public List<Contact> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public List<ThreadRecord> getConversations() {
|
||||
public List<GroupRecord> getConversations() {
|
||||
return conversations;
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user