Merge remote-tracking branch 'upstream/dev'

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

View File

@ -4,18 +4,17 @@ buildscript {
mavenCentral()
}
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
}
}

View File

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

View File

@ -15,6 +15,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
import network.loki.messenger.util.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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,130 @@
package org.thoughtcrime.securesms.conversation.v2
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
import java.util.UUID
class ConversationViewModel(
val threadId: Long,
private val repository: ConversationRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ConversationUiState())
val uiState: StateFlow<ConversationUiState> = _uiState
val recipient: Recipient by lazy {
repository.getRecipientForThreadId(threadId)
}
init {
_uiState.update {
it.copy(isOxenHostedOpenGroup = repository.isOxenHostedOpenGroup(threadId))
}
}
fun saveDraft(text: String) {
repository.saveDraft(threadId, text)
}
fun getDraft(): String? {
return repository.getDraft(threadId)
}
fun inviteContacts(contacts: List<Recipient>) {
repository.inviteContacts(threadId, contacts)
}
fun unblock() {
if (recipient.isContactRecipient) {
repository.unblock(recipient)
}
}
fun deleteLocally(message: MessageRecord) {
repository.deleteLocally(recipient, message)
}
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
repository.deleteForEveryone(threadId, recipient, message)
.onFailure {
showMessage("Couldn't delete message due to error: $it")
}
}
fun deleteMessagesWithoutUnsendRequest(messages: Set<MessageRecord>) = viewModelScope.launch {
repository.deleteMessageWithoutUnsendRequest(threadId, messages)
.onFailure {
showMessage("Couldn't delete message due to error: $it")
}
}
fun banUser(recipient: Recipient) = viewModelScope.launch {
repository.banUser(threadId, recipient)
.onSuccess {
showMessage("Successfully banned user")
}
.onFailure {
showMessage("Couldn't ban user due to error: $it")
}
}
fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch {
repository.banAndDeleteAll(threadId, recipient)
.onSuccess {
showMessage("Successfully banned user and deleted all their messages")
}
.onFailure {
showMessage("Couldn't execute request due to error: $it")
}
}
private fun showMessage(message: String) {
_uiState.update { currentUiState ->
val messages = currentUiState.uiMessages + UiMessage(
id = UUID.randomUUID().mostSignificantBits,
message = message
)
currentUiState.copy(uiMessages = messages)
}
}
fun messageShown(messageId: Long) {
_uiState.update { currentUiState ->
val messages = currentUiState.uiMessages.filterNot { it.id == messageId }
currentUiState.copy(uiMessages = messages)
}
}
@dagger.assisted.AssistedFactory
interface AssistedFactory {
fun create(threadId: Long): Factory
}
@Suppress("UNCHECKED_CAST")
class Factory @AssistedInject constructor(
@Assisted private val threadId: Long,
private val repository: ConversationRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ConversationViewModel(threadId, repository) as T
}
}
}
data class UiMessage(val id: Long, val message: String)
data class ConversationUiState(
val isOxenHostedOpenGroup: Boolean = false,
val uiMessages: List<UiMessage> = emptyList()
)

View File

@ -7,8 +7,8 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import 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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,6 +45,7 @@ import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.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;

View File

@ -0,0 +1,22 @@
package org.thoughtcrime.securesms.dependencies
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.session.libsession.utilities.AppTextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.repository.DefaultConversationRepository
@Module
@InstallIn(SingletonComponent::class)
abstract class AppModule {
@Binds
abstract fun bindTextSecurePreferences(preferences: AppTextSecurePreferences): TextSecurePreferences
@Binds
abstract fun bindConversationRepository(repository: DefaultConversationRepository): ConversationRepository
}

View File

@ -9,16 +9,18 @@ import android.content.Intent
import android.os.Bundle
import android.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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,129 @@
package org.thoughtcrime.securesms.home.search
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.search.model.MessageResult
import java.security.InvalidParameterException
import org.session.libsession.messaging.contacts.Contact as ContactModel
class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val HEADER_VIEW_TYPE = 0
const val CONTENT_VIEW_TYPE = 1
}
private var data: List<Model> = listOf()
private var query: String? = null
fun setNewData(query: String, newData: List<Model>) {
val diffResult = DiffUtil.calculateDiff(GlobalSearchDiff(this.query, query, data, newData))
this.query = query
data = newData
diffResult.dispatchUpdatesTo(this)
}
override fun getItemViewType(position: Int): Int =
if (data[position] is Model.Header) HEADER_VIEW_TYPE else CONTENT_VIEW_TYPE
override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
if (viewType == HEADER_VIEW_TYPE) {
HeaderView(
LayoutInflater.from(parent.context)
.inflate(R.layout.view_global_search_header, parent, false)
)
} else {
ContentView(
LayoutInflater.from(parent.context)
.inflate(R.layout.view_global_search_result, parent, false)
, modelCallback)
}
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>
) {
val newUpdateQuery: String? = payloads.firstOrNull { it is String } as String?
if (newUpdateQuery != null && holder is ContentView) {
holder.bindPayload(newUpdateQuery, data[position])
return
}
if (holder is HeaderView) {
holder.bind(data[position] as Model.Header)
} else if (holder is ContentView) {
holder.bind(query.orEmpty(), data[position])
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
onBindViewHolder(holder,position, mutableListOf())
}
class HeaderView(view: View) : RecyclerView.ViewHolder(view) {
val binding = ViewGlobalSearchHeaderBinding.bind(view)
fun bind(header: Model.Header) {
binding.searchHeader.setText(header.title)
}
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is ContentView) {
holder.binding.searchResultProfilePicture.recycle()
}
}
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
val binding = ViewGlobalSearchResultBinding.bind(view).apply {
searchResultProfilePicture.glide = GlideApp.with(root)
}
fun bindPayload(newQuery: String, model: Model) {
bindQuery(newQuery, model)
}
fun bind(query: String, model: Model) {
binding.searchResultProfilePicture.recycle()
when (model) {
is Model.GroupConversation -> bindModel(query, model)
is Model.Contact -> bindModel(query, model)
is Model.Message -> bindModel(query, model)
is Model.SavedMessages -> bindModel(model)
is Model.Header -> throw InvalidParameterException("Can't display Model.Header as ContentView")
}
binding.root.setOnClickListener { modelCallback(model) }
}
}
data class MessageModel(
val threadRecipient: Recipient,
val messageRecipient: Recipient,
val messageSnippet: String
)
sealed class Model {
data class Header(@StringRes val title: Int) : Model()
data class SavedMessages(val currentUserPublicKey: String): Model()
data class Contact(val contact: ContactModel) : Model()
data class GroupConversation(val groupRecord: GroupRecord) : Model()
data class Message(val messageResult: MessageResult, val unread: Int) : Model()
}
}

View File

@ -0,0 +1,160 @@
package org.thoughtcrime.securesms.home.search
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.TypedValue
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.SearchUtil
import java.util.Locale
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel
class GlobalSearchDiff(
private val oldQuery: String?,
private val newQuery: String?,
private val oldData: List<GlobalSearchAdapter.Model>,
private val newData: List<GlobalSearchAdapter.Model>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldData.size
override fun getNewListSize(): Int = newData.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldData[oldItemPosition] == newData[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldQuery == newQuery && oldData[oldItemPosition] == newData[newItemPosition]
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? =
if (oldQuery != newQuery) newQuery
else null
}
private val BoldStyleFactory = { StyleSpan(Typeface.BOLD) }
fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) {
when (model) {
is ContactModel -> {
binding.searchResultTitle.text = getHighlight(
query,
model.contact.getSearchName()
)
}
is Message -> {
val textSpannable = SpannableStringBuilder()
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
// group chat, bind
val text = "${model.messageResult.messageRecipient.getSearchName()}: "
textSpannable.append(text)
}
textSpannable.append(getHighlight(
query,
model.messageResult.bodySnippet
))
binding.searchResultSubtitle.text = textSpannable
binding.searchResultSubtitle.isVisible = true
binding.searchResultTitle.text = model.messageResult.conversationRecipient.toShortString()
}
is GroupConversation -> {
binding.searchResultTitle.text = getHighlight(
query,
model.groupRecord.title
)
val membersString = model.groupRecord.members.joinToString { address ->
val recipient = Recipient.from(binding.root.context, address, false)
recipient.name ?: "${address.serialize().take(4)}...${address.serialize().takeLast(4)}"
}
binding.searchResultSubtitle.text = getHighlight(query, membersString)
}
}
}
private fun getHighlight(query: String?, toSearch: String): Spannable? {
return SearchUtil.getHighlightedSpan(Locale.getDefault(), BoldStyleFactory, toSearch, query)
}
fun ContentView.bindModel(query: String?, model: GroupConversation) {
binding.searchResultProfilePicture.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
binding.searchResultTimestamp.isVisible = false
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false)
binding.searchResultProfilePicture.update(threadRecipient)
val nameString = model.groupRecord.title
binding.searchResultTitle.text = getHighlight(query, nameString)
val groupRecipients = model.groupRecord.members.map { Recipient.from(binding.root.context, it, false) }
val membersString = groupRecipients.joinToString {
val address = it.address.serialize()
it.name ?: "${address.take(4)}...${address.takeLast(4)}"
}
if (model.groupRecord.isClosedGroup) {
binding.searchResultSubtitle.text = getHighlight(query, membersString)
}
}
fun ContentView.bindModel(query: String?, model: ContactModel) {
binding.searchResultProfilePicture.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultSubtitle.isVisible = false
binding.searchResultTimestamp.isVisible = false
binding.searchResultSubtitle.text = null
val recipient =
Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false)
binding.searchResultProfilePicture.update(recipient)
val nameString = model.contact.getSearchName()
binding.searchResultTitle.text = getHighlight(query, nameString)
}
fun ContentView.bindModel(model: SavedMessages) {
binding.searchResultSubtitle.isVisible = false
binding.searchResultTimestamp.isVisible = false
binding.searchResultTitle.setText(R.string.note_to_self)
binding.searchResultProfilePicture.isVisible = false
binding.searchResultSavedMessages.isVisible = true
}
fun ContentView.bindModel(query: String?, model: Message) {
binding.searchResultProfilePicture.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultTimestamp.isVisible = true
// val hasUnreads = model.unread > 0
// binding.unreadCountIndicator.isVisible = hasUnreads
// if (hasUnreads) {
// binding.unreadCountTextView.text = model.unread.toString()
// }
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.receivedTimestampMs)
binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient)
val textSpannable = SpannableStringBuilder()
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
// group chat, bind
val text = "${model.messageResult.messageRecipient.getSearchName()}: "
textSpannable.append(text)
}
textSpannable.append(getHighlight(
query,
model.messageResult.bodySnippet
))
binding.searchResultSubtitle.text = textSpannable
binding.searchResultTitle.text = model.messageResult.conversationRecipient.toShortString()
binding.searchResultSubtitle.isVisible = true
}
fun Recipient.getSearchName(): String = name ?: address.serialize().let { address -> "${address.take(4)}...${address.takeLast(4)}" }
fun Contact.getSearchName(): String =
if (nickname.isNullOrEmpty()) name ?: "${sessionID.take(4)}...${sessionID.takeLast(4)}"
else "${name ?: "${sessionID.take(4)}...${sessionID.takeLast(4)}"} ($nickname)"

View File

@ -0,0 +1,88 @@
package org.thoughtcrime.securesms.home.search
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.TextView
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import network.loki.messenger.databinding.ViewGlobalSearchInputBinding
class GlobalSearchInputLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs),
View.OnFocusChangeListener,
View.OnClickListener,
TextWatcher, TextView.OnEditorActionListener {
var binding: ViewGlobalSearchInputBinding = ViewGlobalSearchInputBinding.inflate(LayoutInflater.from(context), this, true)
var listener: GlobalSearchInputLayoutListener? = null
private val _query = MutableStateFlow<CharSequence?>(null)
val query: StateFlow<CharSequence?> = _query
override fun onAttachedToWindow() {
super.onAttachedToWindow()
binding.searchInput.onFocusChangeListener = this
binding.searchInput.addTextChangedListener(this)
binding.searchInput.setOnEditorActionListener(this)
binding.searchCancel.setOnClickListener(this)
binding.searchClear.setOnClickListener(this)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
}
override fun onFocusChange(v: View?, hasFocus: Boolean) {
if (v === binding.searchInput) {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0)
listener?.onInputFocusChanged(hasFocus)
}
}
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (v === binding.searchInput && actionId == EditorInfo.IME_ACTION_SEARCH) {
binding.searchInput.clearFocus()
return true
}
return false
}
override fun onClick(v: View?) {
if (v === binding.searchCancel) {
clearSearch(true)
} else if (v === binding.searchClear) {
clearSearch(false)
}
}
fun clearSearch(clearFocus: Boolean) {
binding.searchInput.text = null
if (clearFocus) {
binding.searchInput.clearFocus()
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
_query.value = s?.toString()
}
interface GlobalSearchInputLayoutListener {
fun onInputFocusChanged(hasFocus: Boolean)
}
}

View File

@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.home.search
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.GroupRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.search.model.MessageResult
import org.thoughtcrime.securesms.search.model.SearchResult
data class GlobalSearchResult(
val query: String,
val contacts: List<Contact>,
val threads: List<GroupRecord>,
val messages: List<MessageResult>
) {
val isEmpty: Boolean
get() = contacts.isEmpty() && threads.isEmpty() && messages.isEmpty()
companion object {
val EMPTY = GlobalSearchResult("", emptyList(), emptyList(), emptyList())
const val SEARCH_LIMIT = 5
fun from(searchResult: SearchResult): GlobalSearchResult {
val query = searchResult.query
val contactList = searchResult.contacts.toList()
val threads = searchResult.conversations.toList()
val messages = searchResult.messages.toList()
searchResult.close()
return GlobalSearchResult(query, contactList, threads, messages)
}
}
}

View File

@ -0,0 +1,69 @@
package org.thoughtcrime.securesms.home.search
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.plus
import org.session.libsignal.utilities.SettableFuture
import org.thoughtcrime.securesms.search.SearchRepository
import org.thoughtcrime.securesms.search.model.SearchResult
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltViewModel
class GlobalSearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() {
private val executor = viewModelScope + SupervisorJob()
private val _result: MutableStateFlow<GlobalSearchResult> =
MutableStateFlow(GlobalSearchResult.EMPTY)
val result: StateFlow<GlobalSearchResult> = _result
private val _queryText: MutableStateFlow<CharSequence> = MutableStateFlow("")
fun postQuery(charSequence: CharSequence?) {
charSequence ?: return
_queryText.value = charSequence
}
init {
//
_queryText
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
.mapLatest { query ->
if (query.trim().length < 2) {
SearchResult.EMPTY
} else {
// user input delay here in case we get a new query within a few hundred ms
// this coroutine will be cancelled and expensive query will not be run if typing quickly
// first query of 2 characters will be instant however
delay(300)
val settableFuture = SettableFuture<SearchResult>()
searchRepository.query(query.toString(), settableFuture::set)
try {
// search repository doesn't play nicely with suspend functions (yet)
settableFuture.get(10_000, TimeUnit.MILLISECONDS)
} catch (e: Exception) {
SearchResult.EMPTY
}
}
}
.onEach { result ->
// update the latest _result value
_result.value = GlobalSearchResult.from(result)
}
.launchIn(executor)
}
}

View File

@ -1,7 +1,7 @@
package org.thoughtcrime.securesms.mediasend;
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,229 @@
package org.thoughtcrime.securesms.repository
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.database.LokiMessageDatabase
import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
interface ConversationRepository {
fun isOxenHostedOpenGroup(threadId: Long): Boolean
fun getRecipientForThreadId(threadId: Long): Recipient
fun saveDraft(threadId: Long, text: String)
fun getDraft(threadId: Long): String?
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
fun unblock(recipient: Recipient)
fun deleteLocally(recipient: Recipient, message: MessageRecord)
suspend fun deleteForEveryone(
threadId: Long,
recipient: Recipient,
message: MessageRecord
): ResultOf<Unit>
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
suspend fun deleteMessageWithoutUnsendRequest(
threadId: Long,
messages: Set<MessageRecord>
): ResultOf<Unit>
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit>
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
}
class DefaultConversationRepository @Inject constructor(
private val textSecurePreferences: TextSecurePreferences,
private val messageDataProvider: MessageDataProvider,
private val threadDb: ThreadDatabase,
private val draftDb: DraftDatabase,
private val lokiThreadDb: LokiThreadDatabase,
private val smsDb: SmsDatabase,
private val mmsDb: MmsDatabase,
private val recipientDb: RecipientDatabase,
private val lokiMessageDb: LokiMessageDatabase
) : ConversationRepository {
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
return openGroup?.room == "session" || openGroup?.room == "oxen"
|| openGroup?.room == "lokinet" || openGroup?.room == "crypto"
}
override fun getRecipientForThreadId(threadId: Long): Recipient {
return threadDb.getRecipientForThreadId(threadId)!!
}
override fun saveDraft(threadId: Long, text: String) {
if (text.isEmpty()) return
val drafts = DraftDatabase.Drafts()
drafts.add(DraftDatabase.Draft(DraftDatabase.Draft.TEXT, text))
draftDb.insertDrafts(threadId, drafts)
}
override fun getDraft(threadId: Long): String? {
val drafts = draftDb.getDrafts(threadId)
draftDb.clearDrafts(threadId)
return drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value
}
override fun inviteContacts(threadId: Long, contacts: List<Recipient>) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
for (contact in contacts) {
val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis()
val openGroupInvitation = OpenGroupInvitation()
openGroupInvitation.name = openGroup.name
openGroupInvitation.url = openGroup.joinURL
message.openGroupInvitation = openGroupInvitation
val outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation(
openGroupInvitation,
contact,
message.sentTimestamp
)
smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!)
MessageSender.send(message, contact.address)
}
}
override fun unblock(recipient: Recipient) {
recipientDb.setBlocked(recipient, false)
}
override fun deleteLocally(recipient: Recipient, message: MessageRecord) {
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
textSecurePreferences.getLocalNumber()?.let {
MessageSender.send(unsendRequest, Address.fromSerialized(it))
}
}
messageDataProvider.deleteMessage(message.id, !message.isMms)
}
override suspend fun deleteForEveryone(
threadId: Long,
recipient: Recipient,
message: MessageRecord
): ResultOf<Unit> = suspendCoroutine { continuation ->
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
MessageSender.send(unsendRequest, recipient.address)
}
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
if (openGroup != null) {
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success {
messageDataProvider.deleteMessage(message.id, !message.isMms)
continuation.resume(ResultOf.Success(Unit))
}.fail { error ->
continuation.resumeWithException(error)
}
}
} else {
messageDataProvider.deleteMessage(message.id, !message.isMms)
messageDataProvider.getServerHashForMessage(message.id)?.let { serverHash ->
var publicKey = recipient.address.serialize()
if (recipient.isClosedGroupRecipient) {
publicKey = GroupUtil.doubleDecodeGroupID(publicKey).toHexString()
}
SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
.success {
continuation.resume(ResultOf.Success(Unit))
}.fail { error ->
continuation.resumeWithException(error)
}
}
}
}
override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? {
if (recipient.isOpenGroupRecipient) return null
messageDataProvider.getServerHashForMessage(message.id) ?: return null
val unsendRequest = UnsendRequest()
if (message.isOutgoing) {
unsendRequest.author = textSecurePreferences.getLocalNumber()
} else {
unsendRequest.author = message.individualRecipient.address.contactIdentifier()
}
unsendRequest.timestamp = message.timestamp
return unsendRequest
}
override suspend fun deleteMessageWithoutUnsendRequest(
threadId: Long,
messages: Set<MessageRecord>
): ResultOf<Unit> = suspendCoroutine { continuation ->
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
if (openGroup != null) {
val messageServerIDs = mutableMapOf<Long, MessageRecord>()
for (message in messages) {
val messageServerID =
lokiMessageDb.getServerID(message.id, !message.isMms) ?: continue
messageServerIDs[messageServerID] = message
}
for ((messageServerID, message) in messageServerIDs) {
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success {
messageDataProvider.deleteMessage(message.id, !message.isMms)
}.fail { error ->
continuation.resumeWithException(error)
}
}
} else {
for (message in messages) {
if (message.isMms) {
mmsDb.deleteMessage(message.id)
} else {
smsDb.deleteMessage(message.id)
}
}
}
continuation.resume(ResultOf.Success(Unit))
}
override suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit> =
suspendCoroutine { continuation ->
val sessionID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server)
.success {
continuation.resume(ResultOf.Success(Unit))
}.fail { error ->
continuation.resumeWithException(error)
}
}
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> =
suspendCoroutine { continuation ->
val sessionID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
OpenGroupAPIV2.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
.success {
continuation.resume(ResultOf.Success(Unit))
}.fail { error ->
continuation.resumeWithException(error)
}
}
}

View File

@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.repository
import kotlinx.coroutines.CancellationException
sealed class ResultOf<out T> {
data class Success<out R>(val value: R) : ResultOf<R>()
data class Failure(val throwable: Throwable) : ResultOf<Nothing>()
inline fun onFailure(block: (throwable: Throwable) -> Unit) = this.also {
if (this is Failure) {
block(throwable)
}
}
inline fun onSuccess(block: (value: T) -> Unit) = this.also {
if (this is Success) {
block(value)
}
}
inline fun <R> flatMap(mapper: (T) -> R): ResultOf<R> = when (this) {
is Success -> wrap { mapper(value) }
is Failure -> Failure(throwable)
}
fun getOrThrow(): T = when (this) {
is Success -> value
is Failure -> throw throwable
}
companion object {
inline fun <T> wrap(block: () -> T): ResultOf<T> =
try {
Success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Failure(e)
}
}
}

View File

@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.search
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
import dagger.hilt.android.scopes.ViewModelScoped
import org.session.libsession.utilities.concurrent.SignalExecutors
import org.thoughtcrime.securesms.contacts.ContactAccessor
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.SearchDatabase
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
@Module
@InstallIn(ViewModelComponent::class)
object SearchModule {
@Provides
@ViewModelScoped
fun provideSearchRepository(@ApplicationContext context: Context,
searchDatabase: SearchDatabase,
threadDatabase: ThreadDatabase,
groupDatabase: GroupDatabase,
contactDatabase: SessionContactDatabase) =
SearchRepository(context, searchDatabase, threadDatabase, groupDatabase, contactDatabase, ContactAccessor.getInstance(), SignalExecutors.SERIAL)
}

View File

@ -3,29 +3,39 @@ package org.thoughtcrime.securesms.search;
import android.content.Context;
import android.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);

View File

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