refactor: Use view binding to replace Kotlin synthetics (#824)

* refactor: Migrate home screen to data binding

* Add view binding

* Migrate ConversationView to view binding

* Migrate ConversationActivityV2 to view binding

* View model refactor

* Move more functionality to the view model

* Add ui state events flow

* Update conversation item bindings

* Update profile picture view bindings

* Replace Kotlin synthetics with view bindings

* Fix qr code fragment binding and optimize imports

* View binding refactors

* Make TextSecurePreferences an interface and add an implementation to improve testability

* Add conversation repository

* Migrate remaining TextSecurePreferences functions into the interface

* Add unit conversation unit tests

* Add unit test coverage for remaining view model functions
This commit is contained in:
ceokot 2022-01-14 07:56:15 +02:00 committed by GitHub
parent 366b5abdc8
commit c113a447cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 3579 additions and 2365 deletions

View File

@ -4,18 +4,17 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.android.tools.build:gradle:4.2.2'
classpath files('libs/gradle-witness.jar') classpath files('libs/gradle-witness.jar')
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
classpath "com.google.gms:google-services:4.3.3" classpath "com.google.gms:google-services:4.3.10"
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
} }
} }
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'witness' apply plugin: 'witness'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
@ -32,16 +31,16 @@ dependencies {
implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.2.0' implementation 'androidx.exifinterface:exifinterface:1.3.3'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
implementation 'androidx.activity:activity-ktx:1.2.2' implementation 'androidx.activity:activity-ktx:1.2.2'
implementation 'androidx.fragment:fragment-ktx:1.3.2' implementation 'androidx.fragment:fragment-ktx:1.3.2'
implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.core:core-ktx:1.3.2"
@ -62,9 +61,9 @@ dependencies {
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5' implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
implementation 'commons-net:commons-net:3.7.2' implementation 'commons-net:commons-net:3.7.2'
implementation 'com.github.chrisbanes:PhotoView:2.1.3' implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation "com.github.bumptech.glide:glide:$glideVersion"
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
kapt 'com.github.bumptech.glide:compiler:4.11.0' kapt "com.github.bumptech.glide:compiler:$glideVersion"
implementation 'com.makeramen:roundedimageview:2.1.0' implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.pnikosis:materialish-progress:1.5' implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0' implementation 'org.greenrobot:eventbus:3.0.0'
@ -72,8 +71,8 @@ dependencies {
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
implementation 'com.melnykov:floatingactionbutton:1.3.0' implementation 'com.melnykov:floatingactionbutton:1.3.0'
implementation 'com.google.zxing:android-integration:3.1.0' implementation 'com.google.zxing:android-integration:3.1.0'
implementation "com.google.dagger:hilt-android:2.38.1" implementation "com.google.dagger:hilt-android:$daggerVersion"
kapt "com.google.dagger:hilt-compiler:2.38.1" kapt "com.google.dagger:hilt-compiler:$daggerVersion"
implementation 'mobi.upod:time-duration-picker:1.1.3' implementation 'mobi.upod:time-duration-picker:1.1.3'
implementation 'com.google.zxing:core:3.2.1' implementation 'com.google.zxing:core:3.2.1'
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') { implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
@ -103,7 +102,7 @@ dependencies {
} }
implementation project(":libsignal") implementation project(":libsignal")
implementation project(":libsession") implementation project(":libsession")
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
implementation "org.whispersystems:curve25519-java:$curve25519Version" implementation "org.whispersystems:curve25519-java:$curve25519Version"
implementation 'com.goterl:lazysodium-android:5.0.2@aar' implementation 'com.goterl:lazysodium-android:5.0.2@aar'
implementation "net.java.dev.jna:jna:5.8.0@aar" implementation "net.java.dev.jna:jna:5.8.0@aar"
@ -111,7 +110,7 @@ dependencies {
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion" implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
implementation "com.github.lelloman:android-identicons:v11" implementation "com.github.lelloman:android-identicons:v11"
@ -122,12 +121,15 @@ dependencies {
implementation "com.opencsv:opencsv:4.6" implementation "com.opencsv:opencsv:4.6"
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.11.1' testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation 'org.mockito:mockito-core:1.10.8' testImplementation "org.mockito:mockito-inline:4.0.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation 'org.powermock:powermock-api-mockito:1.6.1' testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4:1.6.1' testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1' testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1' testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
testImplementation 'androidx.test:core:1.3.0' testImplementation 'androidx.test:core:1.3.0'
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Core library // Core library
androidTestImplementation 'androidx.test:core:1.4.0' androidTestImplementation 'androidx.test:core:1.4.0'
@ -231,6 +233,12 @@ android {
} }
} }
sourceSets {
String sharedTestDir = 'src/sharedTest/java'
test.java.srcDirs += sharedTestDir
androidTest.java.srcDirs += sharedTestDir
}
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
@ -279,6 +287,7 @@ android {
buildFeatures { buildFeatures {
dataBinding true dataBinding true
viewBinding true
} }
} }

View File

@ -7,13 +7,14 @@ import android.graphics.Path
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.RelativeLayout import android.widget.RelativeLayout
import kotlinx.android.synthetic.main.view_separator.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewSeparatorBinding
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
class LabeledSeparatorView : RelativeLayout { class LabeledSeparatorView : RelativeLayout {
private lateinit var binding: ViewSeparatorBinding
private val path = Path() private val path = Path()
private val paint: Paint by lazy { private val paint: Paint by lazy {
@ -43,10 +44,9 @@ class LabeledSeparatorView : RelativeLayout {
} }
private fun setUpViewHierarchy() { private fun setUpViewHierarchy() {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context))
val contentView = inflater.inflate(R.layout.view_separator, null)
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(contentView, layoutParams) addView(binding.root, layoutParams)
setWillNotDraw(false) setWillNotDraw(false)
} }
// endregion // endregion
@ -59,9 +59,9 @@ class LabeledSeparatorView : RelativeLayout {
val hMargin = toPx(16, resources).toFloat() val hMargin = toPx(16, resources).toFloat()
path.reset() path.reset()
path.moveTo(0.0f, h / 2) path.moveTo(0.0f, h / 2)
path.lineTo(titleTextView.left - hMargin, h / 2) path.lineTo(binding.titleTextView.left - hMargin, h / 2)
path.addRoundRect(titleTextView.left - hMargin, toPx(1, resources).toFloat(), titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW) path.addRoundRect(binding.titleTextView.left - hMargin, toPx(1, resources).toFloat(), binding.titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
path.moveTo(titleTextView.right + hMargin, h / 2) path.moveTo(binding.titleTextView.right + hMargin, h / 2)
path.lineTo(w, h / 2) path.lineTo(w, h / 2)
path.close() path.close()
c.drawPath(path, paint) c.drawPath(path, paint)

View File

@ -8,8 +8,8 @@ import android.widget.ImageView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.annotation.DimenRes import androidx.annotation.DimenRes
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import kotlinx.android.synthetic.main.view_profile_picture.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewProfilePictureBinding
import org.session.libsession.avatars.ProfileContactPhoto import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
class ProfilePictureView : RelativeLayout { class ProfilePictureView : RelativeLayout {
private lateinit var binding: ViewProfilePictureBinding
lateinit var glide: GlideRequests lateinit var glide: GlideRequests
var publicKey: String? = null var publicKey: String? = null
var displayName: String? = null var displayName: String? = null
@ -35,14 +36,12 @@ class ProfilePictureView : RelativeLayout {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() }
private fun initialize() { private fun initialize() {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this, true)
val contentView = inflater.inflate(R.layout.view_profile_picture, null)
addView(contentView)
} }
// endregion // endregion
// region Updating // region Updating
fun update(recipient: Recipient, threadID: Long) { fun update(recipient: Recipient) {
fun getUserDisplayName(publicKey: String): String { fun getUserDisplayName(publicKey: String): String {
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey) val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
@ -75,27 +74,27 @@ class ProfilePictureView : RelativeLayout {
val publicKey = publicKey ?: return val publicKey = publicKey ?: return
val additionalPublicKey = additionalPublicKey val additionalPublicKey = additionalPublicKey
if (additionalPublicKey != null) { if (additionalPublicKey != null) {
setProfilePictureIfNeeded(doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size) setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size) setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
doubleModeImageViewContainer.visibility = View.VISIBLE binding.doubleModeImageViewContainer.visibility = View.VISIBLE
} else { } else {
glide.clear(doubleModeImageView1) glide.clear(binding.doubleModeImageView1)
glide.clear(doubleModeImageView2) glide.clear(binding.doubleModeImageView2)
doubleModeImageViewContainer.visibility = View.INVISIBLE binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
} }
if (additionalPublicKey == null && !isLarge) { if (additionalPublicKey == null && !isLarge) {
setProfilePictureIfNeeded(singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size) setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size)
singleModeImageView.visibility = View.VISIBLE binding.singleModeImageView.visibility = View.VISIBLE
} else { } else {
glide.clear(singleModeImageView) glide.clear(binding.singleModeImageView)
singleModeImageView.visibility = View.INVISIBLE binding.singleModeImageView.visibility = View.INVISIBLE
} }
if (additionalPublicKey == null && isLarge) { if (additionalPublicKey == null && isLarge) {
setProfilePictureIfNeeded(largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size) setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size)
largeSingleModeImageView.visibility = View.VISIBLE binding.largeSingleModeImageView.visibility = View.VISIBLE
} else { } else {
glide.clear(largeSingleModeImageView) glide.clear(binding.largeSingleModeImageView)
largeSingleModeImageView.visibility = View.INVISIBLE binding.largeSingleModeImageView.visibility = View.INVISIBLE
} }
} }

View File

@ -1,15 +1,12 @@
package org.thoughtcrime.securesms.contacts package org.thoughtcrime.securesms.contacts
import android.content.Context import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.contact_selection_list_divider.view.* import androidx.recyclerview.widget.RecyclerView
import network.loki.messenger.R import network.loki.messenger.databinding.ContactSelectionListDividerBinding
import org.thoughtcrime.securesms.contacts.UserView
import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.mms.GlideRequests
class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
lateinit var glide: GlideRequests lateinit var glide: GlideRequests
@ -24,7 +21,15 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
} }
class UserViewHolder(val view: UserView) : RecyclerView.ViewHolder(view) class UserViewHolder(val view: UserView) : RecyclerView.ViewHolder(view)
class DividerViewHolder(val view: View) : RecyclerView.ViewHolder(view) class DividerViewHolder(
private val binding: ContactSelectionListDividerBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ContactSelectionListItem.Header) {
with(binding){
label.text = item.name
}
}
}
override fun getItemCount(): Int { override fun getItemCount(): Int {
return items.size return items.size
@ -41,8 +46,9 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
return if (viewType == ViewType.Contact) { return if (viewType == ViewType.Contact) {
UserViewHolder(UserView(context)) UserViewHolder(UserView(context))
} else { } else {
val view = LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false) DividerViewHolder(
DividerViewHolder(view) ContactSelectionListDividerBinding.inflate(LayoutInflater.from(context), parent, false)
)
} }
} }
@ -58,8 +64,7 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
if (multiSelect) UserView.ActionIndicator.Tick else UserView.ActionIndicator.None, if (multiSelect) UserView.ActionIndicator.Tick else UserView.ActionIndicator.None,
isSelected) isSelected)
} else if (viewHolder is DividerViewHolder) { } else if (viewHolder is DividerViewHolder) {
item as ContactSelectionListItem.Header viewHolder.bind(item as ContactSelectionListItem.Header)
viewHolder.view.label.text = item.name
} }
} }

View File

@ -1,23 +1,21 @@
package org.thoughtcrime.securesms.contacts package org.thoughtcrime.securesms.contacts
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import androidx.recyclerview.widget.LinearLayoutManager
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.contact_selection_list_fragment.* import androidx.fragment.app.Fragment
import network.loki.messenger.R import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import network.loki.messenger.databinding.ContactSelectionListFragmentBinding
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem
import org.thoughtcrime.securesms.contacts.ContactSelectionListLoader
class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener { class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener {
private lateinit var binding: ContactSelectionListFragmentBinding
private var cursorFilter: String? = null private var cursorFilter: String? = null
var onContactSelectedListener: OnContactSelectedListener? = null var onContactSelectedListener: OnContactSelectedListener? = null
@ -46,20 +44,21 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
fun onContactDeselected(number: String?) fun onContactDeselected(number: String?)
} }
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recyclerView.layoutManager = LinearLayoutManager(activity)
recyclerView.adapter = listAdapter
swipeRefreshLayout.isEnabled = requireActivity().intent.getBooleanExtra(REFRESHABLE, true)
}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
LoaderManager.getInstance(this).initLoader(0, null, this) LoaderManager.getInstance(this).initLoader(0, null, this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.contact_selection_list_fragment, container, false) binding = ContactSelectionListFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.layoutManager = LinearLayoutManager(activity)
binding.recyclerView.adapter = listAdapter
binding.swipeRefreshLayout.isEnabled = requireActivity().intent.getBooleanExtra(REFRESHABLE, true)
} }
override fun onStop() { override fun onStop() {
@ -74,15 +73,15 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
fun resetQueryFilter() { fun resetQueryFilter() {
setQueryFilter(null) setQueryFilter(null)
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
} }
fun setRefreshing(refreshing: Boolean) { fun setRefreshing(refreshing: Boolean) {
swipeRefreshLayout.isRefreshing = refreshing binding.swipeRefreshLayout.isRefreshing = refreshing
} }
fun setOnRefreshListener(onRefreshListener: OnRefreshListener?) { fun setOnRefreshListener(onRefreshListener: OnRefreshListener?) {
swipeRefreshLayout.setOnRefreshListener(onRefreshListener) binding.swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
} }
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> { override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> {
@ -107,8 +106,8 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
return return
} }
listAdapter.items = items listAdapter.items = items
mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE binding.mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE binding.emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
} }
override fun onContactClick(contact: Recipient) { override fun onContactClick(contact: Recipient) {

View File

@ -9,16 +9,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.activity_create_closed_group.emptyStateContainer
import kotlinx.android.synthetic.main.activity_create_closed_group.mainContentContainer
import kotlinx.android.synthetic.main.activity_select_contacts.*
import kotlinx.android.synthetic.main.activity_select_contacts.recyclerView
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySelectContactsBinding
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
//TODO Refactor to avoid using kotlinx.android.synthetic
class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> { class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
private lateinit var binding: ActivitySelectContactsBinding
private var members = listOf<String>() private var members = listOf<String>()
set(value) { field = value; selectContactsAdapter.members = value } set(value) { field = value; selectContactsAdapter.members = value }
private lateinit var usersToExclude: Set<String> private lateinit var usersToExclude: Set<String>
@ -36,18 +33,18 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivitySelectContactsBinding.inflate(layoutInflater)
setContentView(R.layout.activity_select_contacts) setContentView(binding.root)
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title) supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf() usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
val emptyStateText = intent.getStringExtra(emptyStateTextKey) val emptyStateText = intent.getStringExtra(emptyStateTextKey)
if (emptyStateText != null) { if (emptyStateText != null) {
emptyStateMessageTextView.text = emptyStateText binding.emptyStateMessageTextView.text = emptyStateText
} }
recyclerView.adapter = selectContactsAdapter binding.recyclerView.adapter = selectContactsAdapter
recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
LoaderManager.getInstance(this).initLoader(0, null, this) LoaderManager.getInstance(this).initLoader(0, null, this)
} }
@ -73,8 +70,8 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
private fun update(members: List<String>) { private fun update(members: List<String>) {
this.members = members this.members = members
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
invalidateOptionsMenu() invalidateOptionsMenu()
} }
// endregion // endregion

View File

@ -5,9 +5,8 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_conversation.view.profilePictureView
import kotlinx.android.synthetic.main.view_user.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewUserBinding
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
@ -15,6 +14,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
class UserView : LinearLayout { class UserView : LinearLayout {
private lateinit var binding: ViewUserBinding
var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly
enum class ActionIndicator { enum class ActionIndicator {
@ -41,9 +41,7 @@ class UserView : LinearLayout {
} }
private fun setUpViewHierarchy() { private fun setUpViewHierarchy() {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater binding = ViewUserBinding.inflate(LayoutInflater.from(context), this, true)
val contentView = inflater.inflate(R.layout.view_user, null)
addView(contentView)
} }
// endregion // endregion
@ -56,28 +54,32 @@ class UserView : LinearLayout {
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user) val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
val address = user.address.serialize() val address = user.address.serialize()
profilePictureView.glide = glide binding.profilePictureView.glide = glide
profilePictureView.update(user, threadID) binding.profilePictureView.update(user)
actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24) binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address) binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
when (actionIndicator) { when (actionIndicator) {
ActionIndicator.None -> { ActionIndicator.None -> {
actionIndicatorImageView.visibility = View.GONE binding.actionIndicatorImageView.visibility = View.GONE
} }
ActionIndicator.Menu -> { ActionIndicator.Menu -> {
actionIndicatorImageView.visibility = View.VISIBLE binding.actionIndicatorImageView.visibility = View.VISIBLE
actionIndicatorImageView.setImageResource(R.drawable.ic_more_horiz_white) binding.actionIndicatorImageView.setImageResource(R.drawable.ic_more_horiz_white)
} }
ActionIndicator.Tick -> { ActionIndicator.Tick -> {
actionIndicatorImageView.visibility = View.VISIBLE binding.actionIndicatorImageView.visibility = View.VISIBLE
actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle) binding.actionIndicatorImageView.setImageResource(
if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle
)
} }
} }
} }
fun toggleCheckbox(isSelected: Boolean = false) { fun toggleCheckbox(isSelected: Boolean = false) {
actionIndicatorImageView.visibility = View.VISIBLE binding.actionIndicatorImageView.visibility = View.VISIBLE
actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle) binding.actionIndicatorImageView.setImageResource(
if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle
)
} }
fun unbind() { fun unbind() {

View File

@ -4,9 +4,7 @@ import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import kotlinx.android.synthetic.main.view_visible_message.view.*
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
@ -49,15 +47,9 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val viewType = ViewType.allValues[viewType] val viewType = ViewType.allValues[viewType]
when (viewType) { return when (viewType) {
ViewType.Visible -> { ViewType.Visible -> VisibleMessageViewHolder(VisibleMessageView(context))
val view = VisibleMessageView(context) ViewType.Control -> ControlMessageViewHolder(ControlMessageView(context))
return VisibleMessageViewHolder(view)
}
ViewType.Control -> {
val view = ControlMessageView(context)
return ControlMessageViewHolder(view)
}
else -> throw IllegalStateException("Unexpected view type: $viewType.") else -> throw IllegalStateException("Unexpected view type: $viewType.")
} }
} }
@ -71,7 +63,6 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
val view = viewHolder.view val view = viewHolder.view
val isSelected = selectedItems.contains(message) val isSelected = selectedItems.contains(message)
view.snIsSelected = isSelected view.snIsSelected = isSelected
view.messageTimestampTextView.isVisible = isSelected
view.indexInAdapter = position view.indexInAdapter = position
view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery) view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery)
if (!message.isDeleted) { if (!message.isDeleted) {

View File

@ -2,16 +2,12 @@ package org.thoughtcrime.securesms.conversation.v2
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent import android.view.MotionEvent
import android.view.VelocityTracker import android.view.VelocityTracker
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.activity_conversation_v2.*
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max
class ConversationRecyclerView : RecyclerView { class ConversationRecyclerView : RecyclerView {
private val maxLongPressVelocityY = toPx(10, resources) private val maxLongPressVelocityY = toPx(10, resources)
@ -37,10 +33,10 @@ class ConversationRecyclerView : RecyclerView {
if (abs(vy) > maxLongPressVelocityY && abs(vx) < minSwipeVelocityX) { return super.onInterceptTouchEvent(e) } if (abs(vy) > maxLongPressVelocityY && abs(vx) < minSwipeVelocityX) { return super.onInterceptTouchEvent(e) }
// Return false if abs(v.x) > abs(v.y) so that only swipes that are more horizontal than vertical // Return false if abs(v.x) > abs(v.y) so that only swipes that are more horizontal than vertical
// get passed on to the message view // get passed on to the message view
if (abs(vx) > abs(vy)) { return if (abs(vx) > abs(vy)) {
return false false
} else { } else {
return super.onInterceptTouchEvent(e) super.onInterceptTouchEvent(e)
} }
} }

View File

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

View File

@ -7,8 +7,8 @@ import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_delete_message_bottom_sheet.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentDeleteMessageBottomSheetBinding
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.SessionContactDatabase import org.thoughtcrime.securesms.database.SessionContactDatabase
@ -22,6 +22,7 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
lateinit var contactDatabase: SessionContactDatabase lateinit var contactDatabase: SessionContactDatabase
lateinit var recipient: Recipient lateinit var recipient: Recipient
private lateinit var binding: FragmentDeleteMessageBottomSheetBinding
val contact by lazy { val contact by lazy {
val senderId = recipient.address.serialize() val senderId = recipient.address.serialize()
// this dialog won't show for open group contacts // this dialog won't show for open group contacts
@ -37,15 +38,16 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
return inflater.inflate(R.layout.fragment_delete_message_bottom_sheet, container, false) binding = FragmentDeleteMessageBottomSheetBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onClick(v: View?) { override fun onClick(v: View?) {
when (v) { when (v) {
deleteForMeTextView -> onDeleteForMeTapped?.invoke() binding.deleteForMeTextView -> onDeleteForMeTapped?.invoke()
deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke() binding.deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke()
cancelTextView -> onCancelTapped?.invoke() binding.cancelTextView -> onCancelTapped?.invoke()
} }
} }
@ -55,13 +57,13 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
return dismiss() return dismiss()
} }
if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) { if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) {
deleteForEveryoneTextView.text = binding.deleteForEveryoneTextView.text =
resources.getString(R.string.delete_message_for_me_and_recipient, contact) resources.getString(R.string.delete_message_for_me_and_recipient, contact)
} }
deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient binding.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
deleteForMeTextView.setOnClickListener(this) binding.deleteForMeTextView.setOnClickListener(this)
deleteForEveryoneTextView.setOnClickListener(this) binding.deleteForEveryoneTextView.setOnClickListener(this)
cancelTextView.setOnClickListener(this) binding.cancelTextView.setOnClickListener(this)
} }
override fun onStart() { override fun onStart() {

View File

@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.conversation.v2
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.activity_message_detail.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityMessageDetailBinding
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
@ -13,11 +13,11 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Date
import java.util.Locale
class MessageDetailActivity: PassphraseRequiredActionBarActivity() { class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
private lateinit var binding: ActivityMessageDetailBinding
var messageRecord: MessageRecord? = null var messageRecord: MessageRecord? = null
// region Settings // region Settings
@ -29,7 +29,8 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready) super.onCreate(savedInstanceState, ready)
setContentView(R.layout.activity_message_detail) binding = ActivityMessageDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
title = resources.getString(R.string.conversation_context__menu_message_details) title = resources.getString(R.string.conversation_context__menu_message_details)
val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
// We only show this screen for messages fail to send, // We only show this screen for messages fail to send,
@ -37,7 +38,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!) val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author)
updateContent() updateContent()
resend_button.setOnClickListener { binding.resendButton.setOnClickListener {
ResendMessageUtilities.resend(messageRecord!!) ResendMessageUtilities.resend(messageRecord!!)
finish() finish()
} }
@ -46,20 +47,20 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
fun updateContent() { fun updateContent() {
val dateLocale = Locale.getDefault() val dateLocale = Locale.getDefault()
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale) val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
sent_time.text = dateFormatter.format(Date(messageRecord!!.dateSent)) binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) ?: "Message failed to send." val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) ?: "Message failed to send."
error_message.text = errorMessage binding.errorMessage.text = errorMessage
if (messageRecord!!.getExpiresIn() <= 0 || messageRecord!!.getExpireStarted() <= 0) { if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
expires_container.visibility = View.GONE binding.expiresContainer.visibility = View.GONE
} else { } else {
expires_container.visibility = View.VISIBLE binding.expiresContainer.visibility = View.VISIBLE
val elapsed = System.currentTimeMillis() - messageRecord!!.expireStarted val elapsed = System.currentTimeMillis() - messageRecord!!.expireStarted
val remaining = messageRecord!!.expiresIn - elapsed val remaining = messageRecord!!.expiresIn - elapsed
val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1)) val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
expires_in.text = duration binding.expiresIn.text = duration
} }
} }
} }

View File

@ -15,14 +15,16 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.android.synthetic.main.fragment_modal_url_bottom_sheet.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding
import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.UiModeUtilities
class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener { class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener {
private lateinit var binding: FragmentModalUrlBottomSheetBinding
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_modal_url_bottom_sheet, container, false) override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View {
binding = FragmentModalUrlBottomSheetBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -31,10 +33,10 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
val spannable = SpannableStringBuilder(explanation) val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(url) val startIndex = explanation.indexOf(url)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
openURLExplanationTextView.text = spannable binding.openURLExplanationTextView.text = spannable
cancelButton.setOnClickListener(this) binding.cancelButton.setOnClickListener(this)
copyButton.setOnClickListener(this) binding.copyButton.setOnClickListener(this)
openURLButton.setOnClickListener(this) binding.openURLButton.setOnClickListener(this)
} }
private fun open() { private fun open() {
@ -64,9 +66,9 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
override fun onClick(v: View?) { override fun onClick(v: View?) {
when (v) { when (v) {
openURLButton -> open() binding.openURLButton -> open()
copyButton -> copy() binding.copyButton -> copy()
cancelButton -> dismiss() binding.cancelButton -> dismiss()
} }
} }
} }

View File

@ -11,8 +11,8 @@ import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.album_thumbnail_view.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
@ -32,6 +32,8 @@ import org.thoughtcrime.securesms.util.ActivityDispatcher
import kotlin.math.roundToInt import kotlin.math.roundToInt
class AlbumThumbnailView : FrameLayout { class AlbumThumbnailView : FrameLayout {
private lateinit var binding: AlbumThumbnailViewBinding
companion object { companion object {
const val MAX_ALBUM_DISPLAY_SIZE = 5 const val MAX_ALBUM_DISPLAY_SIZE = 5
@ -55,7 +57,7 @@ class AlbumThumbnailView : FrameLayout {
private var slideSize: Int = 0 private var slideSize: Int = 0
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this) binding = AlbumThumbnailViewBinding.inflate(LayoutInflater.from(context), this, true)
} }
override fun dispatchDraw(canvas: Canvas?) { override fun dispatchDraw(canvas: Canvas?) {
@ -73,7 +75,7 @@ class AlbumThumbnailView : FrameLayout {
// Z-check in specific order // Z-check in specific order
val testRect = Rect() val testRect = Rect()
// test "Read More" // test "Read More"
albumCellBodyTextReadMore.getGlobalVisibleRect(testRect) binding.albumCellBodyTextReadMore.getGlobalVisibleRect(testRect)
if (testRect.contains(eventRect)) { if (testRect.contains(eventRect)) {
// dispatch to activity view // dispatch to activity view
ActivityDispatcher.get(context)?.dispatchIntent { context -> ActivityDispatcher.get(context)?.dispatchIntent { context ->
@ -81,15 +83,15 @@ class AlbumThumbnailView : FrameLayout {
} }
return return
} }
val intersectedSpans = albumCellBodyText.getIntersectedModalSpans(eventRect) val intersectedSpans = binding.albumCellBodyText.getIntersectedModalSpans(eventRect)
if (intersectedSpans.isNotEmpty()) { if (intersectedSpans.isNotEmpty()) {
intersectedSpans.forEach { span -> intersectedSpans.forEach { span ->
span.onClick(albumCellBodyText) span.onClick(binding.albumCellBodyText)
} }
return return
} }
// test each album child // test each album child
albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
child.getGlobalVisibleRect(testRect) child.getGlobalVisibleRect(testRect)
if (testRect.contains(eventRect)) { if (testRect.contains(eventRect)) {
// hit intersects with this particular child // hit intersects with this particular child
@ -122,10 +124,10 @@ class AlbumThumbnailView : FrameLayout {
// recreate cell views if different size to what we have already (for recycling) // recreate cell views if different size to what we have already (for recycling)
if (slides.size != this.slideSize) { if (slides.size != this.slideSize) {
albumCellContainer.removeAllViews() binding.albumCellContainer.removeAllViews()
LayoutInflater.from(context).inflate(layoutRes(slides.size), albumCellContainer) LayoutInflater.from(context).inflate(layoutRes(slides.size), binding.albumCellContainer)
val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE
albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText -> binding.albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
// overflowText will be null if !overflowed // overflowText will be null if !overflowed
overflowText.isVisible = overflowed // more than max album size overflowText.isVisible = overflowed // more than max album size
overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE) overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE)
@ -137,17 +139,17 @@ class AlbumThumbnailView : FrameLayout {
val thumbnailView = getThumbnailView(position) val thumbnailView = getThumbnailView(position)
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message) thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
} }
albumCellBodyParent.isVisible = message.body.isNotEmpty() binding.albumCellBodyParent.isVisible = message.body.isNotEmpty()
val body = VisibleMessageContentView.getBodySpans(context, message, null) val body = VisibleMessageContentView.getBodySpans(context, message, null)
albumCellBodyText.text = body binding.albumCellBodyText.text = body
post { post {
// post to await layout of text // post to await layout of text
albumCellBodyText.layout?.let { layout -> binding.albumCellBodyText.layout?.let { layout ->
val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) } val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) }
?: 0 ?: 0
// show read more text if at least one line is ellipsized // 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()) ViewUtil.setPaddingTop(binding.albumCellBodyTextParent, if (maxEllipsis > 0) resources.getDimension(R.dimen.small_spacing).roundToInt() else resources.getDimension(R.dimen.medium_spacing).roundToInt())
albumCellBodyTextReadMore.isVisible = maxEllipsis > 0 binding.albumCellBodyTextReadMore.isVisible = maxEllipsis > 0
} }
} }
} }
@ -165,11 +167,11 @@ class AlbumThumbnailView : FrameLayout {
} }
fun getThumbnailView(position: Int): KThumbnailView = when (position) { fun getThumbnailView(position: Int): KThumbnailView = when (position) {
0 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1) 0 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
1 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2) 1 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
2 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3) 2 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
3 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4) 3 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
4 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5) 4 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position") else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position")
} }

View File

@ -5,14 +5,14 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_link_preview_draft.view.* import network.loki.messenger.databinding.ViewLinkPreviewDraftBinding
import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.thoughtcrime.securesms.util.toPx
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.util.toPx
class LinkPreviewDraftView : LinearLayout { class LinkPreviewDraftView : LinearLayout {
private lateinit var binding: ViewLinkPreviewDraftBinding
var delegate: LinkPreviewDraftViewDelegate? = null var delegate: LinkPreviewDraftViewDelegate? = null
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
@ -21,22 +21,22 @@ class LinkPreviewDraftView : LinearLayout {
private fun initialize() { private fun initialize() {
// Start out with the loader showing and the content view hidden // Start out with the loader showing and the content view hidden
LayoutInflater.from(context).inflate(R.layout.view_link_preview_draft, this) binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true)
linkPreviewDraftContainer.isVisible = false binding.linkPreviewDraftContainer.isVisible = false
thumbnailImageView.clipToOutline = true binding.thumbnailImageView.clipToOutline = true
linkPreviewDraftCancelButton.setOnClickListener { cancel() } binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
} }
fun update(glide: GlideRequests, linkPreview: LinkPreview) { fun update(glide: GlideRequests, linkPreview: LinkPreview) {
// Hide the loader and show the content view // Hide the loader and show the content view
linkPreviewDraftContainer.isVisible = true binding.linkPreviewDraftContainer.isVisible = true
linkPreviewDraftLoader.isVisible = false binding.linkPreviewDraftLoader.isVisible = false
thumbnailImageView.radius = toPx(4, resources) binding.thumbnailImageView.radius = toPx(4, resources)
if (linkPreview.getThumbnail().isPresent) { if (linkPreview.getThumbnail().isPresent) {
// This internally fetches the thumbnail // This internally fetches the thumbnail
thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false) binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false)
} }
linkPreviewDraftTitleTextView.text = linkPreview.title binding.linkPreviewDraftTitleTextView.text = linkPreview.title
} }
private fun cancel() { private fun cancel() {

View File

@ -45,7 +45,7 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
} }
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View { override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent) val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context)
val mentionCandidate = getItem(position) val mentionCandidate = getItem(position)
cell.glide = glide cell.glide = glide
cell.mentionCandidate = mentionCandidate cell.mentionCandidate = mentionCandidate

View File

@ -4,32 +4,29 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_mention_candidate.view.* import network.loki.messenger.databinding.ViewMentionCandidateBinding
import network.loki.messenger.R
import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { class MentionCandidateView : LinearLayout {
private lateinit var binding: ViewMentionCandidateBinding
var mentionCandidate = Mention("", "") var mentionCandidate = Mention("", "")
set(newValue) { field = newValue; update() } set(newValue) { field = newValue; update() }
var glide: GlideRequests? = null var glide: GlideRequests? = null
var openGroupServer: String? = null var openGroupServer: String? = null
var openGroupRoom: String? = null var openGroupRoom: String? = null
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null) constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
companion object { private fun initialize() {
binding = ViewMentionCandidateBinding.inflate(LayoutInflater.from(context), this, true)
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView {
return layoutInflater.inflate(R.layout.view_mention_candidate, parent, false) as MentionCandidateView
}
} }
private fun update() { private fun update() = with(binding) {
mentionCandidateNameTextView.text = mentionCandidate.displayName mentionCandidateNameTextView.text = mentionCandidate.displayName
profilePictureView.publicKey = mentionCandidate.publicKey profilePictureView.publicKey = mentionCandidate.publicKey
profilePictureView.displayName = mentionCandidate.displayName profilePictureView.displayName = mentionCandidate.displayName

View File

@ -5,8 +5,7 @@ import android.content.Intent
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.FrameLayout import android.widget.FrameLayout
import kotlinx.android.synthetic.main.view_open_group_guidelines.view.* import network.loki.messenger.databinding.ViewOpenGroupGuidelinesBinding
import network.loki.messenger.R
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity import org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
@ -18,13 +17,12 @@ class OpenGroupGuidelinesView : FrameLayout {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater ViewOpenGroupGuidelinesBinding.inflate(LayoutInflater.from(context), this, true).apply {
val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null) readButton.setOnClickListener {
addView(contentView) val activity = context as ConversationActivityV2
readButton.setOnClickListener { val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
val activity = context as ConversationActivityV2 activity.push(intent)
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java) }
activity.push(intent)
} }
} }
} }

View File

@ -4,22 +4,22 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_conversation_typing_container.view.* import network.loki.messenger.databinding.ViewConversationTypingContainerBinding
import network.loki.messenger.R
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
class TypingIndicatorViewContainer : LinearLayout { class TypingIndicatorViewContainer : LinearLayout {
private lateinit var binding: ViewConversationTypingContainerBinding
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_conversation_typing_container, this) binding = ViewConversationTypingContainerBinding.inflate(LayoutInflater.from(context), this, true)
} }
fun setTypists(typists: List<Recipient>) { fun setTypists(typists: List<Recipient>) {
if (typists.isEmpty()) { typingIndicator.stopAnimation(); return } if (typists.isEmpty()) { binding.typingIndicator.stopAnimation(); return }
typingIndicator.startAnimation() binding.typingIndicator.startAnimation()
} }
} }

View File

@ -6,8 +6,8 @@ import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_blocked.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.DialogBlockedBinding
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
@ -17,21 +17,21 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
class BlockedDialog(private val recipient: Recipient) : BaseDialog() { class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
override fun setContentView(builder: AlertDialog.Builder) { override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_blocked, null) val binding = DialogBlockedBinding.inflate(LayoutInflater.from(requireContext()))
val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase() val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
val sessionID = recipient.address.toString() val sessionID = recipient.address.toString()
val contact = contactDB.getContactWithSessionID(sessionID) val contact = contactDB.getContactWithSessionID(sessionID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
val title = resources.getString(R.string.dialog_blocked_title, name) val title = resources.getString(R.string.dialog_blocked_title, name)
contentView.blockedTitleTextView.text = title binding.blockedTitleTextView.text = title
val explanation = resources.getString(R.string.dialog_blocked_explanation, name) val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
val spannable = SpannableStringBuilder(explanation) val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name) val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
contentView.blockedExplanationTextView.text = spannable binding.blockedExplanationTextView.text = spannable
contentView.cancelButton.setOnClickListener { dismiss() } binding.cancelButton.setOnClickListener { dismiss() }
contentView.unblockButton.setOnClickListener { unblock() } binding.unblockButton.setOnClickListener { unblock() }
builder.setView(contentView) builder.setView(binding.root)
} }
private fun unblock() { private fun unblock() {

View File

@ -7,8 +7,8 @@ import android.text.style.StyleSpan
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.dialog_download.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.DialogDownloadBinding
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
@ -26,20 +26,20 @@ class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
@Inject lateinit var contactDB: SessionContactDatabase @Inject lateinit var contactDB: SessionContactDatabase
override fun setContentView(builder: AlertDialog.Builder) { override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_download, null) val binding = DialogDownloadBinding.inflate(LayoutInflater.from(requireContext()))
val sessionID = recipient.address.toString() val sessionID = recipient.address.toString()
val contact = contactDB.getContactWithSessionID(sessionID) val contact = contactDB.getContactWithSessionID(sessionID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
val title = resources.getString(R.string.dialog_download_title, name) val title = resources.getString(R.string.dialog_download_title, name)
contentView.downloadTitleTextView.text = title binding.downloadTitleTextView.text = title
val explanation = resources.getString(R.string.dialog_download_explanation, name) val explanation = resources.getString(R.string.dialog_download_explanation, name)
val spannable = SpannableStringBuilder(explanation) val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name) val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
contentView.downloadExplanationTextView.text = spannable binding.downloadExplanationTextView.text = spannable
contentView.cancelButton.setOnClickListener { dismiss() } binding.cancelButton.setOnClickListener { dismiss() }
contentView.downloadButton.setOnClickListener { trust() } binding.downloadButton.setOnClickListener { trust() }
builder.setView(contentView) builder.setView(binding.root)
} }
private fun trust() { private fun trust() {

View File

@ -7,8 +7,8 @@ import android.text.style.StyleSpan
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.DialogJoinOpenGroupBinding
import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
@ -19,17 +19,17 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() { class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() {
override fun setContentView(builder: AlertDialog.Builder) { override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_join_open_group, null) val binding = DialogJoinOpenGroupBinding.inflate(LayoutInflater.from(requireContext()))
val title = resources.getString(R.string.dialog_join_open_group_title, name) val title = resources.getString(R.string.dialog_join_open_group_title, name)
contentView.joinOpenGroupTitleTextView.text = title binding.joinOpenGroupTitleTextView.text = title
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name) val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
val spannable = SpannableStringBuilder(explanation) val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name) val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
contentView.joinOpenGroupExplanationTextView.text = spannable binding.joinOpenGroupExplanationTextView.text = spannable
contentView.cancelButton.setOnClickListener { dismiss() } binding.cancelButton.setOnClickListener { dismiss() }
contentView.joinButton.setOnClickListener { join() } binding.joinButton.setOnClickListener { join() }
builder.setView(contentView) builder.setView(binding.root)
} }
private fun join() { private fun join() {

View File

@ -2,8 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_link_preview.view.* import network.loki.messenger.databinding.DialogLinkPreviewBinding
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
@ -12,10 +11,10 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() { class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
override fun setContentView(builder: AlertDialog.Builder) { override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_link_preview, null) val binding = DialogLinkPreviewBinding.inflate(LayoutInflater.from(requireContext()))
contentView.cancelButton.setOnClickListener { dismiss() } binding.cancelButton.setOnClickListener { dismiss() }
contentView.enableLinkPreviewsButton.setOnClickListener { enable() } binding.enableLinkPreviewsButton.setOnClickListener { enable() }
builder.setView(contentView) builder.setView(binding.root)
} }
private fun enable() { private fun enable() {

View File

@ -2,18 +2,17 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_send_seed.view.* import network.loki.messenger.databinding.DialogSendSeedBinding
import network.loki.messenger.R
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
/** Shown if the user is about to send their recovery phrase to someone. */ /** Shown if the user is about to send their recovery phrase to someone. */
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() { class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() {
override fun setContentView(builder: AlertDialog.Builder) { override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_send_seed, null) val binding = DialogSendSeedBinding.inflate(LayoutInflater.from(requireContext()))
contentView.cancelButton.setOnClickListener { dismiss() } binding.cancelButton.setOnClickListener { dismiss() }
contentView.sendSeedButton.setOnClickListener { send() } binding.sendSeedButton.setOnClickListener { send() }
builder.setView(contentView) builder.setView(binding.root)
} }
private fun send() { private fun send() {

View File

@ -4,13 +4,14 @@ import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.net.Uri import android.net.Uri
import android.text.InputType import android.text.InputType
import android.text.TextWatcher
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_input_bar.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewInputBarBinding
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
@ -27,6 +28,7 @@ import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate { class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate {
private lateinit var binding: ViewInputBarBinding
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private val vMargin by lazy { toDp(4, resources) } private val vMargin by lazy { toDp(4, resources) }
private val minHeight by lazy { toPx(56, resources) } private val minHeight by lazy { toPx(56, resources) }
@ -39,8 +41,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
set(value) { field = value; showOrHideInputIfNeeded() } set(value) { field = value; showOrHideInputIfNeeded() }
var text: String var text: String
get() { return inputBarEditText.text?.toString() ?: "" } get() { return binding.inputBarEditText.text?.toString() ?: "" }
set(value) { inputBarEditText.setText(value) } set(value) { binding.inputBarEditText.setText(value) }
val attachmentButtonsContainerHeight: Int
get() = binding.attachmentsButtonContainer.height
private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) } private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) }
private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone) } private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone) }
@ -52,36 +57,36 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_input_bar, this) binding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true)
// Attachments button // Attachments button
attachmentsButtonContainer.addView(attachmentsButton) binding.attachmentsButtonContainer.addView(attachmentsButton)
attachmentsButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) attachmentsButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
attachmentsButton.onPress = { toggleAttachmentOptions() } attachmentsButton.onPress = { toggleAttachmentOptions() }
// Microphone button // Microphone button
microphoneOrSendButtonContainer.addView(microphoneButton) binding.microphoneOrSendButtonContainer.addView(microphoneButton)
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) microphoneButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
microphoneButton.onLongPress = { startRecordingVoiceMessage() } microphoneButton.onLongPress = { startRecordingVoiceMessage() }
microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) } microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) } microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) } microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }
// Send button // Send button
microphoneOrSendButtonContainer.addView(sendButton) binding.microphoneOrSendButtonContainer.addView(sendButton)
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) sendButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
sendButton.isVisible = false sendButton.isVisible = false
sendButton.onUp = { delegate?.sendMessage() } sendButton.onUp = { delegate?.sendMessage() }
// Edit text // Edit text
val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0 val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0
inputBarEditText.imeOptions = inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled binding.inputBarEditText.imeOptions = binding.inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled
inputBarEditText.inputType = inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES binding.inputBarEditText.inputType = binding.inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
inputBarEditText.delegate = this binding.inputBarEditText.delegate = this
} }
// endregion // endregion
// region General // region General
private fun setHeight(newHeight: Int) { private fun setHeight(newHeight: Int) {
val layoutParams = inputBarLinearLayout.layoutParams as LayoutParams val layoutParams = binding.inputBarLinearLayout.layoutParams as LayoutParams
layoutParams.height = newHeight layoutParams.height = newHeight
inputBarLinearLayout.layoutParams = layoutParams binding.inputBarLinearLayout.layoutParams = layoutParams
delegate?.inputBarHeightChanged(newHeight) delegate?.inputBarHeightChanged(newHeight)
} }
// endregion // endregion
@ -94,7 +99,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
} }
override fun inputBarEditTextHeightChanged(newValue: Int) { override fun inputBarEditTextHeightChanged(newValue: Int) {
val newHeight = max(newValue + 2 * vMargin, minHeight) + inputBarAdditionalContentContainer.height val newHeight = max(newValue + 2 * vMargin, minHeight) + binding.inputBarAdditionalContentContainer.height
setHeight(newHeight) setHeight(newHeight)
} }
@ -117,10 +122,10 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
quote = message quote = message
linkPreview = null linkPreview = null
linkPreviewDraftView = null linkPreviewDraftView = null
inputBarAdditionalContentContainer.removeAllViews() binding.inputBarAdditionalContentContainer.removeAllViews()
val quoteView = QuoteView(context, QuoteView.Mode.Draft) val quoteView = QuoteView(context, QuoteView.Mode.Draft)
quoteView.delegate = this quoteView.delegate = this
inputBarAdditionalContentContainer.addView(quoteView) binding.inputBarAdditionalContentContainer.addView(quoteView)
val attachments = (message as? MmsMessageRecord)?.slideDeck val attachments = (message as? MmsMessageRecord)?.slideDeck
// The max content width is the screen width - 2 times the horizontal input bar padding - the // 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 // quote view content area's start and end margins. This unfortunately has to be calculated manually
@ -132,15 +137,15 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
// The 6 DP below is the padding the quote view applies to itself, which isn't included in the // The 6 DP below is the padding the quote view applies to itself, which isn't included in the
// intrinsic height calculation. // intrinsic height calculation.
val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources) val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources)
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + quoteViewIntrinsicHeight val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight) + quoteViewIntrinsicHeight
additionalContentHeight = quoteViewIntrinsicHeight additionalContentHeight = quoteViewIntrinsicHeight
setHeight(newHeight) setHeight(newHeight)
} }
override fun cancelQuoteDraft() { override fun cancelQuoteDraft() {
quote = null quote = null
inputBarAdditionalContentContainer.removeAllViews() binding.inputBarAdditionalContentContainer.removeAllViews()
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight)
additionalContentHeight = 0 additionalContentHeight = 0
setHeight(newHeight) setHeight(newHeight)
} }
@ -148,12 +153,12 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
fun draftLinkPreview() { fun draftLinkPreview() {
quote = null quote = null
val linkPreviewDraftHeight = toPx(88, resources) val linkPreviewDraftHeight = toPx(88, resources)
inputBarAdditionalContentContainer.removeAllViews() binding.inputBarAdditionalContentContainer.removeAllViews()
val linkPreviewDraftView = LinkPreviewDraftView(context) val linkPreviewDraftView = LinkPreviewDraftView(context)
linkPreviewDraftView.delegate = this linkPreviewDraftView.delegate = this
this.linkPreviewDraftView = linkPreviewDraftView this.linkPreviewDraftView = linkPreviewDraftView
inputBarAdditionalContentContainer.addView(linkPreviewDraftView) binding.inputBarAdditionalContentContainer.addView(linkPreviewDraftView)
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + linkPreviewDraftHeight val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight) + linkPreviewDraftHeight
additionalContentHeight = linkPreviewDraftHeight additionalContentHeight = linkPreviewDraftHeight
setHeight(newHeight) setHeight(newHeight)
} }
@ -167,24 +172,32 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
override fun cancelLinkPreviewDraft() { override fun cancelLinkPreviewDraft() {
if (quote != null) { return } if (quote != null) { return }
linkPreview = null linkPreview = null
inputBarAdditionalContentContainer.removeAllViews() binding.inputBarAdditionalContentContainer.removeAllViews()
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight)
additionalContentHeight = 0 additionalContentHeight = 0
setHeight(newHeight) setHeight(newHeight)
} }
private fun showOrHideInputIfNeeded() { private fun showOrHideInputIfNeeded() {
if (showInput) { if (showInput) {
setOf( inputBarEditText, attachmentsButton ).forEach { it.isVisible = true } setOf( binding.inputBarEditText, attachmentsButton ).forEach { it.isVisible = true }
microphoneButton.isVisible = text.isEmpty() microphoneButton.isVisible = text.isEmpty()
sendButton.isVisible = text.isNotEmpty() sendButton.isVisible = text.isNotEmpty()
} else { } else {
cancelQuoteDraft() cancelQuoteDraft()
cancelLinkPreviewDraft() cancelLinkPreviewDraft()
val views = setOf( inputBarEditText, attachmentsButton, microphoneButton, sendButton ) val views = setOf( binding.inputBarEditText, attachmentsButton, microphoneButton, sendButton )
views.forEach { it.isVisible = false } views.forEach { it.isVisible = false }
} }
} }
fun addTextChangedListener(textWatcher: TextWatcher) {
binding.inputBarEditText.addTextChangedListener(textWatcher)
}
fun setSelection(index: Int) {
binding.inputBarEditText.setSelection(index)
}
// endregion // endregion
} }

View File

@ -45,8 +45,8 @@ class InputBarEditText : AppCompatEditText {
delegate?.inputBarEditTextHeightChanged(constrainedHeight.roundToInt()) delegate?.inputBarEditTextHeightChanged(constrainedHeight.roundToInt())
} }
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection { override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? {
val ic: InputConnection = super.onCreateInputConnection(editorInfo) val ic = super.onCreateInputConnection(editorInfo) ?: return null
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png", "image/gif", "image/jpg")) EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png", "image/gif", "image/jpg"))
val callback = val callback =

View File

@ -8,40 +8,56 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.animateSizeChange import org.thoughtcrime.securesms.util.animateSizeChange
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
import org.thoughtcrime.securesms.util.DateUtils import java.util.Date
import java.util.*
class InputBarRecordingView : RelativeLayout { class InputBarRecordingView : RelativeLayout {
private lateinit var binding: ViewInputBarRecordingBinding
private var startTimestamp = 0L private var startTimestamp = 0L
private val snHandler = Handler(Looper.getMainLooper()) private val snHandler = Handler(Looper.getMainLooper())
private var dotViewAnimation: ValueAnimator? = null private var dotViewAnimation: ValueAnimator? = null
private var pulseAnimation: ValueAnimator? = null private var pulseAnimation: ValueAnimator? = null
var delegate: InputBarRecordingViewDelegate? = null var delegate: InputBarRecordingViewDelegate? = null
val lockView: LinearLayout
get() = binding.lockView
val chevronImageView: ImageView
get() = binding.inputBarChevronImageView
val slideToCancelTextView: TextView
get() = binding.inputBarSlideToCancelTextView
val recordButtonOverlay: RelativeLayout
get() = binding.recordButtonOverlay
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_input_bar_recording, this) binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
inputBarMiddleContentContainer.disableClipping() binding.inputBarMiddleContentContainer.disableClipping()
inputBarCancelButton.setOnClickListener { hide() } binding.inputBarCancelButton.setOnClickListener { hide() }
} }
fun show() { fun show() {
startTimestamp = Date().time startTimestamp = Date().time
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme)) binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
inputBarCancelButton.alpha = 0.0f binding.inputBarCancelButton.alpha = 0.0f
inputBarMiddleContentContainer.alpha = 1.0f binding.inputBarMiddleContentContainer.alpha = 1.0f
lockView.alpha = 1.0f binding.lockView.alpha = 1.0f
isVisible = true isVisible = true
alpha = 0.0f alpha = 0.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
@ -77,7 +93,7 @@ class InputBarRecordingView : RelativeLayout {
dotViewAnimation = animation dotViewAnimation = animation
animation.duration = 500L animation.duration = 500L
animation.addUpdateListener { animator -> animation.addUpdateListener { animator ->
dotView.alpha = animator.animatedValue as Float binding.dotView.alpha = animator.animatedValue as Float
} }
animation.repeatCount = ValueAnimator.INFINITE animation.repeatCount = ValueAnimator.INFINITE
animation.repeatMode = ValueAnimator.REVERSE animation.repeatMode = ValueAnimator.REVERSE
@ -87,12 +103,12 @@ class InputBarRecordingView : RelativeLayout {
private fun pulse() { private fun pulse() {
val collapsedSize = toPx(80.0f, resources) val collapsedSize = toPx(80.0f, resources)
val expandedSize = toPx(104.0f, resources) val expandedSize = toPx(104.0f, resources)
pulseView.animateSizeChange(collapsedSize, expandedSize, 1000) binding.pulseView.animateSizeChange(collapsedSize, expandedSize, 1000)
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.5, 0.0f) val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.5, 0.0f)
pulseAnimation = animation pulseAnimation = animation
animation.duration = 1000L animation.duration = 1000L
animation.addUpdateListener { animator -> animation.addUpdateListener { animator ->
pulseView.alpha = animator.animatedValue as Float binding.pulseView.alpha = animator.animatedValue as Float
if (animator.animatedFraction == 1.0f && isVisible) { pulse() } if (animator.animatedFraction == 1.0f && isVisible) { pulse() }
} }
animation.start() animation.start()
@ -101,21 +117,21 @@ class InputBarRecordingView : RelativeLayout {
private fun animateLockViewUp() { private fun animateLockViewUp() {
val startMarginBottom = toPx(32, resources) val startMarginBottom = toPx(32, resources)
val endMarginBottom = toPx(72, resources) val endMarginBottom = toPx(72, resources)
val layoutParams = lockView.layoutParams as LayoutParams val layoutParams = binding.lockView.layoutParams as LayoutParams
layoutParams.bottomMargin = startMarginBottom layoutParams.bottomMargin = startMarginBottom
lockView.layoutParams = layoutParams binding.lockView.layoutParams = layoutParams
val animation = ValueAnimator.ofObject(IntEvaluator(), startMarginBottom, endMarginBottom) val animation = ValueAnimator.ofObject(IntEvaluator(), startMarginBottom, endMarginBottom)
animation.duration = 250L animation.duration = 250L
animation.addUpdateListener { animator -> animation.addUpdateListener { animator ->
layoutParams.bottomMargin = animator.animatedValue as Int layoutParams.bottomMargin = animator.animatedValue as Int
lockView.layoutParams = layoutParams binding.lockView.layoutParams = layoutParams
} }
animation.start() animation.start()
} }
private fun updateTimer() { private fun updateTimer() {
val duration = (Date().time - startTimestamp) / 1000L val duration = (Date().time - startTimestamp) / 1000L
recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration) binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
snHandler.postDelayed({ updateTimer() }, 500) snHandler.postDelayed({ updateTimer() }, 500)
} }
@ -123,19 +139,19 @@ class InputBarRecordingView : RelativeLayout {
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
fadeOutAnimation.duration = 250L fadeOutAnimation.duration = 250L
fadeOutAnimation.addUpdateListener { animator -> fadeOutAnimation.addUpdateListener { animator ->
inputBarMiddleContentContainer.alpha = animator.animatedValue as Float binding.inputBarMiddleContentContainer.alpha = animator.animatedValue as Float
lockView.alpha = animator.animatedValue as Float binding.lockView.alpha = animator.animatedValue as Float
} }
fadeOutAnimation.start() fadeOutAnimation.start()
val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
fadeInAnimation.duration = 250L fadeInAnimation.duration = 250L
fadeInAnimation.addUpdateListener { animator -> fadeInAnimation.addUpdateListener { animator ->
inputBarCancelButton.alpha = animator.animatedValue as Float binding.inputBarCancelButton.alpha = animator.animatedValue as Float
} }
fadeInAnimation.start() fadeInAnimation.start()
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme)) binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() } binding.recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() } binding.inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }
} }
} }

View File

@ -4,33 +4,29 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import kotlinx.android.synthetic.main.view_mention_candidate.view.* import network.loki.messenger.databinding.ViewMentionCandidateV2Binding
import network.loki.messenger.R
import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : RelativeLayout(context, attrs, defStyleAttr) { class MentionCandidateView : RelativeLayout {
private lateinit var binding: ViewMentionCandidateV2Binding
var candidate = Mention("", "") var candidate = Mention("", "")
set(newValue) { field = newValue; update() } set(newValue) { field = newValue; update() }
var glide: GlideRequests? = null var glide: GlideRequests? = null
var openGroupServer: String? = null var openGroupServer: String? = null
var openGroupRoom: String? = null var openGroupRoom: String? = null
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null) constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
companion object { private fun initialize() {
binding = ViewMentionCandidateV2Binding.inflate(LayoutInflater.from(context), this, true)
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView {
return layoutInflater.inflate(R.layout.view_mention_candidate_v2, parent, false) as MentionCandidateView
}
} }
private fun update() { private fun update() = with(binding) {
mentionCandidateNameTextView.text = candidate.displayName mentionCandidateNameTextView.text = candidate.displayName
profilePictureView.publicKey = candidate.publicKey profilePictureView.publicKey = candidate.publicKey
profilePictureView.displayName = candidate.displayName profilePictureView.displayName = candidate.displayName

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
@ -42,7 +41,7 @@ class MentionCandidatesView(context: Context, attrs: AttributeSet?, defStyleAttr
override fun getItem(position: Int): Mention { return candidates[position] } override fun getItem(position: Int): Mention { return candidates[position] }
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View { override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent) val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context)
val mentionCandidate = getItem(position) val mentionCandidate = getItem(position)
cell.glide = glide cell.glide = glide
cell.candidate = mentionCandidate cell.candidate = mentionCandidate

View File

@ -12,7 +12,6 @@ import android.os.AsyncTask
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
@ -24,7 +23,6 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import kotlinx.android.synthetic.main.activity_conversation_v2.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
@ -35,7 +33,12 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.* import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.ExpirationDialog
import org.thoughtcrime.securesms.MediaOverviewActivity
import org.thoughtcrime.securesms.MuteDialog
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.ShortcutLauncherActivity
import org.thoughtcrime.securesms.contacts.SelectContactsActivity import org.thoughtcrime.securesms.contacts.SelectContactsActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
@ -101,15 +104,12 @@ object ConversationMenuHelper {
val searchViewItem = menu.findItem(R.id.menu_search) val searchViewItem = menu.findItem(R.id.menu_search)
(context as ConversationActivityV2).searchViewItem = searchViewItem (context as ConversationActivityV2).searchViewItem = searchViewItem
val searchView = searchViewItem.actionView as SearchView val searchView = searchViewItem.actionView as SearchView
val searchViewModel = context.searchViewModel!!
val queryListener = object : OnQueryTextListener { val queryListener = object : OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
return true return true
} }
override fun onQueryTextChange(query: String): Boolean { override fun onQueryTextChange(query: String): Boolean {
searchViewModel.onQueryUpdated(query, threadId)
context.searchBottomBar.showLoading()
context.onSearchQueryUpdated(query) context.onSearchQueryUpdated(query)
return true return true
} }
@ -117,10 +117,7 @@ object ConversationMenuHelper {
searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
searchView.setOnQueryTextListener(queryListener) searchView.setOnQueryTextListener(queryListener)
searchViewModel.onSearchOpened() context.onSearchOpened()
context.searchBottomBar.visibility = View.VISIBLE
context.searchBottomBar.setData(0, 0)
context.inputBar.visibility = View.GONE
for (i in 0 until menu.size()) { for (i in 0 until menu.size()) {
if (menu.getItem(i) != searchViewItem) { if (menu.getItem(i) != searchViewItem) {
menu.getItem(i).isVisible = false menu.getItem(i).isVisible = false
@ -131,11 +128,7 @@ object ConversationMenuHelper {
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
searchView.setOnQueryTextListener(null) searchView.setOnQueryTextListener(null)
searchViewModel.onSearchClosed() context.onSearchClosed()
context.searchBottomBar.visibility = View.GONE
context.inputBar.visibility = View.VISIBLE
context.onSearchQueryUpdated(null)
context.invalidateOptionsMenu()
return true return true
} }
}) })
@ -169,7 +162,7 @@ object ConversationMenuHelper {
} }
private fun search(context: Context) { private fun search(context: Context) {
val searchViewModel = (context as ConversationActivityV2).searchViewModel!! val searchViewModel = (context as ConversationActivityV2).searchViewModel
searchViewModel.onSearchOpened() searchViewModel.onSearchOpened()
} }

View File

@ -7,35 +7,41 @@ import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.view_control_message.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewControlMessageBinding
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
class ControlMessageView : LinearLayout { class ControlMessageView : LinearLayout {
private lateinit var binding: ViewControlMessageBinding
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_control_message, this) binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true)
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
} }
// endregion // endregion
// region Updating // region Updating
fun bind(message: MessageRecord, previous: MessageRecord?) { fun bind(message: MessageRecord, previous: MessageRecord?) {
dateBreakTextView.showDateBreak(message, previous) binding.dateBreakTextView.showDateBreak(message, previous)
iconImageView.visibility = View.GONE binding.iconImageView.visibility = View.GONE
if (message.isExpirationTimerUpdate) { if (message.isExpirationTimerUpdate) {
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)) binding.iconImageView.setImageDrawable(
iconImageView.visibility = View.VISIBLE ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)
)
binding.iconImageView.visibility = View.VISIBLE
} else if (message.isMediaSavedNotification) { } else if (message.isMediaSavedNotification) {
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme)) binding.iconImageView.setImageDrawable(
iconImageView.visibility = View.VISIBLE ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme)
)
binding.iconImageView.visibility = View.VISIBLE
} }
textView.text = message.getDisplayBody(context) binding.textView.text = message.getDisplayBody(context)
} }
fun recycle() { fun recycle() {

View File

@ -6,32 +6,28 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.view.*
import kotlinx.android.synthetic.main.view_deleted_message.view.*
import kotlinx.android.synthetic.main.view_document.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewDeletedMessageBinding
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import java.util.*
class DeletedMessageView : LinearLayout { class DeletedMessageView : LinearLayout {
private lateinit var binding: ViewDeletedMessageBinding
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_deleted_message, this) binding = ViewDeletedMessageBinding.inflate(LayoutInflater.from(context), this, true)
} }
// endregion // endregion
// region Updating // region Updating
fun bind(message: MessageRecord, @ColorInt textColor: Int) { fun bind(message: MessageRecord, @ColorInt textColor: Int) {
assert(message.isDeleted) assert(message.isDeleted)
deleteTitleTextView.text = context.getString(R.string.deleted_message) binding.deleteTitleTextView.text = context.getString(R.string.deleted_message)
deleteTitleTextView.setTextColor(textColor) binding.deleteTitleTextView.setTextColor(textColor)
deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
} }
// endregion // endregion
} }

View File

@ -6,29 +6,27 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import kotlinx.android.synthetic.main.view_document.view.* import network.loki.messenger.databinding.ViewDocumentBinding
import network.loki.messenger.R
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
class DocumentView : LinearLayout { class DocumentView : LinearLayout {
private lateinit var binding: ViewDocumentBinding
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_document, this) binding = ViewDocumentBinding.inflate(LayoutInflater.from(context), this, true)
} }
// endregion // endregion
// region Updating // region Updating
fun bind(message: MmsMessageRecord, @ColorInt textColor: Int) { fun bind(message: MmsMessageRecord, @ColorInt textColor: Int) {
val document = message.slideDeck.documentSlide!! val document = message.slideDeck.documentSlide!!
documentTitleTextView.text = document.fileName.or("Untitled File") binding.documentTitleTextView.text = document.fileName.or("Untitled File")
documentTitleTextView.setTextColor(textColor) binding.documentTitleTextView.setTextColor(textColor)
documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) binding.documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
} }
// endregion // endregion
} }

View File

@ -11,8 +11,8 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_link_preview.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewLinkPreviewBinding
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.UiModeUtilities
class LinkPreviewView : LinearLayout { class LinkPreviewView : LinearLayout {
private lateinit var binding: ViewLinkPreviewBinding
private val cornerMask by lazy { CornerMask(this) } private val cornerMask by lazy { CornerMask(this) }
private var url: String? = null private var url: String? = null
lateinit var bodyTextView: TextView lateinit var bodyTextView: TextView
@ -33,7 +34,7 @@ class LinkPreviewView : LinearLayout {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_link_preview, this) binding = ViewLinkPreviewBinding.inflate(LayoutInflater.from(context), this, true)
} }
// endregion // endregion
@ -44,20 +45,20 @@ class LinkPreviewView : LinearLayout {
// Thumbnail // Thumbnail
if (linkPreview.getThumbnail().isPresent) { if (linkPreview.getThumbnail().isPresent) {
// This internally fetches the thumbnail // This internally fetches the thumbnail
thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message) binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
thumbnailImageView.loadIndicator.isVisible = false binding.thumbnailImageView.loadIndicator.isVisible = false
} }
// Title // Title
titleTextView.text = linkPreview.title binding.titleTextView.text = linkPreview.title
val textColorID = if (message.isOutgoing && UiModeUtilities.isDayUiMode(context)) { val textColorID = if (message.isOutgoing && UiModeUtilities.isDayUiMode(context)) {
R.color.white R.color.white
} else { } else {
if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.white if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.white
} }
titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme)) binding.titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
// Body // Body
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
mainLinkPreviewContainer.addView(bodyTextView) binding.mainLinkPreviewContainer.addView(bodyTextView)
// Corner radii // Corner radii
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
cornerMask.setTopLeftRadius(cornerRadii[0]) cornerMask.setTopLeftRadius(cornerRadii[0])
@ -78,7 +79,7 @@ class LinkPreviewView : LinearLayout {
val rawYInt = event.rawY.toInt() val rawYInt = event.rawY.toInt()
val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt) val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
val previewRect = Rect() val previewRect = Rect()
mainLinkPreviewParent.getGlobalVisibleRect(previewRect) binding.mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
if (previewRect.contains(hitRect)) { if (previewRect.contains(hitRect)) {
openURL() openURL()
return return

View File

@ -6,15 +6,15 @@ import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.view_open_group_invitation.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupV2 import network.loki.messenger.databinding.ViewOpenGroupInvitationBinding
import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.OpenGroupUrlParser
import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
class OpenGroupInvitationView : LinearLayout { class OpenGroupInvitationView : LinearLayout {
private lateinit var binding: ViewOpenGroupInvitationBinding
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
constructor(context: Context): super(context) { initialize() } constructor(context: Context): super(context) { initialize() }
@ -22,7 +22,7 @@ class OpenGroupInvitationView : LinearLayout {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_open_group_invitation, this) binding = ViewOpenGroupInvitationBinding.inflate(LayoutInflater.from(context), this, true)
} }
fun bind(message: MessageRecord, @ColorInt textColor: Int) { fun bind(message: MessageRecord, @ColorInt textColor: Int) {
@ -31,12 +31,14 @@ class OpenGroupInvitationView : LinearLayout {
val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation
this.data = data this.data = data
val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus
openGroupInvitationIconImageView.setImageResource(iconID) with(binding){
openGroupTitleTextView.text = data.groupName openGroupInvitationIconImageView.setImageResource(iconID)
openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl) openGroupTitleTextView.text = data.groupName
openGroupTitleTextView.setTextColor(textColor) openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl)
openGroupJoinMessageTextView.setTextColor(textColor) openGroupTitleTextView.setTextColor(textColor)
openGroupURLTextView.setTextColor(textColor) openGroupJoinMessageTextView.setTextColor(textColor)
openGroupURLTextView.setTextColor(textColor)
}
} }
fun joinOpenGroup() { fun joinOpenGroup() {

View File

@ -11,8 +11,8 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.view.isVisible import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.view_quote.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewQuoteBinding
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
@ -39,6 +39,7 @@ class QuoteView : LinearLayout {
@Inject lateinit var contactDb: SessionContactDatabase @Inject lateinit var contactDb: SessionContactDatabase
private lateinit var binding: ViewQuoteBinding
private lateinit var mode: Mode private lateinit var mode: Mode
private val vPadding by lazy { toPx(6, resources) } private val vPadding by lazy { toPx(6, resources) }
var delegate: QuoteViewDelegate? = null var delegate: QuoteViewDelegate? = null
@ -52,19 +53,19 @@ class QuoteView : LinearLayout {
constructor(context: Context, mode: Mode) : super(context) { constructor(context: Context, mode: Mode) : super(context) {
this.mode = mode this.mode = mode
LayoutInflater.from(context).inflate(R.layout.view_quote, this) binding = ViewQuoteBinding.inflate(LayoutInflater.from(context), this, true)
// Add padding here (not on mainQuoteViewContainer) to get a bit of a top inset while avoiding // Add padding here (not on binding.mainQuoteViewContainer) to get a bit of a top inset while avoiding
// the clipping issue described in getIntrinsicHeight(maxContentWidth:). // the clipping issue described in getIntrinsicHeight(maxContentWidth:).
setPadding(0, toPx(6, resources), 0, 0) setPadding(0, toPx(6, resources), 0, 0)
when (mode) { when (mode) {
Mode.Draft -> quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() } Mode.Draft -> binding.quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
Mode.Regular -> { Mode.Regular -> {
quoteViewCancelButton.isVisible = false binding.quoteViewCancelButton.isVisible = false
mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme)) binding.mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme))
val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams val quoteViewMainContentContainerLayoutParams = binding.quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
// Since we're not showing the cancel button we can shorten the end margin // Since we're not showing the cancel button we can shorten the end margin
quoteViewMainContentContainerLayoutParams.marginEnd = resources.getDimension(R.dimen.medium_spacing).roundToInt() quoteViewMainContentContainerLayoutParams.marginEnd = resources.getDimension(R.dimen.medium_spacing).roundToInt()
quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams binding.quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
} }
} }
} }
@ -73,19 +74,19 @@ class QuoteView : LinearLayout {
// region General // region General
fun getIntrinsicContentHeight(maxContentWidth: Int): Int { fun getIntrinsicContentHeight(maxContentWidth: Int): Int {
// If we're showing an attachment thumbnail, just constrain to the height of that // If we're showing an attachment thumbnail, just constrain to the height of that
if (quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) } if (binding.quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) }
var result = 0 var result = 0
var authorTextViewIntrinsicHeight = 0 var authorTextViewIntrinsicHeight = 0
if (quoteViewAuthorTextView.isVisible) { if (binding.quoteViewAuthorTextView.isVisible) {
val author = quoteViewAuthorTextView.text val author = binding.quoteViewAuthorTextView.text
authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, quoteViewAuthorTextView.paint, maxContentWidth) authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, binding.quoteViewAuthorTextView.paint, maxContentWidth)
result += authorTextViewIntrinsicHeight result += authorTextViewIntrinsicHeight
} }
val body = quoteViewBodyTextView.text val body = binding.quoteViewBodyTextView.text
val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, quoteViewBodyTextView.paint, maxContentWidth) val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
val staticLayout = TextUtilities.getIntrinsicLayout(body, quoteViewBodyTextView.paint, maxContentWidth) val staticLayout = TextUtilities.getIntrinsicLayout(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
result += bodyTextViewIntrinsicHeight result += bodyTextViewIntrinsicHeight
if (!quoteViewAuthorTextView.isVisible) { if (!binding.quoteViewAuthorTextView.isVisible) {
// We want to at least be as high as the cancel button 36DP, and no higher than 3 lines of text. // We want to at least be as high as the cancel button 36DP, and no higher than 3 lines of text.
// Height from intrinsic layout is the height of the text before truncation so we shorten // Height from intrinsic layout is the height of the text before truncation so we shorten
// proportionally to our max lines setting. // proportionally to our max lines setting.
@ -115,82 +116,90 @@ class QuoteView : LinearLayout {
// Reduce the max body text view line count to 2 if this is a group thread because // Reduce the max body text view line count to 2 if this is a group thread because
// we'll be showing the author text view and we don't want the overall quote view height // we'll be showing the author text view and we don't want the overall quote view height
// to get too big. // to get too big.
quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3 binding.quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3
// Author // Author
if (thread.isGroupRecipient) { if (thread.isGroupRecipient) {
val author = contactDb.getContactWithSessionID(authorPublicKey) val author = contactDb.getContactWithSessionID(authorPublicKey)
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
quoteViewAuthorTextView.text = authorDisplayName binding.quoteViewAuthorTextView.text = authorDisplayName
quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage)) binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
} }
quoteViewAuthorTextView.isVisible = thread.isGroupRecipient binding.quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
// Body // Body
quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context); binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context);
quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage)) binding.quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
// Accent line / attachment preview // Accent line / attachment preview
val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing
quoteViewAccentLine.isVisible = !hasAttachments binding.quoteViewAccentLine.isVisible = !hasAttachments
quoteViewAttachmentPreviewContainer.isVisible = hasAttachments binding.quoteViewAttachmentPreviewContainer.isVisible = hasAttachments
if (!hasAttachments) { if (!hasAttachments) {
val accentLineLayoutParams = quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams val accentLineLayoutParams = binding.quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams
accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height
quoteViewAccentLine.layoutParams = accentLineLayoutParams binding.quoteViewAccentLine.layoutParams = accentLineLayoutParams
quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage)) binding.quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage))
} else if (attachments != null) { } else if (attachments != null) {
quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme)) binding.quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme))
val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent
val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme) val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme)
quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor) binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
quoteViewAttachmentPreviewImageView.isVisible = false binding.quoteViewAttachmentPreviewImageView.isVisible = false
quoteViewAttachmentThumbnailImageView.isVisible = false binding.quoteViewAttachmentThumbnailImageView.isVisible = false
if (attachments.audioSlide != null) { when {
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone) attachments.audioSlide != null -> {
quoteViewAttachmentPreviewImageView.isVisible = true binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio) binding.quoteViewAttachmentPreviewImageView.isVisible = true
} else if (attachments.documentSlide != null) { binding.quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light) }
quoteViewAttachmentPreviewImageView.isVisible = true attachments.documentSlide != null -> {
quoteViewBodyTextView.text = resources.getString(R.string.document) binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
} else if (attachments.thumbnailSlide != null) { binding.quoteViewAttachmentPreviewImageView.isVisible = true
val slide = attachments.thumbnailSlide!! binding.quoteViewBodyTextView.text = resources.getString(R.string.document)
// This internally fetches the thumbnail }
quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources) attachments.thumbnailSlide != null -> {
quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false) val slide = attachments.thumbnailSlide!!
quoteViewAttachmentThumbnailImageView.isVisible = true // This internally fetches the thumbnail
quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image) binding.quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources)
binding.quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false)
binding.quoteViewAttachmentThumbnailImageView.isVisible = true
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
}
} }
} }
mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth)) binding.mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth))
val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams val quoteViewMainContentContainerLayoutParams = binding.quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
// The start margin is different if we just show the accent line vs if we show an attachment thumbnail // 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) quoteViewMainContentContainerLayoutParams.marginStart = if (!hasAttachments) toPx(16, resources) else toPx(48, resources)
quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams binding.quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
} }
// endregion // endregion
// region Convenience // region Convenience
@ColorInt private fun getLineColor(isOutgoingMessage: Boolean): Int { @ColorInt private fun getLineColor(isOutgoingMessage: Boolean): Int {
val isLightMode = UiModeUtilities.isDayUiMode(context) val isLightMode = UiModeUtilities.isDayUiMode(context)
if ((mode == Mode.Regular && isLightMode) || (mode == Mode.Draft && isLightMode)) { return when {
return ResourcesCompat.getColor(resources, R.color.black, context.theme) mode == Mode.Regular && isLightMode || mode == Mode.Draft && isLightMode -> {
} else if (mode == Mode.Regular && !isLightMode) { ResourcesCompat.getColor(resources, R.color.black, context.theme)
if (isOutgoingMessage) { }
return ResourcesCompat.getColor(resources, R.color.black, context.theme) mode == Mode.Regular && !isLightMode -> {
} else { if (isOutgoingMessage) {
return ResourcesCompat.getColor(resources, R.color.accent, context.theme) ResourcesCompat.getColor(resources, R.color.black, context.theme)
} else {
ResourcesCompat.getColor(resources, R.color.accent, context.theme)
}
}
else -> { // Draft & dark mode
ResourcesCompat.getColor(resources, R.color.accent, context.theme)
} }
} else { // Draft & dark mode
return ResourcesCompat.getColor(resources, R.color.accent, context.theme)
} }
} }
@ColorInt private fun getTextColor(isOutgoingMessage: Boolean): Int { @ColorInt private fun getTextColor(isOutgoingMessage: Boolean): Int {
if (mode == Mode.Draft) { return ResourcesCompat.getColor(resources, R.color.text, context.theme) } if (mode == Mode.Draft) { return ResourcesCompat.getColor(resources, R.color.text, context.theme) }
val isLightMode = UiModeUtilities.isDayUiMode(context) val isLightMode = UiModeUtilities.isDayUiMode(context)
if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) { return if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) {
return ResourcesCompat.getColor(resources, R.color.black, context.theme) ResourcesCompat.getColor(resources, R.color.black, context.theme)
} else { } else {
return ResourcesCompat.getColor(resources, R.color.white, context.theme) ResourcesCompat.getColor(resources, R.color.white, context.theme)
} }
} }
// endregion // endregion

View File

@ -6,15 +6,15 @@ import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.view_untrusted_attachment.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
import java.util.* import java.util.Locale
class UntrustedAttachmentView: LinearLayout { class UntrustedAttachmentView: LinearLayout {
private lateinit var binding: ViewUntrustedAttachmentBinding
enum class AttachmentType { enum class AttachmentType {
AUDIO, AUDIO,
DOCUMENT, DOCUMENT,
@ -27,7 +27,7 @@ class UntrustedAttachmentView: LinearLayout {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_untrusted_attachment, this) binding = ViewUntrustedAttachmentBinding.inflate(LayoutInflater.from(context), this, true)
} }
// endregion // endregion
@ -42,8 +42,8 @@ class UntrustedAttachmentView: LinearLayout {
iconDrawable.mutate().setTint(textColor) iconDrawable.mutate().setTint(textColor)
val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT)) val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT))
untrustedAttachmentIcon.setImageDrawable(iconDrawable) binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable)
untrustedAttachmentTitle.text = text binding.untrustedAttachmentTitle.text = text
} }
// endregion // endregion

View File

@ -23,8 +23,9 @@ import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat import androidx.core.graphics.BlendModeCompat
import androidx.core.text.getSpans import androidx.core.text.getSpans
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import kotlinx.android.synthetic.main.view_visible_message_content.view.* import androidx.core.view.children
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.ViewUtil
@ -40,14 +41,14 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.util.getColorWithID import org.thoughtcrime.securesms.util.getColorWithID
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
import java.util.* import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout { class VisibleMessageContentView : LinearLayout {
private lateinit var binding: ViewVisibleMessageContentBinding
var onContentClick: ((event: MotionEvent) -> Unit)? = null var onContentClick: ((event: MotionEvent) -> Unit)? = null
var onContentDoubleTap: (() -> Unit)? = null var onContentDoubleTap: (() -> Unit)? = null
var delegate: VisibleMessageContentViewDelegate? = null var delegate: VisibleMessageContentViewDelegate? = null
@ -59,7 +60,7 @@ class VisibleMessageContentView : LinearLayout {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_visible_message_content, this) binding = ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(context), this, true)
} }
// endregion // endregion
@ -74,17 +75,17 @@ class VisibleMessageContentView : LinearLayout {
background.colorFilter = filter background.colorFilter = filter
setBackground(background) setBackground(background)
// Body // Body
mainContainer.removeAllViews() binding.mainContainer.removeAllViews()
onContentClick = null onContentClick = null
onContentDoubleTap = null onContentDoubleTap = null
if (message.isDeleted) { if (message.isDeleted) {
val deletedMessageView = DeletedMessageView(context) val deletedMessageView = DeletedMessageView(context)
deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message)) deletedMessageView.bind(message, getTextColor(context,message))
mainContainer.addView(deletedMessageView) binding.mainContainer.addView(deletedMessageView)
} else if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) { } else if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
val linkPreviewView = LinkPreviewView(context) val linkPreviewView = LinkPreviewView(context)
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery) linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
mainContainer.addView(linkPreviewView) binding.mainContainer.addView(linkPreviewView)
onContentClick = { event -> linkPreviewView.calculateHit(event) } onContentClick = { event -> linkPreviewView.calculateHit(event) }
// Body text view is inside the link preview for layout convenience // Body text view is inside the link preview for layout convenience
} else if (message is MmsMessageRecord && message.quote != null) { } else if (message is MmsMessageRecord && message.quote != null) {
@ -102,10 +103,10 @@ class VisibleMessageContentView : LinearLayout {
quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread, quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId,
quote.isOriginalMissing, glide) quote.isOriginalMissing, glide)
mainContainer.addView(quoteView) binding.mainContainer.addView(quoteView)
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) val bodyTextView = getBodyTextView(context, message, searchQuery)
ViewUtil.setPaddingTop(bodyTextView, 0) ViewUtil.setPaddingTop(bodyTextView, 0)
mainContainer.addView(bodyTextView) binding.mainContainer.addView(bodyTextView)
onContentClick = { event -> onContentClick = { event ->
val r = Rect() val r = Rect()
quoteView.getGlobalVisibleRect(r) quoteView.getGlobalVisibleRect(r)
@ -124,34 +125,34 @@ class VisibleMessageContentView : LinearLayout {
voiceMessageView.indexInAdapter = indexInAdapter voiceMessageView.indexInAdapter = indexInAdapter
voiceMessageView.delegate = context as? ConversationActivityV2 voiceMessageView.delegate = context as? ConversationActivityV2
voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster) voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
mainContainer.addView(voiceMessageView) binding.mainContainer.addView(voiceMessageView)
// We have to use onContentClick (rather than a click listener directly on the voice // We have to use onContentClick (rather than a click listener directly on the voice
// message view) so as to not interfere with all the other gestures. // message view) so as to not interfere with all the other gestures.
onContentClick = { voiceMessageView.togglePlayback() } onContentClick = { voiceMessageView.togglePlayback() }
onContentDoubleTap = { voiceMessageView.handleDoubleTap() } onContentDoubleTap = { voiceMessageView.handleDoubleTap() }
} else { } else {
val untrustedView = UntrustedAttachmentView(context) val untrustedView = UntrustedAttachmentView(context)
untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message)) untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, getTextColor(context,message))
mainContainer.addView(untrustedView) binding.mainContainer.addView(untrustedView)
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
} }
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) { } else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
// Document attachment // Document attachment
if (contactIsTrusted || message.isOutgoing) { if (contactIsTrusted || message.isOutgoing) {
val documentView = DocumentView(context) val documentView = DocumentView(context)
documentView.bind(message, VisibleMessageContentView.getTextColor(context, message)) documentView.bind(message, getTextColor(context, message))
mainContainer.addView(documentView) binding.mainContainer.addView(documentView)
} else { } else {
val untrustedView = UntrustedAttachmentView(context) val untrustedView = UntrustedAttachmentView(context)
untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message)) untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, getTextColor(context,message))
mainContainer.addView(untrustedView) binding.mainContainer.addView(untrustedView)
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
} }
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) { } else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
// Images/Video attachment // Images/Video attachment
if (contactIsTrusted || message.isOutgoing) { if (contactIsTrusted || message.isOutgoing) {
val albumThumbnailView = AlbumThumbnailView(context) val albumThumbnailView = AlbumThumbnailView(context)
mainContainer.addView(albumThumbnailView) binding.mainContainer.addView(albumThumbnailView)
// isStart and isEnd of cluster needed for calculating the mask for full bubble image groups // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups
// bind after add view because views are inflated and calculated during bind // bind after add view because views are inflated and calculated during bind
albumThumbnailView.bind( albumThumbnailView.bind(
@ -165,18 +166,18 @@ class VisibleMessageContentView : LinearLayout {
} }
} else { } else {
val untrustedView = UntrustedAttachmentView(context) val untrustedView = UntrustedAttachmentView(context)
untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message)) untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, getTextColor(context,message))
mainContainer.addView(untrustedView) binding.mainContainer.addView(untrustedView)
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
} }
} else if (message.isOpenGroupInvitation) { } else if (message.isOpenGroupInvitation) {
val openGroupInvitationView = OpenGroupInvitationView(context) val openGroupInvitationView = OpenGroupInvitationView(context)
openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message)) openGroupInvitationView.bind(message, getTextColor(context, message))
mainContainer.addView(openGroupInvitationView) binding.mainContainer.addView(openGroupInvitationView)
onContentClick = { openGroupInvitationView.joinOpenGroup() } onContentClick = { openGroupInvitationView.joinOpenGroup() }
} else { } else {
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) val bodyTextView = getBodyTextView(context, message, searchQuery)
mainContainer.addView(bodyTextView) binding.mainContainer.addView(bodyTextView)
onContentClick = { event -> onContentClick = { event ->
// intersectedModalSpans should only be a list of one item // intersectedModalSpans should only be a list of one item
bodyTextView.getIntersectedModalSpans(event).forEach { span -> bodyTextView.getIntersectedModalSpans(event).forEach { span ->
@ -188,21 +189,33 @@ class VisibleMessageContentView : LinearLayout {
private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable { private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable {
val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster) val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster)
@DrawableRes val backgroundID: Int @DrawableRes val backgroundID = when {
if (isSingleMessage) { isSingleMessage -> {
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
} else if (isStartOfMessageCluster) { }
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start isStartOfMessageCluster -> {
} else if (isEndOfMessageCluster) { if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end }
} else { isEndOfMessageCluster -> {
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end
}
else -> {
if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle
}
} }
return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!! return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!!
} }
fun recycle() { fun recycle() {
mainContainer.removeAllViews() binding.mainContainer.removeAllViews()
}
fun playVoiceMessage() {
binding.mainContainer.children.forEach { view ->
if (view is VoiceMessageView) {
return@forEach view.togglePlayback()
}
}
} }
// endregion // endregion
@ -227,8 +240,10 @@ class VisibleMessageContentView : LinearLayout {
var body = message.body.toSpannable() var body = message.body.toSpannable()
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context) body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context)
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { BackgroundColorSpan(Color.WHITE) }, body, searchQuery) body = SearchUtil.getHighlightedSpan(Locale.getDefault(),
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery) { BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
body = SearchUtil.getHighlightedSpan(Locale.getDefault(),
{ ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
Linkify.addLinks(body, Linkify.WEB_URLS) Linkify.addLinks(body, Linkify.WEB_URLS)

View File

@ -5,12 +5,15 @@ import android.content.res.Resources
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.* import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -19,25 +22,35 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.view_visible_message.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact.ContactContext import org.session.libsession.messaging.contacts.Contact.ContactContext
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.* import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.* import org.thoughtcrime.securesms.util.DateUtils
import java.util.* import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.getColorWithID
import org.thoughtcrime.securesms.util.toDp
import org.thoughtcrime.securesms.util.toPx
import java.util.Date
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sqrt import kotlin.math.sqrt
@AndroidEntryPoint @AndroidEntryPoint
class VisibleMessageView : LinearLayout { class VisibleMessageView : LinearLayout {
@ -48,6 +61,7 @@ class VisibleMessageView : LinearLayout {
@Inject lateinit var smsDb: SmsDatabase @Inject lateinit var smsDb: SmsDatabase
@Inject lateinit var mmsDb: MmsDatabase @Inject lateinit var mmsDb: MmsDatabase
private lateinit var binding: ViewVisibleMessageBinding
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate() private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
private val swipeToReplyIconRect = Rect() private val swipeToReplyIconRect = Rect()
@ -60,7 +74,11 @@ class VisibleMessageView : LinearLayout {
private var onDoubleTap: (() -> Unit)? = null private var onDoubleTap: (() -> Unit)? = null
var indexInAdapter: Int = -1 var indexInAdapter: Int = -1
var snIsSelected = false var snIsSelected = false
set(value) { field = value; handleIsSelectedChanged()} set(value) {
field = value
binding.messageTimestampTextView.isVisible = isSelected
handleIsSelectedChanged()
}
var onPress: ((event: MotionEvent) -> Unit)? = null var onPress: ((event: MotionEvent) -> Unit)? = null
var onSwipeToReply: (() -> Unit)? = null var onSwipeToReply: (() -> Unit)? = null
var onLongPress: (() -> Unit)? = null var onLongPress: (() -> Unit)? = null
@ -68,7 +86,7 @@ class VisibleMessageView : LinearLayout {
companion object { companion object {
const val swipeToReplyThreshold = 64.0f // dp const val swipeToReplyThreshold = 64.0f // dp
const val longPressMovementTreshold = 10.0f // dp const val longPressMovementThreshold = 10.0f // dp
const val longPressDurationThreshold = 250L // ms const val longPressDurationThreshold = 250L // ms
const val maxDoubleTapInterval = 200L const val maxDoubleTapInterval = 200L
} }
@ -79,12 +97,12 @@ class VisibleMessageView : LinearLayout {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_visible_message, this) binding = ViewVisibleMessageBinding.inflate(LayoutInflater.from(context), this, true)
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
isHapticFeedbackEnabled = true isHapticFeedbackEnabled = true
setWillNotDraw(false) setWillNotDraw(false)
expirationTimerViewContainer.disableClipping() binding.expirationTimerViewContainer.disableClipping()
messageContentContainer.disableClipping() binding.messageContentContainer.disableClipping()
} }
// endregion // endregion
@ -101,47 +119,46 @@ class VisibleMessageView : LinearLayout {
// Show profile picture and sender name if this is a group thread AND // Show profile picture and sender name if this is a group thread AND
// the message is incoming // the message is incoming
if (isGroupThread && !message.isOutgoing) { if (isGroupThread && !message.isOutgoing) {
profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE binding.profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
profilePictureView.publicKey = senderSessionID binding.profilePictureView.publicKey = senderSessionID
profilePictureView.glide = glide binding.profilePictureView.glide = glide
profilePictureView.update(message.individualRecipient, threadID) binding.profilePictureView.update(message.individualRecipient)
profilePictureView.setOnClickListener { binding.profilePictureView.setOnClickListener {
showUserDetails(senderSessionID, threadID) showUserDetails(senderSessionID, threadID)
} }
if (thread.isOpenGroupRecipient) { if (thread.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server) val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server)
moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE binding.moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
} else { } else {
moderatorIconImageView.visibility = View.INVISIBLE binding.moderatorIconImageView.visibility = View.INVISIBLE
} }
senderNameTextView.isVisible = isStartOfMessageCluster binding.senderNameTextView.isVisible = isStartOfMessageCluster
val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
senderNameTextView.text = contact?.displayName(context) ?: senderSessionID binding.senderNameTextView.text = contact?.displayName(context) ?: senderSessionID
} else { } else {
profilePictureContainer.visibility = View.GONE binding.profilePictureContainer.visibility = View.GONE
senderNameTextView.visibility = View.GONE binding.senderNameTextView.visibility = View.GONE
} }
// Date break // Date break
dateBreakTextView.showDateBreak(message, previous) binding.dateBreakTextView.showDateBreak(message, previous)
// Timestamp // Timestamp
messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) binding.messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
// Margins // Margins
val startPadding: Int val startPadding = if (isGroupThread) {
if (isGroupThread) { if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() else 0
startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() else 0
} else { } else {
startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt()
else resources.getDimension(R.dimen.medium_spacing).toInt() else resources.getDimension(R.dimen.medium_spacing).toInt()
} }
val endPadding = if (message.isOutgoing) resources.getDimension(R.dimen.medium_spacing).toInt() val endPadding = if (message.isOutgoing) resources.getDimension(R.dimen.medium_spacing).toInt()
else resources.getDimension(R.dimen.very_large_spacing).toInt() else resources.getDimension(R.dimen.very_large_spacing).toInt()
messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0) binding.messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0)
// Set inter-message spacing // Set inter-message spacing
setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster) setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster)
// Gravity // Gravity
val gravity = if (message.isOutgoing) Gravity.END else Gravity.START val gravity = if (message.isOutgoing) Gravity.END else Gravity.START
mainContainer.gravity = gravity or Gravity.BOTTOM binding.mainContainer.gravity = gravity or Gravity.BOTTOM
// Message status indicator // Message status indicator
val (iconID, iconColor) = getMessageStatusImage(message) val (iconID, iconColor) = getMessageStatusImage(message)
if (iconID != null) { if (iconID != null) {
@ -149,24 +166,24 @@ class VisibleMessageView : LinearLayout {
if (iconColor != null) { if (iconColor != null) {
drawable?.setTint(iconColor) drawable?.setTint(iconColor)
} }
messageStatusImageView.setImageDrawable(drawable) binding.messageStatusImageView.setImageDrawable(drawable)
} }
if (message.isOutgoing) { if (message.isOutgoing) {
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId) val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID binding.messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
} else { } else {
messageStatusImageView.isVisible = false binding.messageStatusImageView.isVisible = false
} }
// Expiration timer // Expiration timer
updateExpirationTimer(message) updateExpirationTimer(message)
// Calculate max message bubble width // Calculate max message bubble width
var maxWidth = screenWidth - startPadding - endPadding var maxWidth = screenWidth - startPadding - endPadding
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width } if (binding.profilePictureContainer.visibility != View.GONE) { maxWidth -= binding.profilePictureContainer.width }
// Populate content view // Populate content view
messageContentView.indexInAdapter = indexInAdapter binding.messageContentView.indexInAdapter = indexInAdapter
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false)) binding.messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false))
messageContentView.delegate = contentViewDelegate binding.messageContentView.delegate = contentViewDelegate
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() } onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() }
} }
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
@ -207,7 +224,7 @@ class VisibleMessageView : LinearLayout {
} }
private fun updateExpirationTimer(message: MessageRecord) { private fun updateExpirationTimer(message: MessageRecord) {
val expirationTimerViewLayoutParams = expirationTimerView.layoutParams as RelativeLayout.LayoutParams val expirationTimerViewLayoutParams = binding.expirationTimerView.layoutParams as RelativeLayout.LayoutParams
val ruleToAdd = if (message.isOutgoing) RelativeLayout.ALIGN_START else RelativeLayout.ALIGN_END val ruleToAdd = if (message.isOutgoing) RelativeLayout.ALIGN_START else RelativeLayout.ALIGN_END
val ruleToRemove = if (message.isOutgoing) RelativeLayout.ALIGN_END else RelativeLayout.ALIGN_START val ruleToRemove = if (message.isOutgoing) RelativeLayout.ALIGN_END else RelativeLayout.ALIGN_START
expirationTimerViewLayoutParams.removeRule(ruleToRemove) expirationTimerViewLayoutParams.removeRule(ruleToRemove)
@ -216,20 +233,20 @@ class VisibleMessageView : LinearLayout {
val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt() val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt()
expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0 expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0
expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize) expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize)
expirationTimerView.layoutParams = expirationTimerViewLayoutParams binding.expirationTimerView.layoutParams = expirationTimerViewLayoutParams
if (message.expiresIn > 0 && !message.isPending) { if (message.expiresIn > 0 && !message.isPending) {
expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme)) binding.expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
expirationTimerView.isVisible = true binding.expirationTimerView.isVisible = true
expirationTimerView.setPercentComplete(0.0f) binding.expirationTimerView.setPercentComplete(0.0f)
if (message.expireStarted > 0) { if (message.expireStarted > 0) {
expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn) binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
expirationTimerView.startAnimation() binding.expirationTimerView.startAnimation()
if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) { if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) {
ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule() ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule()
} }
} else if (!message.isMediaPending) { } else if (!message.isMediaPending) {
expirationTimerView.setPercentComplete(0.0f) binding.expirationTimerView.setPercentComplete(0.0f)
expirationTimerView.stopAnimation() binding.expirationTimerView.stopAnimation()
ThreadUtils.queue { ThreadUtils.queue {
val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager
val id = message.getId() val id = message.getId()
@ -238,11 +255,11 @@ class VisibleMessageView : LinearLayout {
expirationManager.scheduleDeletion(id, mms, message.expiresIn) expirationManager.scheduleDeletion(id, mms, message.expiresIn)
} }
} else { } else {
expirationTimerView.stopAnimation() binding.expirationTimerView.stopAnimation()
expirationTimerView.setPercentComplete(0.0f) binding.expirationTimerView.setPercentComplete(0.0f)
} }
} else { } else {
expirationTimerView.isVisible = false binding.expirationTimerView.isVisible = false
} }
} }
@ -255,14 +272,14 @@ class VisibleMessageView : LinearLayout {
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
if (translationX < 0 && !expirationTimerView.isVisible) { if (translationX < 0 && !binding.expirationTimerView.isVisible) {
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
val threshold = VisibleMessageView.swipeToReplyThreshold val threshold = swipeToReplyThreshold
val iconSize = toPx(24, context.resources) val iconSize = toPx(24, context.resources)
val bottomVOffset = paddingBottom + messageStatusImageView.height + (messageContentView.height - iconSize) / 2 val bottomVOffset = paddingBottom + binding.messageStatusImageView.height + (binding.messageContentView.height - iconSize) / 2
swipeToReplyIconRect.left = messageContentContainer.right - messageContentContainer.paddingEnd + spacing swipeToReplyIconRect.left = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + spacing
swipeToReplyIconRect.top = height - bottomVOffset - iconSize swipeToReplyIconRect.top = height - bottomVOffset - iconSize
swipeToReplyIconRect.right = messageContentContainer.right - messageContentContainer.paddingEnd + iconSize + spacing swipeToReplyIconRect.right = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + iconSize + spacing
swipeToReplyIconRect.bottom = height - bottomVOffset swipeToReplyIconRect.bottom = height - bottomVOffset
swipeToReplyIcon.bounds = swipeToReplyIconRect swipeToReplyIcon.bounds = swipeToReplyIconRect
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt() swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
@ -274,8 +291,8 @@ class VisibleMessageView : LinearLayout {
} }
fun recycle() { fun recycle() {
profilePictureView.recycle() binding.profilePictureView.recycle()
messageContentView.recycle() binding.messageContentView.recycle()
} }
// endregion // endregion
@ -296,13 +313,13 @@ class VisibleMessageView : LinearLayout {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
val newLongPressCallback = Runnable { onLongPress() } val newLongPressCallback = Runnable { onLongPress() }
this.longPressCallback = newLongPressCallback this.longPressCallback = newLongPressCallback
gestureHandler.postDelayed(newLongPressCallback, VisibleMessageView.longPressDurationThreshold) gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold)
onDownTimestamp = Date().time onDownTimestamp = Date().time
} }
private fun onMove(event: MotionEvent) { private fun onMove(event: MotionEvent) {
val translationX = toDp(event.rawX + dx, context.resources) val translationX = toDp(event.rawX + dx, context.resources)
if (abs(translationX) < VisibleMessageView.longPressMovementTreshold || snIsSelected) { if (abs(translationX) < longPressMovementThreshold || snIsSelected) {
return return
} else { } else {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
@ -313,20 +330,16 @@ class VisibleMessageView : LinearLayout {
val sign = -1.0f val sign = -1.0f
val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign
this.translationX = x this.translationX = x
this.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving binding.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving
postInvalidate() // Ensure onDraw(canvas:) is called postInvalidate() // Ensure onDraw(canvas:) is called
if (abs(x) > VisibleMessageView.swipeToReplyThreshold && abs(previousTranslationX) < VisibleMessageView.swipeToReplyThreshold) { if (abs(x) > swipeToReplyThreshold && abs(previousTranslationX) < swipeToReplyThreshold) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
} else {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
} }
previousTranslationX = x previousTranslationX = x
} }
private fun onCancel(event: MotionEvent) { private fun onCancel(event: MotionEvent) {
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) { if (abs(translationX) > swipeToReplyThreshold) {
onSwipeToReply?.invoke() onSwipeToReply?.invoke()
} }
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
@ -334,9 +347,9 @@ class VisibleMessageView : LinearLayout {
} }
private fun onUp(event: MotionEvent) { private fun onUp(event: MotionEvent) {
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) { if (abs(translationX) > swipeToReplyThreshold) {
onSwipeToReply?.invoke() onSwipeToReply?.invoke()
} else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) { } else if ((Date().time - onDownTimestamp) < longPressDurationThreshold) {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
val pressCallback = this.pressCallback val pressCallback = this.pressCallback
if (pressCallback != null) { if (pressCallback != null) {
@ -363,7 +376,7 @@ class VisibleMessageView : LinearLayout {
} }
.start() .start()
// Bit of a hack to keep the date break text view from moving // Bit of a hack to keep the date break text view from moving
dateBreakTextView.animate() binding.dateBreakTextView.animate()
.translationX(0.0f) .translationX(0.0f)
.setDuration(150) .setDuration(150)
.start() .start()
@ -375,7 +388,7 @@ class VisibleMessageView : LinearLayout {
} }
fun onContentClick(event: MotionEvent) { fun onContentClick(event: MotionEvent) {
messageContentView.onContentClick?.invoke(event) binding.messageContentView.onContentClick?.invoke(event)
} }
private fun onPress(event: MotionEvent) { private fun onPress(event: MotionEvent) {
@ -393,5 +406,9 @@ class VisibleMessageView : LinearLayout {
val activity = context as AppCompatActivity val activity = context as AppCompatActivity
userDetailsBottomSheet.show(activity.supportFragmentManager, userDetailsBottomSheet.tag) userDetailsBottomSheet.show(activity.supportFragmentManager, userDetailsBottomSheet.tag)
} }
fun playVoiceMessage() {
binding.messageContentView.playVoiceMessage()
}
// endregion // endregion
} }

View File

@ -9,8 +9,8 @@ import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.view_voice_message.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVoiceMessageBinding
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.audio.AudioSlidePlayer import org.thoughtcrime.securesms.audio.AudioSlidePlayer
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
@ -26,6 +26,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
@Inject lateinit var attachmentDb: AttachmentDatabase @Inject lateinit var attachmentDb: AttachmentDatabase
private lateinit var binding: ViewVoiceMessageBinding
private val cornerMask by lazy { CornerMask(this) } private val cornerMask by lazy { CornerMask(this) }
private var isPlaying = false private var isPlaying = false
set(value) { set(value) {
@ -44,8 +45,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_voice_message, this) binding = ViewVoiceMessageBinding.inflate(LayoutInflater.from(context), this, true)
voiceMessageViewDurationTextView.text = String.format("%01d:%02d", binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(0), TimeUnit.MILLISECONDS.toMinutes(0),
TimeUnit.MILLISECONDS.toSeconds(0)) TimeUnit.MILLISECONDS.toSeconds(0))
} }
@ -54,7 +55,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
// region Updating // region Updating
fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
val audio = message.slideDeck.audioSlide!! val audio = message.slideDeck.audioSlide!!
voiceMessageViewLoader.isVisible = audio.isInProgress binding.voiceMessageViewLoader.isVisible = audio.isInProgress
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
cornerMask.setTopLeftRadius(cornerRadii[0]) cornerMask.setTopLeftRadius(cornerRadii[0])
cornerMask.setTopRightRadius(cornerRadii[1]) cornerMask.setTopRightRadius(cornerRadii[1])
@ -74,8 +75,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras -> attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras ->
if (audioExtras.durationMs > 0) { if (audioExtras.durationMs > 0) {
duration = audioExtras.durationMs duration = audioExtras.durationMs
voiceMessageViewDurationTextView.visibility = View.VISIBLE binding.voiceMessageViewDurationTextView.visibility = View.VISIBLE
voiceMessageViewDurationTextView.text = String.format("%01d:%02d", binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs), TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs)) TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs))
} }
@ -99,12 +100,12 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
private fun handleProgressChanged(progress: Double) { private fun handleProgressChanged(progress: Double) {
this.progress = progress this.progress = progress
voiceMessageViewDurationTextView.text = String.format("%01d:%02d", binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()), TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()),
TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong())) TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong()))
val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams val layoutParams = binding.progressView.layoutParams as RelativeLayout.LayoutParams
layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt() layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
progressView.layoutParams = layoutParams binding.progressView.layoutParams = layoutParams
} }
override fun onPlayerStop(player: AudioSlidePlayer) { override fun onPlayerStop(player: AudioSlidePlayer) {
@ -118,7 +119,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
private fun renderIcon() { private fun renderIcon() {
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
voiceMessagePlaybackImageView.setImageResource(iconID) binding.voiceMessagePlaybackImageView.setImageResource(iconID)
} }
// endregion // endregion

View File

@ -5,11 +5,12 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_search_bottom_bar.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewSearchBottomBarBinding
class SearchBottomBar : LinearLayout { class SearchBottomBar : LinearLayout {
private lateinit var binding: ViewSearchBottomBarBinding
private var eventListener: EventListener? = null private var eventListener: EventListener? = null
// region Lifecycle // region Lifecycle
@ -18,10 +19,10 @@ class SearchBottomBar : LinearLayout {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
fun initialize() { fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_search_bottom_bar, this) binding = ViewSearchBottomBarBinding.inflate(LayoutInflater.from(context), this, true)
} }
fun setData(position: Int, count: Int) { fun setData(position: Int, count: Int) = with(binding) {
searchProgressWheel.visibility = GONE searchProgressWheel.visibility = GONE
searchUp.setOnClickListener { v: View? -> searchUp.setOnClickListener { v: View? ->
if (eventListener != null) { if (eventListener != null) {
@ -43,7 +44,7 @@ class SearchBottomBar : LinearLayout {
} }
fun showLoading() { fun showLoading() {
searchProgressWheel.visibility = VISIBLE binding.searchProgressWheel.visibility = VISIBLE
} }
private fun setViewEnabled(view: View, enabled: Boolean) { private fun setViewEnabled(view: View, enabled: Boolean) {

View File

@ -5,6 +5,7 @@ import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -13,8 +14,8 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import kotlinx.android.synthetic.main.thumbnail_view.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ThumbnailViewBinding
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.utilities.Util.equals import org.session.libsession.utilities.Util.equals
import org.session.libsignal.utilities.ListenableFuture import org.session.libsignal.utilities.ListenableFuture
@ -22,11 +23,13 @@ import org.session.libsignal.utilities.SettableFuture
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.*
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.mms.GlideRequest
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide
open class KThumbnailView: FrameLayout { open class KThumbnailView: FrameLayout {
private lateinit var binding: ThumbnailViewBinding
companion object { companion object {
private const val WIDTH = 0 private const val WIDTH = 0
private const val HEIGHT = 1 private const val HEIGHT = 1
@ -37,10 +40,10 @@ open class KThumbnailView: FrameLayout {
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
private val image by lazy { thumbnail_image } private val image by lazy { binding.thumbnailImage }
private val playOverlay by lazy { play_overlay } private val playOverlay by lazy { binding.playOverlay }
val loadIndicator: View by lazy { thumbnail_load_indicator } val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
val downloadIndicator: View by lazy { thumbnail_download_icon } val downloadIndicator: View by lazy { binding.thumbnailDownloadIcon }
private val dimensDelegate = ThumbnailDimensDelegate() private val dimensDelegate = ThumbnailDimensDelegate()
@ -48,7 +51,7 @@ open class KThumbnailView: FrameLayout {
private var radius: Int = 0 private var radius: Int = 0
private fun initialize(attrs: AttributeSet?) { private fun initialize(attrs: AttributeSet?) {
inflate(context, R.layout.thumbnail_view, this) binding = ThumbnailViewBinding.inflate(LayoutInflater.from(context), this)
if (attrs != null) { if (attrs != null) {
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0) val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)

View File

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

View File

@ -9,16 +9,18 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.util.TypedValue import android.util.TypedValue
import android.view.* import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter import androidx.fragment.app.FragmentPagerAdapter
import kotlinx.android.synthetic.main.activity_create_private_chat.*
import kotlinx.android.synthetic.main.fragment_enter_public_key.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityCreatePrivateChatBinding
import network.loki.messenger.databinding.FragmentEnterPublicKeyBinding
import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
@ -27,13 +29,13 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.PublicKeyValidation import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
private lateinit var binding: ActivityCreatePrivateChatBinding
private val adapter = CreatePrivateChatActivityAdapter(this) private val adapter = CreatePrivateChatActivityAdapter(this)
private var isKeyboardShowing = false private var isKeyboardShowing = false
set(value) { set(value) {
@ -47,37 +49,36 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivityCreatePrivateChatBinding.inflate(layoutInflater)
// Set content view // Set content view
setContentView(R.layout.activity_create_private_chat) setContentView(binding.root)
// Set title // Set title
supportActionBar!!.title = resources.getString(R.string.activity_create_private_chat_title) supportActionBar!!.title = resources.getString(R.string.activity_create_private_chat_title)
// Set up view pager // Set up view pager
viewPager.adapter = adapter binding.viewPager.adapter = adapter
tabLayout.setupWithViewPager(viewPager) binding.tabLayout.setupWithViewPager(binding.viewPager)
rootLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { binding.rootLayout.viewTreeObserver.addOnGlobalLayoutListener {
val diff = binding.rootLayout.rootView.height - binding.rootLayout.height
override fun onGlobalLayout() { val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics
val diff = rootLayout.rootView.height - rootLayout.height val estimatedKeyboardHeight =
val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics)
val estimatedKeyboardHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics) this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight)
this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight) }
}
})
} }
// endregion // endregion
// region Updating // region Updating
private fun showLoader() { private fun showLoader() {
loader.visibility = View.VISIBLE binding.loader.visibility = View.VISIBLE
loader.animate().setDuration(150).alpha(1.0f).start() binding.loader.animate().setDuration(150).alpha(1.0f).start()
} }
private fun hideLoader() { private fun hideLoader() {
loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation) super.onAnimationEnd(animation)
loader.visibility = View.GONE binding.loader.visibility = View.GONE
} }
}) })
} }
@ -156,6 +157,8 @@ private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatAc
// region Enter Public Key Fragment // region Enter Public Key Fragment
class EnterPublicKeyFragment : Fragment() { class EnterPublicKeyFragment : Fragment() {
private lateinit var binding: FragmentEnterPublicKeyBinding
var isKeyboardShowing = false var isKeyboardShowing = false
set(value) { field = value; handleIsKeyboardShowingChanged() } set(value) { field = value; handleIsKeyboardShowingChanged() }
@ -165,32 +168,34 @@ class EnterPublicKeyFragment : Fragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_enter_public_key, container, false) binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard with(binding) {
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT) publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
publicKeyEditText.setOnEditorActionListener { v, actionID, _ -> publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
if (actionID == EditorInfo.IME_ACTION_DONE) { publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager if (actionID == EditorInfo.IME_ACTION_DONE) {
imm.hideSoftInputFromWindow(v.windowToken, 0) val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
createPrivateChatIfPossible() imm.hideSoftInputFromWindow(v.windowToken, 0)
true createPrivateChatIfPossible()
} else { true
false } else {
false
}
} }
publicKeyTextView.text = hexEncodedPublicKey
copyButton.setOnClickListener { copyPublicKey() }
shareButton.setOnClickListener { sharePublicKey() }
createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() }
} }
publicKeyTextView.text = hexEncodedPublicKey
copyButton.setOnClickListener { copyPublicKey() }
shareButton.setOnClickListener { sharePublicKey() }
createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() }
} }
private fun handleIsKeyboardShowingChanged() { private fun handleIsKeyboardShowingChanged() {
val optionalContentContainer = optionalContentContainer ?: return binding.optionalContentContainer.isVisible = !isKeyboardShowing
optionalContentContainer.isVisible = !isKeyboardShowing
} }
private fun copyPublicKey() { private fun copyPublicKey() {
@ -209,7 +214,7 @@ class EnterPublicKeyFragment : Fragment() {
} }
private fun createPrivateChatIfPossible() { private fun createPrivateChatIfPossible() {
val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString() val hexEncodedPublicKey = binding.publicKeyEditText.text?.trim().toString()
val activity = requireActivity() as CreatePrivateChatActivity val activity = requireActivity() as CreatePrivateChatActivity
activity.createPrivateChatIfPossible(hexEncodedPublicKey) activity.createPrivateChatIfPossible(hexEncodedPublicKey)
} }

View File

@ -1,22 +1,23 @@
package org.thoughtcrime.securesms.groups package org.thoughtcrime.securesms.groups
import android.os.Bundle import android.os.Bundle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_closed_group_edit_bottom_sheet.* import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import network.loki.messenger.R import network.loki.messenger.databinding.FragmentClosedGroupEditBottomSheetBinding
public class ClosedGroupEditingOptionsBottomSheet : BottomSheetDialogFragment() { class ClosedGroupEditingOptionsBottomSheet : BottomSheetDialogFragment() {
private lateinit var binding: FragmentClosedGroupEditBottomSheetBinding
var onRemoveTapped: (() -> Unit)? = null var onRemoveTapped: (() -> Unit)? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_closed_group_edit_bottom_sheet, container, false) binding = FragmentClosedGroupEditBottomSheetBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
removeFromGroup.setOnClickListener { onRemoveTapped?.invoke() } binding.removeFromGroup.setOnClickListener { onRemoveTapped?.invoke() }
} }
} }

View File

@ -10,8 +10,8 @@ import android.widget.Toast
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_create_closed_group.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityCreateClosedGroupBinding
import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
@ -28,8 +28,8 @@ import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.fadeIn import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut import org.thoughtcrime.securesms.util.fadeOut
//TODO Refactor to avoid using kotlinx.android.synthetic
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> { class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
private lateinit var binding: ActivityCreateClosedGroupBinding
private var isLoading = false private var isLoading = false
set(newValue) { field = newValue; invalidateOptionsMenu() } set(newValue) { field = newValue; invalidateOptionsMenu() }
private var members = listOf<String>() private var members = listOf<String>()
@ -50,11 +50,12 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
setContentView(R.layout.activity_create_closed_group) binding = ActivityCreateClosedGroupBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar!!.title = resources.getString(R.string.activity_create_closed_group_title) supportActionBar!!.title = resources.getString(R.string.activity_create_closed_group_title)
recyclerView.adapter = this.selectContactsAdapter binding.recyclerView.adapter = this.selectContactsAdapter
recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() } binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
LoaderManager.getInstance(this).initLoader(0, null, this) LoaderManager.getInstance(this).initLoader(0, null, this)
} }
@ -80,8 +81,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
private fun update(members: List<String>) { private fun update(members: List<String>) {
//if there is a Note to self conversation, it loads self in the list, so we need to remove it here //if there is a Note to self conversation, it loads self in the list, so we need to remove it here
this.members = members.minus(publicKey) this.members = members.minus(publicKey)
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
invalidateOptionsMenu() invalidateOptionsMenu()
} }
// endregion // endregion
@ -95,12 +96,12 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
} }
private fun createNewPrivateChat() { private fun createNewPrivateChat() {
setResult(Companion.closedGroupCreatedResultCode) setResult(closedGroupCreatedResultCode)
finish() finish()
} }
private fun createClosedGroup() { private fun createClosedGroup() {
val name = nameEditText.text.trim() val name = binding.nameEditText.text.trim()
if (name.isEmpty()) { if (name.isEmpty()) {
return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show() return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
} }
@ -116,9 +117,9 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
} }
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!! val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
isLoading = true isLoading = true
loaderContainer.fadeIn() binding.loaderContainer.fadeIn()
MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
loaderContainer.fadeOut() binding.loaderContainer.fadeOut()
isLoading = false isLoading = false
val threadID = DatabaseComponent.get(this).threadDatabase().getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) val threadID = DatabaseComponent.get(this).threadDatabase().getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
if (!isFinishing) { if (!isFinishing) {
@ -126,7 +127,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
finish() finish()
} }
}.failUi { }.failUi {
loaderContainer.fadeOut() binding.loaderContainer.fadeOut()
isLoading = false isLoading = false
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show() Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
} }

View File

@ -8,12 +8,14 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.* import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.activity_settings.*
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task import nl.komponents.kovenant.task

View File

@ -13,62 +13,63 @@ import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.* import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.activity_join_public_chat.*
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.PublicKeyValidation import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.groups.DefaultGroupsViewModel import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.State import org.thoughtcrime.securesms.util.State
import java.util.* import java.util.Locale
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
private lateinit var binding: ActivityJoinPublicChatBinding
private val adapter = JoinPublicChatActivityAdapter(this) private val adapter = JoinPublicChatActivityAdapter(this)
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivityJoinPublicChatBinding.inflate(layoutInflater)
// Set content view // Set content view
setContentView(R.layout.activity_join_public_chat) setContentView(binding.root)
// Set title // Set title
supportActionBar!!.title = resources.getString(R.string.activity_join_public_chat_title) supportActionBar!!.title = resources.getString(R.string.activity_join_public_chat_title)
// Set up view pager // Set up view pager
viewPager.adapter = adapter binding.viewPager.adapter = adapter
tabLayout.setupWithViewPager(viewPager) binding.tabLayout.setupWithViewPager(binding.viewPager)
} }
// endregion // endregion
// region Updating // region Updating
private fun showLoader() { private fun showLoader() {
loader.visibility = View.VISIBLE binding.loader.visibility = View.VISIBLE
loader.animate().setDuration(150).alpha(1.0f).start() binding.loader.animate().setDuration(150).alpha(1.0f).start()
} }
private fun hideLoader() { private fun hideLoader() {
loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation) super.onAnimationEnd(animation)
loader.visibility = View.GONE binding.loader.visibility = View.GONE
} }
}) })
} }
@ -166,26 +167,28 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
// region Enter Chat URL Fragment // region Enter Chat URL Fragment
class EnterChatURLFragment : Fragment() { class EnterChatURLFragment : Fragment() {
private lateinit var binding: FragmentEnterChatUrlBinding
private val viewModel by activityViewModels<DefaultGroupsViewModel>() private val viewModel by activityViewModels<DefaultGroupsViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false) binding = FragmentEnterChatUrlBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard binding.chatURLEditText.imeOptions = binding.chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } binding.joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
viewModel.defaultRooms.observe(viewLifecycleOwner) { state -> viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
defaultRoomsContainer.isVisible = state is State.Success binding.defaultRoomsContainer.isVisible = state is State.Success
defaultRoomsLoaderContainer.isVisible = state is State.Loading binding.defaultRoomsLoaderContainer.isVisible = state is State.Loading
defaultRoomsLoader.isVisible = state is State.Loading binding.defaultRoomsLoader.isVisible = state is State.Loading
when (state) { when (state) {
State.Loading -> { State.Loading -> {
// TODO: Show a loader // TODO: Show a binding.loader
} }
is State.Error -> { is State.Error -> {
// TODO: Hide the loader // TODO: Hide the binding.loader
} }
is State.Success -> { is State.Success -> {
populateDefaultGroups(state.value) populateDefaultGroups(state.value)
@ -195,10 +198,10 @@ class EnterChatURLFragment : Fragment() {
} }
private fun populateDefaultGroups(groups: List<DefaultGroup>) { private fun populateDefaultGroups(groups: List<DefaultGroup>) {
defaultRoomsGridLayout.removeAllViews() binding.defaultRoomsGridLayout.removeAllViews()
defaultRoomsGridLayout.useDefaultMargins = false binding.defaultRoomsGridLayout.useDefaultMargins = false
groups.forEach { defaultGroup -> groups.forEach { defaultGroup ->
val chip = layoutInflater.inflate(R.layout.default_group_chip, defaultRoomsGridLayout, false) as Chip val chip = layoutInflater.inflate(R.layout.default_group_chip, binding.defaultRoomsGridLayout, false) as Chip
val drawable = defaultGroup.image?.let { bytes -> val drawable = defaultGroup.image?.let { bytes ->
val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size) val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size)
RoundedBitmapDrawableFactory.create(resources,bitmap).apply { RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
@ -210,18 +213,18 @@ class EnterChatURLFragment : Fragment() {
chip.setOnClickListener { chip.setOnClickListener {
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL) (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
} }
defaultRoomsGridLayout.addView(chip) binding.defaultRoomsGridLayout.addView(chip)
} }
if ((groups.size and 1) != 0) { // This checks that the number of rooms is even if ((groups.size and 1) != 0) { // This checks that the number of rooms is even
layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout) layoutInflater.inflate(R.layout.grid_layout_filler, binding.defaultRoomsGridLayout)
} }
} }
// region Convenience // region Convenience
private fun joinPublicChatIfPossible() { private fun joinPublicChatIfPossible() {
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0) inputMethodManager.hideSoftInputFromWindow(binding.chatURLEditText.windowToken, 0)
val chatURL = chatURLEditText.text.trim().toString().toLowerCase(Locale.US) val chatURL = binding.chatURLEditText.text.trim().toString().toLowerCase(Locale.US)
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL) (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
} }
// endregion // endregion

View File

@ -1,16 +1,16 @@
package org.thoughtcrime.securesms.groups package org.thoughtcrime.securesms.groups
import android.os.Bundle import android.os.Bundle
import kotlinx.android.synthetic.main.activity_open_group_guidelines.* import network.loki.messenger.databinding.ActivityOpenGroupGuidelinesBinding
import network.loki.messenger.R
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
class OpenGroupGuidelinesActivity : BaseActionBarActivity() { class OpenGroupGuidelinesActivity : BaseActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_open_group_guidelines) val binding = ActivityOpenGroupGuidelinesBinding.inflate(layoutInflater)
communityGuidelinesTextView.text = """ setContentView(binding.root)
binding.communityGuidelinesTextView.text = """
Welcome to Oxen. Welcome to Oxen.
Oxen believes privacy is an important part of our future. People have been safeguarding the right to privacy since the dawn of humanity, but the digital world has turned privacy into a privilege. Enough is enough. We're taking it back. For you. For us. For everyone. Oxen believes privacy is an important part of our future. People have been safeguarding the right to privacy since the dawn of humanity, but the digital world has turned privacy into a privilege. Enough is enough. We're taking it back. For you. For us. For everyone.

View File

@ -6,13 +6,12 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.* import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
import network.loki.messenger.R
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.UiModeUtilities
public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener { class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
private lateinit var binding: FragmentConversationBottomSheetBinding
//FIXME AC: Supplying a threadRecord directly into the field from an activity //FIXME AC: Supplying a threadRecord directly into the field from an activity
// is not the best idea. It doesn't survive configuration change. // is not the best idea. It doesn't survive configuration change.
// We should be dealing with IDs and all sorts of serializable data instead // We should be dealing with IDs and all sorts of serializable data instead
@ -29,20 +28,21 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.
var onSetMuteTapped: ((Boolean) -> Unit)? = null var onSetMuteTapped: ((Boolean) -> Unit)? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_conversation_bottom_sheet, container, false) binding = FragmentConversationBottomSheetBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onClick(v: View?) { override fun onClick(v: View?) {
when (v) { when (v) {
detailsTextView -> onViewDetailsTapped?.invoke() binding.detailsTextView -> onViewDetailsTapped?.invoke()
pinTextView -> onPinTapped?.invoke() binding.pinTextView -> onPinTapped?.invoke()
unpinTextView -> onUnpinTapped?.invoke() binding.unpinTextView -> onUnpinTapped?.invoke()
blockTextView -> onBlockTapped?.invoke() binding.blockTextView -> onBlockTapped?.invoke()
unblockTextView -> onUnblockTapped?.invoke() binding.unblockTextView -> onUnblockTapped?.invoke()
deleteTextView -> onDeleteTapped?.invoke() binding.deleteTextView -> onDeleteTapped?.invoke()
notificationsTextView -> onNotificationTapped?.invoke() binding.notificationsTextView -> onNotificationTapped?.invoke()
unMuteNotificationsTextView -> onSetMuteTapped?.invoke(false) binding.unMuteNotificationsTextView -> onSetMuteTapped?.invoke(false)
muteNotificationsTextView -> onSetMuteTapped?.invoke(true) binding.muteNotificationsTextView -> onSetMuteTapped?.invoke(true)
} }
} }
@ -51,26 +51,26 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.
if (!this::thread.isInitialized) { return dismiss() } if (!this::thread.isInitialized) { return dismiss() }
val recipient = thread.recipient val recipient = thread.recipient
if (!recipient.isGroupRecipient && !recipient.isLocalNumber) { if (!recipient.isGroupRecipient && !recipient.isLocalNumber) {
detailsTextView.visibility = View.VISIBLE binding.detailsTextView.visibility = View.VISIBLE
unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE binding.unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE
blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE binding.blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE
detailsTextView.setOnClickListener(this) binding.detailsTextView.setOnClickListener(this)
blockTextView.setOnClickListener(this) binding.blockTextView.setOnClickListener(this)
unblockTextView.setOnClickListener(this) binding.unblockTextView.setOnClickListener(this)
} else { } else {
detailsTextView.visibility = View.GONE binding.detailsTextView.visibility = View.GONE
} }
unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
unMuteNotificationsTextView.setOnClickListener(this) binding.unMuteNotificationsTextView.setOnClickListener(this)
muteNotificationsTextView.setOnClickListener(this) binding.muteNotificationsTextView.setOnClickListener(this)
notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted binding.notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
notificationsTextView.setOnClickListener(this) binding.notificationsTextView.setOnClickListener(this)
deleteTextView.setOnClickListener(this) binding.deleteTextView.setOnClickListener(this)
pinTextView.isVisible = !thread.isPinned binding.pinTextView.isVisible = !thread.isPinned
unpinTextView.isVisible = thread.isPinned binding.unpinTextView.isVisible = thread.isPinned
pinTextView.setOnClickListener(this) binding.pinTextView.setOnClickListener(this)
unpinTextView.setOnClickListener(this) binding.unpinTextView.setOnClickListener(this)
} }
override fun onStart() { override fun onStart() {

View File

@ -11,8 +11,8 @@ import android.widget.LinearLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.view_conversation.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewConversationBinding
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.RecipientDatabase
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale import java.util.Locale
class ConversationView : LinearLayout { class ConversationView : LinearLayout {
private lateinit var binding: ViewConversationBinding
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
var thread: ThreadRecord? = null var thread: ThreadRecord? = null
@ -31,7 +32,7 @@ class ConversationView : LinearLayout {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_conversation, this) binding = ViewConversationBinding.inflate(LayoutInflater.from(context), this, true)
layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT) layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
} }
// endregion // endregion
@ -39,83 +40,83 @@ class ConversationView : LinearLayout {
// region Updating // region Updating
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) { fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
this.thread = thread this.thread = thread
if (thread.isPinned) { background = if (thread.isPinned) {
conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0) binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0)
background = ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background) ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background)
} else { } else {
conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
background = ContextCompat.getDrawable(context, R.drawable.conversation_view_background) ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
} }
profilePictureView.glide = glide binding.profilePictureView.glide = glide
val unreadCount = thread.unreadCount val unreadCount = thread.unreadCount
if (thread.recipient.isBlocked) { if (thread.recipient.isBlocked) {
accentView.setBackgroundResource(R.color.destructive) binding.accentView.setBackgroundResource(R.color.destructive)
accentView.visibility = View.VISIBLE binding.accentView.visibility = View.VISIBLE
} else { } else {
accentView.setBackgroundResource(R.color.accent) binding.accentView.setBackgroundResource(R.color.accent)
// Using thread.isRead we can determine if the last message was our own, and display it as 'read' even though previous messages may not be // Using thread.isRead we can determine if the last message was our own, and display it as 'read' even though previous messages may not be
// This would also not trigger the disappearing message timer which may or may not be desirable // This would also not trigger the disappearing message timer which may or may not be desirable
accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE binding.accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE
} }
val formattedUnreadCount = if (thread.isRead) { val formattedUnreadCount = if (thread.isRead) {
null null
} else { } else {
if (unreadCount < 100) unreadCount.toString() else "99+" if (unreadCount < 100) unreadCount.toString() else "99+"
} }
unreadCountTextView.text = formattedUnreadCount binding.unreadCountTextView.text = formattedUnreadCount
val textSize = if (unreadCount < 100) 12.0f else 9.0f val textSize = if (unreadCount < 100) 12.0f else 9.0f
unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL) binding.unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL)
unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead) binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead)
val senderDisplayName = getUserDisplayName(thread.recipient) val senderDisplayName = getUserDisplayName(thread.recipient)
?: thread.recipient.address.toString() ?: thread.recipient.address.toString()
conversationViewDisplayNameTextView.text = senderDisplayName binding.conversationViewDisplayNameTextView.text = senderDisplayName
timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date) binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
val recipient = thread.recipient val recipient = thread.recipient
muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL
val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) { val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
R.drawable.ic_outline_notifications_off_24 R.drawable.ic_outline_notifications_off_24
} else { } else {
R.drawable.ic_notifications_mentions R.drawable.ic_notifications_mentions
} }
muteIndicatorImageView.setImageResource(drawableRes) binding.muteIndicatorImageView.setImageResource(drawableRes)
val rawSnippet = thread.getDisplayBody(context) val rawSnippet = thread.getDisplayBody(context)
val snippet = highlightMentions(rawSnippet, thread.threadId, context) val snippet = highlightMentions(rawSnippet, thread.threadId, context)
snippetTextView.text = snippet binding.snippetTextView.text = snippet
snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
if (isTyping) { if (isTyping) {
typingIndicatorView.startAnimation() binding.typingIndicatorView.startAnimation()
} else { } else {
typingIndicatorView.stopAnimation() binding.typingIndicatorView.stopAnimation()
} }
typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE binding.typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE
statusIndicatorImageView.visibility = View.VISIBLE binding.statusIndicatorImageView.visibility = View.VISIBLE
when { when {
!thread.isOutgoing -> statusIndicatorImageView.visibility = View.GONE !thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE
thread.isFailed -> { thread.isFailed -> {
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_error)?.mutate() val drawable = ContextCompat.getDrawable(context, R.drawable.ic_error)?.mutate()
drawable?.setTint(ContextCompat.getColor(context, R.color.destructive)) drawable?.setTint(ContextCompat.getColor(context, R.color.destructive))
statusIndicatorImageView.setImageDrawable(drawable) binding.statusIndicatorImageView.setImageDrawable(drawable)
} }
thread.isPending -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot) thread.isPending -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot)
thread.isRead -> statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check) thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
else -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check) else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
} }
post { post {
profilePictureView.update(thread.recipient, thread.threadId) binding.profilePictureView.update(thread.recipient)
} }
} }
fun recycle() { fun recycle() {
profilePictureView.recycle() binding.profilePictureView.recycle()
} }
private fun getUserDisplayName(recipient: Recipient): String? { private fun getUserDisplayName(recipient: Recipient): String? {
if (recipient.isLocalNumber) { return if (recipient.isLocalNumber) {
return context.getString(R.string.note_to_self) context.getString(R.string.note_to_self)
} else { } else {
return recipient.name // Internally uses the Contact API recipient.name // Internally uses the Contact API
} }
} }
// endregion // endregion

View File

@ -10,7 +10,6 @@ import android.os.Bundle
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -19,24 +18,22 @@ import androidx.lifecycle.lifecycleScope
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.android.synthetic.main.seed_reminder_stub.*
import kotlinx.android.synthetic.main.seed_reminder_stub.view.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityHomeBinding
import network.loki.messenger.databinding.SeedReminderStubBinding
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.utilities.* import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.Util import org.session.libsession.utilities.ProfilePictureModifiedEvent
import org.session.libsignal.utilities.ThreadUtils import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.MuteDialog import org.thoughtcrime.securesms.MuteDialog
@ -58,13 +55,20 @@ import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.onboarding.SeedActivity import org.thoughtcrime.securesms.onboarding.SeedActivity
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.util.* import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.getColorWithID
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener,
SeedReminderViewDelegate, NewConversationButtonSetViewDelegate, LoaderManager.LoaderCallbacks<Cursor> { SeedReminderViewDelegate, NewConversationButtonSetViewDelegate, LoaderManager.LoaderCallbacks<Cursor> {
private lateinit var binding: ActivityHomeBinding
private lateinit var glide: GlideRequests private lateinit var glide: GlideRequests
private var broadcastReceiver: BroadcastReceiver? = null private var broadcastReceiver: BroadcastReceiver? = null
@ -75,57 +79,57 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
private val publicKey: String private val publicKey: String
get() = TextSecurePreferences.getLocalNumber(this)!! get() = TextSecurePreferences.getLocalNumber(this)!!
private val homeAdapter:HomeAdapter by lazy { private val homeAdapter: HomeAdapter by lazy {
HomeAdapter(this, threadDb.conversationList) HomeAdapter(context = this, cursor = threadDb.conversationList, listener = this)
} }
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
// Set content view // Set content view
setContentView(R.layout.activity_home) binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
// Set custom toolbar // Set custom toolbar
setSupportActionBar(toolbar) setSupportActionBar(binding.toolbar)
// Set up Glide // Set up Glide
glide = GlideApp.with(this) glide = GlideApp.with(this)
// Set up toolbar buttons // Set up toolbar buttons
profileButton.glide = glide binding.profileButton.glide = glide
profileButton.setOnClickListener { openSettings() } binding.profileButton.setOnClickListener { openSettings() }
pathStatusViewContainer.disableClipping() binding.pathStatusViewContainer.disableClipping()
pathStatusViewContainer.setOnClickListener { showPath() } binding.pathStatusViewContainer.setOnClickListener { showPath() }
// Set up seed reminder view // Set up seed reminder view
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
if (!hasViewedSeed) { if (!hasViewedSeed) {
seedReminderStub.inflate().apply { binding.seedReminderStub.setOnInflateListener { _, inflated ->
val seedReminderView = this.seedReminderView val stubBinding = SeedReminderStubBinding.bind(inflated)
val seedReminderViewTitle = SpannableString("You're almost finished! 80%") // Intentionally not yet translated 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) seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
seedReminderView.title = seedReminderViewTitle stubBinding.seedReminderView.title = seedReminderViewTitle
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1) stubBinding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
seedReminderView.setProgress(80, false) stubBinding.seedReminderView.setProgress(80, false)
seedReminderView.delegate = this@HomeActivity stubBinding.seedReminderView.delegate = this@HomeActivity
} }
binding.seedReminderStub.inflate()
} else { } else {
seedReminderStub.isVisible = false binding.seedReminderStub.isVisible = false
} }
// Set up recycler view // Set up recycler view
homeAdapter.setHasStableIds(true) homeAdapter.setHasStableIds(true)
homeAdapter.glide = glide homeAdapter.glide = glide
homeAdapter.conversationClickListener = this binding.recyclerView.adapter = homeAdapter
recyclerView.adapter = homeAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
// Set up empty state view // Set up empty state view
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() } binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
IP2Country.configureIfNeeded(this@HomeActivity) IP2Country.configureIfNeeded(this@HomeActivity)
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will) // This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
LoaderManager.getInstance(this).restartLoader(0, null, this) LoaderManager.getInstance(this).restartLoader(0, null, this)
// Set up new conversation button set // Set up new conversation button set
newConversationButtonSet.delegate = this binding.newConversationButtonSet.delegate = this
// Observe blocked contacts changed events // Observe blocked contacts changed events
val broadcastReceiver = object : BroadcastReceiver() { val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
recyclerView.adapter!!.notifyDataSetChanged() binding.recyclerView.adapter!!.notifyDataSetChanged()
} }
} }
this.broadcastReceiver = broadcastReceiver this.broadcastReceiver = broadcastReceiver
@ -138,7 +142,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
// Set up typing observer // Set up typing observer
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this@HomeActivity, Observer<Set<Long>> { threadIDs -> ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this@HomeActivity, Observer<Set<Long>> { threadIDs ->
val adapter = recyclerView.adapter as HomeAdapter val adapter = binding.recyclerView.adapter as HomeAdapter
adapter.typingThreadIDs = threadIDs ?: setOf() adapter.typingThreadIDs = threadIDs ?: setOf()
}) })
updateProfileButton() updateProfileButton()
@ -177,11 +181,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true) ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared
IdentityKeyUtil.checkUpdate(this) IdentityKeyUtil.checkUpdate(this)
profileButton.recycle() // clear cached image before update tje profilePictureView binding.profileButton.recycle() // clear cached image before update tje profilePictureView
profileButton.update() binding.profileButton.update()
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
if (hasViewedSeed) { if (hasViewedSeed) {
seedReminderView?.isVisible = false binding.seedReminderStub.isVisible = false
} }
if (TextSecurePreferences.getConfigurationMessageSynced(this)) { if (TextSecurePreferences.getConfigurationMessageSynced(this)) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@ -214,8 +218,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
// region Updating // region Updating
private fun updateEmptyState() { private fun updateEmptyState() {
val threadCount = (recyclerView.adapter as HomeAdapter).itemCount val threadCount = (binding.recyclerView.adapter as HomeAdapter).itemCount
emptyStateContainer.visibility = if (threadCount == 0) View.VISIBLE else View.GONE binding.emptyStateContainer.isVisible = threadCount == 0
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
@ -226,10 +230,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
} }
private fun updateProfileButton() { private fun updateProfileButton() {
profileButton.publicKey = publicKey binding.profileButton.publicKey = publicKey
profileButton.displayName = TextSecurePreferences.getProfileName(this) binding.profileButton.displayName = TextSecurePreferences.getProfileName(this)
profileButton.recycle() binding.profileButton.recycle()
profileButton.update() binding.profileButton.update()
} }
// endregion // endregion
@ -239,13 +243,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
show(intent) show(intent)
} }
override fun onConversationClick(view: ConversationView) { override fun onConversationClick(thread: ThreadRecord) {
val thread = view.thread ?: return val intent = Intent(this, ConversationActivityV2::class.java)
openConversation(thread) intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId)
push(intent)
} }
override fun onLongConversationClick(view: ConversationView) { override fun onLongConversationClick(thread: ThreadRecord) {
val thread = view.thread ?: return
val bottomSheet = ConversationOptionsBottomSheet() val bottomSheet = ConversationOptionsBottomSheet()
bottomSheet.thread = thread bottomSheet.thread = thread
bottomSheet.onViewDetailsTapped = { bottomSheet.onViewDetailsTapped = {
@ -286,15 +290,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
} }
bottomSheet.onPinTapped = { bottomSheet.onPinTapped = {
bottomSheet.dismiss() bottomSheet.dismiss()
if (!thread.isPinned) { setConversationPinned(thread.threadId, true)
pinConversation(thread)
}
} }
bottomSheet.onUnpinTapped = { bottomSheet.onUnpinTapped = {
bottomSheet.dismiss() bottomSheet.dismiss()
if (thread.isPinned) { setConversationPinned(thread.threadId, false)
unpinConversation(thread)
}
} }
bottomSheet.show(supportFragmentManager, bottomSheet.tag) bottomSheet.show(supportFragmentManager, bottomSheet.tag)
} }
@ -305,10 +305,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
.setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact) .setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ -> .setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
ThreadUtils.queue { lifecycleScope.launch(Dispatchers.IO) {
recipientDatabase.setBlocked(thread.recipient, true) recipientDatabase.setBlocked(thread.recipient, true)
Util.runOnMain { withContext(Dispatchers.Main) {
recyclerView.adapter!!.notifyDataSetChanged() binding.recyclerView.adapter!!.notifyDataSetChanged()
dialog.dismiss() dialog.dismiss()
} }
} }
@ -321,10 +321,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
.setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact) .setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ -> .setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
ThreadUtils.queue { lifecycleScope.launch(Dispatchers.IO) {
recipientDatabase.setBlocked(thread.recipient, false) recipientDatabase.setBlocked(thread.recipient, false)
Util.runOnMain { withContext(Dispatchers.Main) {
recyclerView.adapter!!.notifyDataSetChanged() binding.recyclerView.adapter!!.notifyDataSetChanged()
dialog.dismiss() dialog.dismiss()
} }
} }
@ -333,18 +333,18 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
private fun setConversationMuted(thread: ThreadRecord, isMuted: Boolean) { private fun setConversationMuted(thread: ThreadRecord, isMuted: Boolean) {
if (!isMuted) { if (!isMuted) {
ThreadUtils.queue { lifecycleScope.launch(Dispatchers.IO) {
recipientDatabase.setMuted(thread.recipient, 0) recipientDatabase.setMuted(thread.recipient, 0)
Util.runOnMain { withContext(Dispatchers.Main) {
recyclerView.adapter!!.notifyDataSetChanged() binding.recyclerView.adapter!!.notifyDataSetChanged()
} }
} }
} else { } else {
MuteDialog.show(this) { until: Long -> MuteDialog.show(this) { until: Long ->
ThreadUtils.queue { lifecycleScope.launch(Dispatchers.IO) {
recipientDatabase.setMuted(thread.recipient, until) recipientDatabase.setMuted(thread.recipient, until)
Util.runOnMain { withContext(Dispatchers.Main) {
recyclerView.adapter!!.notifyDataSetChanged() binding.recyclerView.adapter!!.notifyDataSetChanged()
} }
} }
} }
@ -352,28 +352,19 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
} }
private fun setNotifyType(thread: ThreadRecord, newNotifyType: Int) { private fun setNotifyType(thread: ThreadRecord, newNotifyType: Int) {
ThreadUtils.queue { lifecycleScope.launch(Dispatchers.IO) {
recipientDatabase.setNotifyType(thread.recipient, newNotifyType) recipientDatabase.setNotifyType(thread.recipient, newNotifyType)
Util.runOnMain { withContext(Dispatchers.Main) {
recyclerView.adapter!!.notifyDataSetChanged() binding.recyclerView.adapter!!.notifyDataSetChanged()
} }
} }
} }
private fun pinConversation(thread: ThreadRecord) { private fun setConversationPinned(threadId: Long, pinned: Boolean) {
ThreadUtils.queue { lifecycleScope.launch(Dispatchers.IO) {
threadDb.setPinned(thread.threadId, true) threadDb.setPinned(threadId, pinned)
Util.runOnMain { withContext(Dispatchers.Main) {
LoaderManager.getInstance(this).restartLoader(0, null, this) LoaderManager.getInstance(this@HomeActivity).restartLoader(0, null, this@HomeActivity)
}
}
}
private fun unpinConversation(thread: ThreadRecord) {
ThreadUtils.queue {
threadDb.setPinned(thread.threadId, false)
Util.runOnMain {
LoaderManager.getInstance(this).restartLoader(0, null, this)
} }
} }
} }
@ -381,16 +372,15 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
private fun deleteConversation(thread: ThreadRecord) { private fun deleteConversation(thread: ThreadRecord) {
val threadID = thread.threadId val threadID = thread.threadId
val recipient = thread.recipient val recipient = thread.recipient
val message: String val message = if (recipient.isGroupRecipient) {
if (recipient.isGroupRecipient) {
val group = groupDatabase.getGroup(recipient.address.toString()).orNull() val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) { if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) {
message = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
} else { } else {
message = resources.getString(R.string.activity_home_leave_group_dialog_message) resources.getString(R.string.activity_home_leave_group_dialog_message)
} }
} else { } else {
message = resources.getString(R.string.activity_home_delete_conversation_dialog_message) resources.getString(R.string.activity_home_delete_conversation_dialog_message)
} }
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
dialog.setMessage(message) dialog.setMessage(message)
@ -419,7 +409,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
if (v2OpenGroup != null) { if (v2OpenGroup != null) {
OpenGroupManager.delete(v2OpenGroup.server, v2OpenGroup.room, this@HomeActivity) OpenGroupManager.delete(v2OpenGroup.server, v2OpenGroup.room, this@HomeActivity)
} else { } else {
ThreadUtils.queue { lifecycleScope.launch(Dispatchers.IO) {
threadDb.deleteConversation(threadID) threadDb.deleteConversation(threadID)
} }
} }
@ -436,12 +426,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
dialog.create().show() dialog.create().show()
} }
private fun openConversation(thread: ThreadRecord) {
val intent = Intent(this, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId)
push(intent)
}
private fun openSettings() { private fun openSettings() {
val intent = Intent(this, SettingsActivity::class.java) val intent = Intent(this, SettingsActivity::class.java)
show(intent, isForResult = true) show(intent, isForResult = true)

View File

@ -9,20 +9,23 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
class HomeAdapter(context: Context, cursor: Cursor?) : CursorRecyclerViewAdapter<HomeAdapter.ViewHolder>(context, cursor) { class HomeAdapter(
context: Context,
cursor: Cursor?,
val listener: ConversationClickListener
) : CursorRecyclerViewAdapter<HomeAdapter.ViewHolder>(context, cursor) {
private val threadDatabase = DatabaseComponent.get(context).threadDatabase() private val threadDatabase = DatabaseComponent.get(context).threadDatabase()
lateinit var glide: GlideRequests lateinit var glide: GlideRequests
var typingThreadIDs = setOf<Long>() var typingThreadIDs = setOf<Long>()
set(value) { field = value; notifyDataSetChanged() } set(value) { field = value; notifyDataSetChanged() }
var conversationClickListener: ConversationClickListener? = null
class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view) class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = ConversationView(context) val view = ConversationView(context)
view.setOnClickListener { conversationClickListener?.onConversationClick(view) } view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } }
view.setOnLongClickListener { view.setOnLongClickListener {
conversationClickListener?.onLongConversationClick(view) view.thread?.let { listener.onLongConversationClick(it) }
true true
} }
return ViewHolder(view) return ViewHolder(view)
@ -45,6 +48,6 @@ class HomeAdapter(context: Context, cursor: Cursor?) : CursorRecyclerViewAdapter
} }
interface ConversationClickListener { interface ConversationClickListener {
fun onConversationClick(view: ConversationView) fun onConversationClick(thread: ThreadRecord)
fun onLongConversationClick(view: ConversationView) fun onLongConversationClick(thread: ThreadRecord)
} }

View File

@ -17,26 +17,33 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import kotlinx.android.synthetic.main.activity_path.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityPathBinding
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.Snode
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.util.*
import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.PathDotView import org.thoughtcrime.securesms.util.PathDotView
import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.util.animateSizeChange
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut
import org.thoughtcrime.securesms.util.getColorWithID
class PathActivity : PassphraseRequiredActionBarActivity() { class PathActivity : PassphraseRequiredActionBarActivity() {
private lateinit var binding: ActivityPathBinding
private val broadcastReceivers = mutableListOf<BroadcastReceiver>() private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
setContentView(R.layout.activity_path) binding = ActivityPathBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar!!.title = resources.getString(R.string.activity_path_title) supportActionBar!!.title = resources.getString(R.string.activity_path_title)
pathRowsContainer.disableClipping() binding.pathRowsContainer.disableClipping()
learnMoreButton.setOnClickListener { learnMore() } binding.learnMoreButton.setOnClickListener { learnMore() }
update(false) update(false)
registerObservers() registerObservers()
} }
@ -82,7 +89,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
private fun handleOnionRequestPathCountriesLoaded() { update(false) } private fun handleOnionRequestPathCountriesLoaded() { update(false) }
private fun update(isAnimated: Boolean) { private fun update(isAnimated: Boolean) {
pathRowsContainer.removeAllViews() binding.pathRowsContainer.removeAllViews()
if (OnionRequestAPI.paths.isNotEmpty()) { if (OnionRequestAPI.paths.isNotEmpty()) {
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish() val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000 val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000
@ -94,18 +101,18 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
val destinationRow = getPathRow(resources.getString(R.string.activity_path_destination_row_title), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval) val destinationRow = getPathRow(resources.getString(R.string.activity_path_destination_row_title), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
val rows = listOf( youRow ) + pathRows + listOf( destinationRow ) val rows = listOf( youRow ) + pathRows + listOf( destinationRow )
for (row in rows) { for (row in rows) {
pathRowsContainer.addView(row) binding.pathRowsContainer.addView(row)
} }
if (isAnimated) { if (isAnimated) {
spinner.fadeOut() binding.spinner.fadeOut()
} else { } else {
spinner.alpha = 0.0f binding.spinner.alpha = 0.0f
} }
} else { } else {
if (isAnimated) { if (isAnimated) {
spinner.fadeIn() binding.spinner.fadeIn()
} else { } else {
spinner.alpha = 1.0f binding.spinner.alpha = 1.0f
} }
} }
} }

View File

@ -14,10 +14,9 @@ import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.EntryPoint
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_user_details_bottom_sheet.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentUserDetailsBottomSheetBinding
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
@ -34,13 +33,15 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
@Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var threadDb: ThreadDatabase
private lateinit var binding: FragmentUserDetailsBottomSheetBinding
companion object { companion object {
const val ARGUMENT_PUBLIC_KEY = "publicKey" const val ARGUMENT_PUBLIC_KEY = "publicKey"
const val ARGUMENT_THREAD_ID = "threadId" const val ARGUMENT_THREAD_ID = "threadId"
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_user_details_bottom_sheet, container, false) binding = FragmentUserDetailsBottomSheetBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -49,58 +50,62 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
val threadID = arguments?.getLong(ARGUMENT_THREAD_ID) ?: return dismiss() val threadID = arguments?.getLong(ARGUMENT_THREAD_ID) ?: return dismiss()
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false) val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss() val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
profilePictureView.publicKey = publicKey with(binding) {
profilePictureView.glide = GlideApp.with(this) profilePictureView.publicKey = publicKey
profilePictureView.isLarge = true profilePictureView.glide = GlideApp.with(this@UserDetailsBottomSheet)
profilePictureView.update(recipient, -1) profilePictureView.isLarge = true
nameTextViewContainer.visibility = View.VISIBLE profilePictureView.update(recipient)
nameTextViewContainer.setOnClickListener {
nameTextViewContainer.visibility = View.INVISIBLE
nameEditTextContainer.visibility = View.VISIBLE
nicknameEditText.text = null
nicknameEditText.requestFocus()
showSoftKeyboard()
}
cancelNicknameEditingButton.setOnClickListener {
nicknameEditText.clearFocus()
hideSoftKeyboard()
nameTextViewContainer.visibility = View.VISIBLE nameTextViewContainer.visibility = View.VISIBLE
nameEditTextContainer.visibility = View.INVISIBLE nameTextViewContainer.setOnClickListener {
} nameTextViewContainer.visibility = View.INVISIBLE
saveNicknameButton.setOnClickListener { nameEditTextContainer.visibility = View.VISIBLE
saveNickName(recipient) nicknameEditText.text = null
} nicknameEditText.requestFocus()
nicknameEditText.setOnEditorActionListener { _, actionId, _ -> showSoftKeyboard()
when (actionId) {
EditorInfo.IME_ACTION_DONE -> {
saveNickName(recipient)
return@setOnEditorActionListener true
}
else -> return@setOnEditorActionListener false
} }
} cancelNicknameEditingButton.setOnClickListener {
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally nicknameEditText.clearFocus()
hideSoftKeyboard()
nameTextViewContainer.visibility = View.VISIBLE
nameEditTextContainer.visibility = View.INVISIBLE
}
saveNicknameButton.setOnClickListener {
saveNickName(recipient)
}
nicknameEditText.setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE -> {
saveNickName(recipient)
return@setOnEditorActionListener true
}
else -> return@setOnEditorActionListener false
}
}
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient messageButton.isVisible = !threadRecipient.isOpenGroupRecipient
publicKeyTextView.text = publicKey publicKeyTextView.text = publicKey
publicKeyTextView.setOnLongClickListener { publicKeyTextView.setOnLongClickListener {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard =
val clip = ClipData.newPlainText("Session ID", publicKey) requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(clip) val clip = ClipData.newPlainText("Session ID", publicKey)
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() clipboard.setPrimaryClip(clip)
true Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
} .show()
messageButton.setOnClickListener { true
val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient) }
val intent = Intent( messageButton.setOnClickListener {
context, val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient)
ConversationActivityV2::class.java val intent = Intent(
) context,
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) ConversationActivityV2::class.java
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1) )
startActivity(intent) intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
dismiss() intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1)
startActivity(intent)
dismiss()
}
} }
} }
@ -111,7 +116,7 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
window.setDimAmount(if (isLightMode) 0.1f else 0.75f) window.setDimAmount(if (isLightMode) 0.1f else 0.75f)
} }
fun saveNickName(recipient: Recipient) { fun saveNickName(recipient: Recipient) = with(binding) {
nicknameEditText.clearFocus() nicknameEditText.clearFocus()
hideSoftKeyboard() hideSoftKeyboard()
nameTextViewContainer.visibility = View.VISIBLE nameTextViewContainer.visibility = View.VISIBLE
@ -131,11 +136,11 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
@SuppressLint("ServiceCast") @SuppressLint("ServiceCast")
fun showSoftKeyboard() { fun showSoftKeyboard() {
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.showSoftInput(nicknameEditText, 0) imm?.showSoftInput(binding.nicknameEditText, 0)
} }
fun hideSoftKeyboard() { fun hideSoftKeyboard() {
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.hideSoftInputFromWindow(nicknameEditText.windowToken, 0) imm?.hideSoftInputFromWindow(binding.nicknameEditText.windowToken, 0)
} }
} }

View File

@ -1,7 +1,7 @@
package org.thoughtcrime.securesms.mediasend; package org.thoughtcrime.securesms.mediasend;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProvider;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Matrix; import android.graphics.Matrix;
@ -80,7 +80,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
controller = (Controller) getActivity(); controller = (Controller) getActivity();
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this); camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE); orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
} }
@Nullable @Nullable

View File

@ -1,7 +1,7 @@
package org.thoughtcrime.securesms.mediasend; package org.thoughtcrime.securesms.mediasend;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProvider;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Point; import android.graphics.Point;
@ -66,7 +66,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
bucketId = getArguments().getString(KEY_BUCKET_ID); bucketId = getArguments().getString(KEY_BUCKET_ID);
folderTitle = getArguments().getString(KEY_FOLDER_TITLE); folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
maxSelection = getArguments().getInt(KEY_MAX_SELECTION); maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
} }
@Override @Override

View File

@ -1,7 +1,7 @@
package org.thoughtcrime.securesms.mediasend; package org.thoughtcrime.securesms.mediasend;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProvider;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Rect; import android.graphics.Rect;
@ -313,7 +313,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
} }
private void initViewModel() { private void initViewModel() {
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
viewModel.getSelectedMedia().observe(this, media -> { viewModel.getSelectedMedia().observe(this, media -> {
if (Util.isEmpty(media)) { if (Util.isEmpty(media)) {

View File

@ -7,8 +7,8 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.TextView.OnEditorActionListener import android.widget.TextView.OnEditorActionListener
import android.widget.Toast import android.widget.Toast
import kotlinx.android.synthetic.main.activity_display_name.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityDisplayNameBinding
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
@ -16,28 +16,32 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
class DisplayNameActivity : BaseActionBarActivity() { class DisplayNameActivity : BaseActionBarActivity() {
private lateinit var binding: ActivityDisplayNameBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setUpActionBarSessionLogo() setUpActionBarSessionLogo()
setContentView(R.layout.activity_display_name) binding = ActivityDisplayNameBinding.inflate(layoutInflater)
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard setContentView(binding.root)
displayNameEditText.setOnEditorActionListener( with(binding) {
OnEditorActionListener { _, actionID, event -> displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
if (actionID == EditorInfo.IME_ACTION_SEARCH || displayNameEditText.setOnEditorActionListener(
actionID == EditorInfo.IME_ACTION_DONE || OnEditorActionListener { _, actionID, event ->
(event.action == KeyEvent.ACTION_DOWN && if (actionID == EditorInfo.IME_ACTION_SEARCH ||
event.keyCode == KeyEvent.KEYCODE_ENTER)) { actionID == EditorInfo.IME_ACTION_DONE ||
this.register() (event.action == KeyEvent.ACTION_DOWN &&
return@OnEditorActionListener true event.keyCode == KeyEvent.KEYCODE_ENTER)) {
} register()
false return@OnEditorActionListener true
}) }
registerButton.setOnClickListener { register() } false
})
registerButton.setOnClickListener { register() }
}
} }
private fun register() { private fun register() {
val displayName = displayNameEditText.text.toString().trim() val displayName = binding.displayNameEditText.text.toString().trim()
if (displayName.isEmpty()) { if (displayName.isEmpty()) {
return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show() return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show()
} }
@ -45,7 +49,7 @@ class DisplayNameActivity : BaseActionBarActivity() {
return Toast.makeText(this, R.string.activity_display_name_display_name_too_long_error, Toast.LENGTH_SHORT).show() return Toast.makeText(this, R.string.activity_display_name_display_name_too_long_error, Toast.LENGTH_SHORT).show()
} }
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0) inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
TextSecurePreferences.setProfileName(this, displayName) TextSecurePreferences.setProfileName(this, displayName)
val intent = Intent(this, PNModeActivity::class.java) val intent = Intent(this, PNModeActivity::class.java)
push(intent) push(intent)

View File

@ -3,19 +3,17 @@ package org.thoughtcrime.securesms.onboarding
import android.animation.FloatEvaluator import android.animation.FloatEvaluator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.content.Context.LAYOUT_INFLATER_SERVICE
import android.os.Handler import android.os.Handler
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout
import android.widget.ScrollView import android.widget.ScrollView
import kotlinx.android.synthetic.main.view_fake_chat.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewFakeChatBinding
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
class FakeChatView : ScrollView { class FakeChatView : ScrollView {
private lateinit var binding: ViewFakeChatBinding
// region Settings // region Settings
private val spacing = context.resources.getDimension(R.dimen.medium_spacing) private val spacing = context.resources.getDimension(R.dimen.medium_spacing)
private val startDelay: Long = 1000 private val startDelay: Long = 1000
@ -41,17 +39,15 @@ class FakeChatView : ScrollView {
} }
private fun setUpViewHierarchy() { private fun setUpViewHierarchy() {
val inflater = context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater binding = ViewFakeChatBinding.inflate(LayoutInflater.from(context), this, true)
val contentView = inflater.inflate(R.layout.view_fake_chat, null) as LinearLayout binding.root.disableClipping()
contentView.disableClipping()
addView(contentView)
isVerticalScrollBarEnabled = false isVerticalScrollBarEnabled = false
} }
// endregion // endregion
// region Animation // region Animation
fun startAnimating() { fun startAnimating() {
listOf( bubble1, bubble2, bubble3, bubble4, bubble5 ).forEach { it.alpha = 0.0f } listOf( binding.bubble1, binding.bubble2, binding.bubble3, binding.bubble4, binding.bubble5 ).forEach { it.alpha = 0.0f }
fun show(bubble: View) { fun show(bubble: View) {
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
animation.duration = animationDuration animation.duration = animationDuration
@ -61,18 +57,18 @@ class FakeChatView : ScrollView {
animation.start() animation.start()
} }
Handler().postDelayed({ Handler().postDelayed({
show(bubble1) show(binding.bubble1)
Handler().postDelayed({ Handler().postDelayed({
show(bubble2) show(binding.bubble2)
Handler().postDelayed({ Handler().postDelayed({
show(bubble3) show(binding.bubble3)
smoothScrollTo(0, (bubble1.height + spacing).toInt()) smoothScrollTo(0, (binding.bubble1.height + spacing).toInt())
Handler().postDelayed({ Handler().postDelayed({
show(bubble4) show(binding.bubble4)
smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt()) smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt())
Handler().postDelayed({ Handler().postDelayed({
show(bubble5) show(binding.bubble5)
smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt() + (bubble3.height + spacing).toInt()) smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt() + (binding.bubble3.height + spacing).toInt())
}, delayBetweenMessages) }, delayBetweenMessages)
}, delayBetweenMessages) }, delayBetweenMessages)
}, delayBetweenMessages) }, delayBetweenMessages)

View File

@ -2,25 +2,27 @@ package org.thoughtcrime.securesms.onboarding
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import network.loki.messenger.databinding.ActivityLandingBinding
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.service.KeyCachingService
class LandingActivity : BaseActionBarActivity() { class LandingActivity : BaseActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_landing) val binding = ActivityLandingBinding.inflate(layoutInflater)
setContentView(binding.root)
setUpActionBarSessionLogo(true) setUpActionBarSessionLogo(true)
findViewById<FakeChatView>(R.id.fakeChatView).startAnimating() with(binding) {
findViewById<View>(R.id.registerButton).setOnClickListener { register() } fakeChatView.startAnimating()
findViewById<View>(R.id.restoreButton).setOnClickListener { restore() } registerButton.setOnClickListener { register() }
findViewById<View>(R.id.linkButton).setOnClickListener { link() } restoreButton.setOnClickListener { restore() }
linkButton.setOnClickListener { link() }
}
IdentityKeyUtil.generateIdentityKeyPair(this) IdentityKeyUtil.generateIdentityKeyPair(this)
TextSecurePreferences.setPasswordDisabled(this, true) TextSecurePreferences.setPasswordDisabled(this, true)
// AC: This is a temporary workaround to trick the old code that the screen is unlocked. // AC: This is a temporary workaround to trick the old code that the screen is unlocked.

View File

@ -4,7 +4,9 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.* import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
@ -13,14 +15,13 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter import androidx.fragment.app.FragmentPagerAdapter
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_link_device.*
import kotlinx.android.synthetic.main.fragment_recovery_phrase.*
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.crypto.MnemonicCodec
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
@ -30,13 +31,14 @@ import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
private lateinit var binding: ActivityLinkDeviceBinding
private val adapter = LinkDeviceActivityAdapter(this) private val adapter = LinkDeviceActivityAdapter(this)
private var restoreJob: Job? = null private var restoreJob: Job? = null
@ -55,9 +57,10 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis()) setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
setLastProfileUpdateTime(this@LinkDeviceActivity, 0) setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
} }
setContentView(R.layout.activity_link_device) binding = ActivityLinkDeviceBinding.inflate(layoutInflater)
viewPager.adapter = adapter setContentView(binding.root)
tabLayout.setupWithViewPager(viewPager) binding.viewPager.adapter = adapter
binding.tabLayout.setupWithViewPager(binding.viewPager)
} }
// endregion // endregion
@ -107,8 +110,8 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis()) TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true) TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true)
loader.isVisible = true binding.loader.isVisible = true
val snackBar = Snackbar.make(containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE) val snackBar = Snackbar.make(binding.containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.registration_activity__skip) { register(true) } .setAction(R.string.registration_activity__skip) { register(true) }
val skipJob = launch { val skipJob = launch {
@ -127,13 +130,13 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
register(false) register(false)
} }
loader.isVisible = false binding.loader.isVisible = false
} }
} }
private fun register(skipped: Boolean) { private fun register(skipped: Boolean) {
restoreJob?.cancel() restoreJob?.cancel()
loader.isVisible = false binding.loader.isVisible = false
TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis()) TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis())
val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java) val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
@ -175,30 +178,34 @@ private class LinkDeviceActivityAdapter(private val activity: LinkDeviceActivity
// region Recovery Phrase Fragment // region Recovery Phrase Fragment
class RecoveryPhraseFragment : Fragment() { class RecoveryPhraseFragment : Fragment() {
private lateinit var binding: FragmentRecoveryPhraseBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_recovery_phrase, container, false) binding = FragmentRecoveryPhraseBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard with(binding) {
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT) mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
mnemonicEditText.setOnEditorActionListener { v, actionID, _ -> mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
if (actionID == EditorInfo.IME_ACTION_DONE) { mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager if (actionID == EditorInfo.IME_ACTION_DONE) {
imm.hideSoftInputFromWindow(v.windowToken, 0) val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
handleContinueButtonTapped() imm.hideSoftInputFromWindow(v.windowToken, 0)
true handleContinueButtonTapped()
} else { true
false } else {
false
}
} }
continueButton.setOnClickListener { handleContinueButtonTapped() }
} }
continueButton.setOnClickListener { handleContinueButtonTapped() }
} }
private fun handleContinueButtonTapped() { private fun handleContinueButtonTapped() {
val mnemonic = mnemonicEditText.text?.trim().toString() val mnemonic = binding.mnemonicEditText.text?.trim().toString()
(requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic) (requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic)
} }
} }

View File

@ -13,9 +13,8 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import kotlinx.android.synthetic.main.activity_display_name.registerButton
import kotlinx.android.synthetic.main.activity_pn_mode.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityPnModeBinding
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
@ -28,6 +27,7 @@ import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.PNModeView import org.thoughtcrime.securesms.util.PNModeView
class PNModeActivity : BaseActionBarActivity() { class PNModeActivity : BaseActionBarActivity() {
private lateinit var binding: ActivityPnModeBinding
private var selectedOptionView: PNModeView? = null private var selectedOptionView: PNModeView? = null
// region Lifecycle // region Lifecycle
@ -35,15 +35,18 @@ class PNModeActivity : BaseActionBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setUpActionBarSessionLogo(true) setUpActionBarSessionLogo(true)
TextSecurePreferences.setHasSeenWelcomeScreen(this, true) TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
setContentView(R.layout.activity_pn_mode) binding = ActivityPnModeBinding.inflate(layoutInflater)
contentView.disableClipping() setContentView(binding.root)
fcmOptionView.setOnClickListener { toggleFCM() } with(binding) {
fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme) contentView.disableClipping()
fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme) fcmOptionView.setOnClickListener { toggleFCM() }
backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() } fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme) fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme) backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() }
registerButton.setOnClickListener { register() } backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
registerButton.setOnClickListener { register() }
}
toggleFCM() toggleFCM()
} }
@ -63,8 +66,7 @@ class PNModeActivity : BaseActionBarActivity() {
// region Interaction // region Interaction
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId when(item.itemId) {
when(id) {
R.id.learnMoreButton -> learnMore() R.id.learnMoreButton -> learnMore()
else -> { /* Do nothing */ } else -> { /* Do nothing */ }
} }
@ -81,52 +83,52 @@ class PNModeActivity : BaseActionBarActivity() {
} }
} }
private fun toggleFCM() { private fun toggleFCM() = with(binding) {
when (selectedOptionView) { when (selectedOptionView) {
null -> { null -> {
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView) performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.transparent, R.color.accent) GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.transparent, R.color.accent)
animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent) animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent)
selectedOptionView = fcmOptionView selectedOptionView = fcmOptionView
} }
fcmOptionView -> { fcmOptionView -> {
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView) performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.accent, R.color.transparent) GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.accent, R.color.transparent)
animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border) animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border)
selectedOptionView = null selectedOptionView = null
} }
backgroundPollingOptionView -> { backgroundPollingOptionView -> {
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView) performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.transparent, R.color.accent) GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.transparent, R.color.accent)
animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent) animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent)
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView) performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.accent, R.color.transparent) GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.accent, R.color.transparent)
animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border) animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border)
selectedOptionView = fcmOptionView selectedOptionView = fcmOptionView
} }
} }
} }
private fun toggleBackgroundPolling() { private fun toggleBackgroundPolling() = with(binding) {
when (selectedOptionView) { when (selectedOptionView) {
null -> { null -> {
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView) performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.transparent, R.color.accent) GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.transparent, R.color.accent)
animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent) animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent)
selectedOptionView = backgroundPollingOptionView selectedOptionView = backgroundPollingOptionView
} }
backgroundPollingOptionView -> { backgroundPollingOptionView -> {
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView) performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.accent, R.color.transparent) GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.accent, R.color.transparent)
animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border) animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border)
selectedOptionView = null selectedOptionView = null
} }
fcmOptionView -> { fcmOptionView -> {
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView) performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.transparent, R.color.accent) GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.transparent, R.color.accent)
animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent) animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent)
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView) performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.accent, R.color.transparent) GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.accent, R.color.transparent)
animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border) animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border)
selectedOptionView = backgroundPollingOptionView selectedOptionView = backgroundPollingOptionView
} }
@ -153,7 +155,7 @@ class PNModeActivity : BaseActionBarActivity() {
dialog.create().show() dialog.create().show()
return return
} }
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView)) TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView))
val application = ApplicationContext.getInstance(this) val application = ApplicationContext.getInstance(this)
application.startPollingIfNeeded() application.startPollingIfNeeded()
application.registerForFCMIfNeeded(true) application.registerForFCMIfNeeded(true)

View File

@ -11,8 +11,8 @@ import android.text.style.ClickableSpan
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import kotlinx.android.synthetic.main.activity_recovery_phrase_restore.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.crypto.MnemonicCodec
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() { class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -36,9 +36,10 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis()) setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis()) setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
} }
setContentView(R.layout.activity_recovery_phrase_restore) binding = ActivityRecoveryPhraseRestoreBinding.inflate(layoutInflater)
mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard setContentView(binding.root)
restoreButton.setOnClickListener { restore() } binding.mnemonicEditText.imeOptions = binding.mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
binding.restoreButton.setOnClickListener { restore() }
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy") val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(object : ClickableSpan() { termsExplanation.setSpan(object : ClickableSpan() {
@ -54,14 +55,14 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
openURL("https://getsession.org/privacy-policy/") openURL("https://getsession.org/privacy-policy/")
} }
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsTextView.movementMethod = LinkMovementMethod.getInstance() binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
termsTextView.text = termsExplanation binding.termsTextView.text = termsExplanation
} }
// endregion // endregion
// region Interaction // region Interaction
private fun restore() { private fun restore() {
val mnemonic = mnemonicEditText.text.toString() val mnemonic = binding.mnemonicEditText.text.toString()
try { try {
val loadFileContents: (String) -> String = { fileName -> val loadFileContents: (String) -> String = { fileName ->
MnemonicUtilities.loadFileContents(this, fileName) MnemonicUtilities.loadFileContents(this, fileName)

View File

@ -16,8 +16,8 @@ import android.text.style.StyleSpan
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import com.goterl.lazysodium.utils.KeyPair import com.goterl.lazysodium.utils.KeyPair
import kotlinx.android.synthetic.main.activity_register.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityRegisterBinding
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.KeyHelper
@ -26,9 +26,9 @@ import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
import java.util.*
class RegisterActivity : BaseActionBarActivity() { class RegisterActivity : BaseActionBarActivity() {
private lateinit var binding: ActivityRegisterBinding
private var seed: ByteArray? = null private var seed: ByteArray? = null
private var ed25519KeyPair: KeyPair? = null private var ed25519KeyPair: KeyPair? = null
private var x25519KeyPair: ECKeyPair? = null private var x25519KeyPair: ECKeyPair? = null
@ -37,7 +37,8 @@ class RegisterActivity : BaseActionBarActivity() {
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_register) binding = ActivityRegisterBinding.inflate(layoutInflater)
setContentView(binding.root)
setUpActionBarSessionLogo() setUpActionBarSessionLogo()
TextSecurePreferences.apply { TextSecurePreferences.apply {
setHasViewedSeed(this@RegisterActivity, false) setHasViewedSeed(this@RegisterActivity, false)
@ -45,8 +46,8 @@ class RegisterActivity : BaseActionBarActivity() {
setRestorationTime(this@RegisterActivity, 0) setRestorationTime(this@RegisterActivity, 0)
setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis()) setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis())
} }
registerButton.setOnClickListener { register() } binding.registerButton.setOnClickListener { register() }
copyButton.setOnClickListener { copyPublicKey() } binding.copyButton.setOnClickListener { copyPublicKey() }
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy") val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(object : ClickableSpan() { termsExplanation.setSpan(object : ClickableSpan() {
@ -62,8 +63,8 @@ class RegisterActivity : BaseActionBarActivity() {
openURL("https://getsession.org/privacy-policy/") openURL("https://getsession.org/privacy-policy/")
} }
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsTextView.movementMethod = LinkMovementMethod.getInstance() binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
termsTextView.text = termsExplanation binding.termsTextView.text = termsExplanation
updateKeyPair() updateKeyPair()
} }
// endregion // endregion
@ -94,12 +95,12 @@ class RegisterActivity : BaseActionBarActivity() {
} }
count += 1 count += 1
if (count < limit) { if (count < limit) {
publicKeyTextView.text = mangledHexEncodedPublicKey binding.publicKeyTextView.text = mangledHexEncodedPublicKey
Handler().postDelayed({ Handler().postDelayed({
animate() animate()
}, 32) }, 32)
} else { } else {
publicKeyTextView.text = hexEncodedPublicKey binding.publicKeyTextView.text = hexEncodedPublicKey
} }
} }
animate() animate()

View File

@ -9,8 +9,8 @@ import android.text.SpannableString
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import kotlinx.android.synthetic.main.activity_seed.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySeedBinding
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.crypto.MnemonicCodec
import org.session.libsignal.utilities.hexEncodedPrivateKey import org.session.libsignal.utilities.hexEncodedPrivateKey
@ -21,6 +21,8 @@ import org.thoughtcrime.securesms.util.getColorWithID
class SeedActivity : BaseActionBarActivity() { class SeedActivity : BaseActionBarActivity() {
private lateinit var binding: ActivitySeedBinding
private val seed by lazy { private val seed by lazy {
var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED) var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED)
if (hexEncodedSeed == null) { if (hexEncodedSeed == null) {
@ -35,27 +37,30 @@ class SeedActivity : BaseActionBarActivity() {
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_seed) binding = ActivitySeedBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar!!.title = resources.getString(R.string.activity_seed_title) supportActionBar!!.title = resources.getString(R.string.activity_seed_title)
val seedReminderViewTitle = SpannableString("You're almost finished! 90%") // Intentionally not yet translated val seedReminderViewTitle = SpannableString("You're almost finished! 90%") // Intentionally not yet translated
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
seedReminderView.title = seedReminderViewTitle with(binding) {
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2) seedReminderView.title = seedReminderViewTitle
seedReminderView.setProgress(90, false) seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2)
seedReminderView.hideContinueButton() seedReminderView.setProgress(90, false)
var redactedSeed = seed seedReminderView.hideContinueButton()
var index = 0 var redactedSeed = seed
for (character in seed) { var index = 0
if (character.isLetter()) { for (character in seed) {
redactedSeed = redactedSeed.replaceRange(index, index + 1, "") if (character.isLetter()) {
redactedSeed = redactedSeed.replaceRange(index, index + 1, "")
}
index += 1
} }
index += 1 seedTextView.setTextColor(resources.getColorWithID(R.color.accent, theme))
seedTextView.text = redactedSeed
seedTextView.setOnLongClickListener { revealSeed(); true }
revealButton.setOnLongClickListener { revealSeed(); true }
copyButton.setOnClickListener { copySeed() }
} }
seedTextView.setTextColor(resources.getColorWithID(R.color.accent, theme))
seedTextView.text = redactedSeed
seedTextView.setOnLongClickListener { revealSeed(); true }
revealButton.setOnLongClickListener { revealSeed(); true }
copyButton.setOnClickListener { copySeed() }
} }
// endregion // endregion
@ -63,14 +68,16 @@ class SeedActivity : BaseActionBarActivity() {
private fun revealSeed() { private fun revealSeed() {
val seedReminderViewTitle = SpannableString("Account secured! 100%") // Intentionally not yet translated val seedReminderViewTitle = SpannableString("Account secured! 100%") // Intentionally not yet translated
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 17, 21, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 17, 21, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
seedReminderView.title = seedReminderViewTitle with(binding) {
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3) seedReminderView.title = seedReminderViewTitle
seedReminderView.setProgress(100, true) seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3)
val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams seedReminderView.setProgress(100, true)
seedTextViewLayoutParams.height = seedTextView.height val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams
seedTextView.layoutParams = seedTextViewLayoutParams seedTextViewLayoutParams.height = seedTextView.height
seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme)) seedTextView.layoutParams = seedTextViewLayoutParams
seedTextView.text = seed seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
seedTextView.text = seed
}
TextSecurePreferences.setHasViewedSeed(this, true) TextSecurePreferences.setHasViewedSeed(this, true)
} }
// endregion // endregion

View File

@ -6,16 +6,17 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import kotlinx.android.synthetic.main.view_seed_reminder.view.* import network.loki.messenger.databinding.ViewSeedReminderBinding
import network.loki.messenger.R
class SeedReminderView : FrameLayout { class SeedReminderView : FrameLayout {
private lateinit var binding: ViewSeedReminderBinding
var title: CharSequence var title: CharSequence
get() = titleTextView.text get() = binding.titleTextView.text
set(value) { titleTextView.text = value } set(value) { binding.titleTextView.text = value }
var subtitle: CharSequence var subtitle: CharSequence
get() = subtitleTextView.text get() = binding.subtitleTextView.text
set(value) { subtitleTextView.text = value } set(value) { binding.subtitleTextView.text = value }
var delegate: SeedReminderViewDelegate? = null var delegate: SeedReminderViewDelegate? = null
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) {
@ -35,22 +36,20 @@ class SeedReminderView : FrameLayout {
} }
private fun setUpViewHierarchy() { private fun setUpViewHierarchy() {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater binding = ViewSeedReminderBinding.inflate(LayoutInflater.from(context), this, true)
val contentView = inflater.inflate(R.layout.view_seed_reminder, null) binding.button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() }
addView(contentView)
button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() }
} }
fun setProgress(progress: Int, isAnimated: Boolean) { fun setProgress(progress: Int, isAnimated: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
progressBar.setProgress(progress, isAnimated) binding.progressBar.setProgress(progress, isAnimated)
} else { } else {
progressBar.progress = progress binding.progressBar.progress = progress
} }
} }
fun hideContinueButton() { fun hideContinueButton() {
button.visibility = View.GONE binding.button.visibility = View.GONE
} }
} }

View File

@ -4,10 +4,12 @@ import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.dialog_clear_all_data.* import kotlinx.coroutines.Dispatchers
import kotlinx.android.synthetic.main.dialog_clear_all_data.view.* import kotlinx.coroutines.Job
import kotlinx.coroutines.* import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.DialogClearAllDataBinding
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
@ -15,6 +17,7 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
class ClearAllDataDialog : BaseDialog() { class ClearAllDataDialog : BaseDialog() {
private lateinit var binding: DialogClearAllDataBinding
enum class Steps { enum class Steps {
INFO_PROMPT, INFO_PROMPT,
@ -34,15 +37,15 @@ class ClearAllDataDialog : BaseDialog() {
} }
override fun setContentView(builder: AlertDialog.Builder) { override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null) binding = DialogClearAllDataBinding.inflate(LayoutInflater.from(requireContext()))
contentView.cancelButton.setOnClickListener { binding.cancelButton.setOnClickListener {
if (step == Steps.NETWORK_PROMPT) { if (step == Steps.NETWORK_PROMPT) {
clearAllData(false) clearAllData(false)
} else if (step != Steps.DELETING) { } else if (step != Steps.DELETING) {
dismiss() dismiss()
} }
} }
contentView.clearAllDataButton.setOnClickListener { binding.clearAllDataButton.setOnClickListener {
when(step) { when(step) {
Steps.INFO_PROMPT -> step = Steps.NETWORK_PROMPT Steps.INFO_PROMPT -> step = Steps.NETWORK_PROMPT
Steps.NETWORK_PROMPT -> { Steps.NETWORK_PROMPT -> {
@ -51,36 +54,33 @@ class ClearAllDataDialog : BaseDialog() {
Steps.DELETING -> { /* do nothing intentionally */ } Steps.DELETING -> { /* do nothing intentionally */ }
} }
} }
builder.setView(contentView) builder.setView(binding.root)
builder.setCancelable(false) builder.setCancelable(false)
} }
private fun updateUI() { private fun updateUI() {
dialog?.let {
dialog?.let { view ->
val isLoading = step == Steps.DELETING val isLoading = step == Steps.DELETING
when (step) { when (step) {
Steps.INFO_PROMPT -> { Steps.INFO_PROMPT -> {
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation) binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation)
view.cancelButton.setText(R.string.cancel) binding.cancelButton.setText(R.string.cancel)
view.clearAllDataButton.setText(R.string.delete) binding.clearAllDataButton.setText(R.string.delete)
} }
else -> { else -> {
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation) binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation)
view.cancelButton.setText(R.string.dialog_clear_all_data_local_only) binding.cancelButton.setText(R.string.dialog_clear_all_data_local_only)
view.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network) binding.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network)
} }
} }
view.cancelButton.isVisible = !isLoading binding.cancelButton.isVisible = !isLoading
view.clearAllDataButton.isVisible = !isLoading binding.clearAllDataButton.isVisible = !isLoading
view.progressBar.isVisible = isLoading binding.progressBar.isVisible = isLoading
view.setCanceledOnTouchOutside(!isLoading) it.setCanceledOnTouchOutside(!isLoading)
isCancelable = !isLoading isCancelable = !isLoading
} }
} }

View File

@ -10,9 +10,9 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter import androidx.fragment.app.FragmentPagerAdapter
import kotlinx.android.synthetic.main.activity_qr_code.*
import kotlinx.android.synthetic.main.fragment_view_my_qr_code.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityQrCodeBinding
import network.loki.messenger.databinding.FragmentViewMyQrCodeBinding
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
@ -20,23 +20,29 @@ import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.* import org.thoughtcrime.securesms.util.FileProviderUtil
import org.thoughtcrime.securesms.util.QRCodeUtilities
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.util.toPx
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
private lateinit var binding: ActivityQrCodeBinding
private val adapter = QRCodeActivityAdapter(this) private val adapter = QRCodeActivityAdapter(this)
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivityQrCodeBinding.inflate(layoutInflater)
// Set content view // Set content view
setContentView(R.layout.activity_qr_code) setContentView(binding.root)
// Set title // Set title
supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title) supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title)
// Set up view pager // Set up view pager
viewPager.adapter = adapter binding.viewPager.adapter = adapter
tabLayout.setupWithViewPager(viewPager) binding.tabLayout.setupWithViewPager(binding.viewPager)
} }
// endregion // endregion
@ -91,6 +97,7 @@ private class QRCodeActivityAdapter(val activity: QRCodeActivity) : FragmentPage
// region View My QR Code Fragment // region View My QR Code Fragment
class ViewMyQRCodeFragment : Fragment() { class ViewMyQRCodeFragment : Fragment() {
private lateinit var binding: FragmentViewMyQrCodeBinding
private val hexEncodedPublicKey: String private val hexEncodedPublicKey: String
get() { get() {
@ -98,18 +105,19 @@ class ViewMyQRCodeFragment : Fragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_view_my_qr_code, container, false) binding = FragmentViewMyQrCodeBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val size = toPx(280, resources) val size = toPx(280, resources)
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false) val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
qrCodeImageView.setImageBitmap(qrCode) binding.qrCodeImageView.setImageBitmap(qrCode)
// val explanation = SpannableStringBuilder("This is your unique public QR code. Other users can scan this to start a conversation with you.") // val explanation = SpannableStringBuilder("This is your unique public QR code. Other users can scan this to start a conversation with you.")
// explanation.setSpan(StyleSpan(Typeface.BOLD), 8, 34, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) // explanation.setSpan(StyleSpan(Typeface.BOLD), 8, 34, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation) binding.explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation)
shareButton.setOnClickListener { shareQRCode() } binding.shareButton.setOnClickListener { shareQRCode() }
} }
private fun shareQRCode() { private fun shareQRCode() {

View File

@ -6,8 +6,8 @@ import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_seed.view.*
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.DialogSeedBinding
import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.crypto.MnemonicCodec
import org.session.libsignal.utilities.hexEncodedPrivateKey import org.session.libsignal.utilities.hexEncodedPrivateKey
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
@ -28,11 +28,11 @@ class SeedDialog : BaseDialog() {
} }
override fun setContentView(builder: AlertDialog.Builder) { override fun setContentView(builder: AlertDialog.Builder) {
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_seed, null) val binding = DialogSeedBinding.inflate(LayoutInflater.from(requireContext()))
contentView.seedTextView.text = seed binding.seedTextView.text = seed
contentView.cancelButton.setOnClickListener { dismiss() } binding.cancelButton.setOnClickListener { dismiss() }
contentView.copyButton.setOnClickListener { copySeed() } binding.copyButton.setOnClickListener { copySeed() }
builder.setView(contentView) builder.setView(binding.root)
} }
private fun copySeed() { private fun copySeed() {

View File

@ -7,7 +7,11 @@ import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.ActionMode import android.view.ActionMode
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -15,9 +19,9 @@ import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.activity_settings.*
import network.loki.messenger.BuildConfig import network.loki.messenger.BuildConfig
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySettingsBinding
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all import nl.komponents.kovenant.all
import nl.komponents.kovenant.ui.alwaysUi import nl.komponents.kovenant.ui.alwaysUi
@ -34,12 +38,17 @@ import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
import org.thoughtcrime.securesms.util.* import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.util.push
import java.io.File import java.io.File
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.Date
class SettingsActivity : PassphraseRequiredActionBarActivity() { class SettingsActivity : PassphraseRequiredActionBarActivity() {
private lateinit var binding: ActivitySettingsBinding
private var displayNameEditActionMode: ActionMode? = null private var displayNameEditActionMode: ActionMode? = null
set(value) { field = value; handleDisplayNameEditActionModeChanged() } set(value) { field = value; handleDisplayNameEditActionModeChanged() }
private lateinit var glide: GlideRequests private lateinit var glide: GlideRequests
@ -59,33 +68,36 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
setContentView(R.layout.activity_settings) binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey
glide = GlideApp.with(this) glide = GlideApp.with(this)
profilePictureView.glide = glide with(binding) {
profilePictureView.publicKey = hexEncodedPublicKey profilePictureView.glide = glide
profilePictureView.displayName = displayName profilePictureView.publicKey = hexEncodedPublicKey
profilePictureView.isLarge = true profilePictureView.displayName = displayName
profilePictureView.update() profilePictureView.isLarge = true
profilePictureView.setOnClickListener { showEditProfilePictureUI() } profilePictureView.update()
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) } profilePictureView.setOnClickListener { showEditProfilePictureUI() }
btnGroupNameDisplay.text = displayName ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
publicKeyTextView.text = hexEncodedPublicKey btnGroupNameDisplay.text = displayName
copyButton.setOnClickListener { copyPublicKey() } publicKeyTextView.text = hexEncodedPublicKey
shareButton.setOnClickListener { sharePublicKey() } copyButton.setOnClickListener { copyPublicKey() }
privacyButton.setOnClickListener { showPrivacySettings() } shareButton.setOnClickListener { sharePublicKey() }
notificationsButton.setOnClickListener { showNotificationSettings() } privacyButton.setOnClickListener { showPrivacySettings() }
chatsButton.setOnClickListener { showChatSettings() } notificationsButton.setOnClickListener { showNotificationSettings() }
sendInvitationButton.setOnClickListener { sendInvitation() } chatsButton.setOnClickListener { showChatSettings() }
faqButton.setOnClickListener { showFAQ() } sendInvitationButton.setOnClickListener { sendInvitation() }
surveyButton.setOnClickListener { showSurvey() } faqButton.setOnClickListener { showFAQ() }
helpTranslateButton.setOnClickListener { helpTranslate() } surveyButton.setOnClickListener { showSurvey() }
seedButton.setOnClickListener { showSeed() } helpTranslateButton.setOnClickListener { helpTranslate() }
clearAllDataButton.setOnClickListener { clearAllData() } seedButton.setOnClickListener { showSeed() }
debugLogButton.setOnClickListener { shareLogs() } clearAllDataButton.setOnClickListener { clearAllData() }
val isLightMode = UiModeUtilities.isDayUiMode(this) debugLogButton.setOnClickListener { shareLogs() }
oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode) val isLightMode = UiModeUtilities.isDayUiMode(this@SettingsActivity)
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode)
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -152,22 +164,22 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
private fun handleDisplayNameEditActionModeChanged() { private fun handleDisplayNameEditActionModeChanged() {
val isEditingDisplayName = this.displayNameEditActionMode !== null val isEditingDisplayName = this.displayNameEditActionMode !== null
btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE binding.btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE binding.displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (isEditingDisplayName) { if (isEditingDisplayName) {
displayNameEditText.setText(btnGroupNameDisplay.text) binding.displayNameEditText.setText(binding.btnGroupNameDisplay.text)
displayNameEditText.selectAll() binding.displayNameEditText.selectAll()
displayNameEditText.requestFocus() binding.displayNameEditText.requestFocus()
inputMethodManager.showSoftInput(displayNameEditText, 0) inputMethodManager.showSoftInput(binding.displayNameEditText, 0)
} else { } else {
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0) inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
} }
} }
private fun updateProfile(isUpdatingProfilePicture: Boolean) { private fun updateProfile(isUpdatingProfilePicture: Boolean) {
loader.isVisible = true binding.loader.isVisible = true
val promises = mutableListOf<Promise<*, Exception>>() val promises = mutableListOf<Promise<*, Exception>>()
val displayName = displayNameToBeUploaded val displayName = displayNameToBeUploaded
if (displayName != null) { if (displayName != null) {
@ -192,15 +204,15 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
compoundPromise.alwaysUi { compoundPromise.alwaysUi {
if (displayName != null) { if (displayName != null) {
btnGroupNameDisplay.text = displayName binding.btnGroupNameDisplay.text = displayName
} }
if (isUpdatingProfilePicture && profilePicture != null) { if (isUpdatingProfilePicture && profilePicture != null) {
profilePictureView.recycle() // Clear the cached image before updating binding.profilePictureView.recycle() // Clear the cached image before updating
profilePictureView.update() binding.profilePictureView.update()
} }
displayNameToBeUploaded = null displayNameToBeUploaded = null
profilePictureToBeUploaded = null profilePictureToBeUploaded = null
loader.isVisible = false binding.loader.isVisible = false
} }
} }
// endregion // endregion
@ -211,7 +223,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
* @return true if the update was successful. * @return true if the update was successful.
*/ */
private fun saveDisplayName(): Boolean { private fun saveDisplayName(): Boolean {
val displayName = displayNameEditText.text.toString().trim() val displayName = binding.displayNameEditText.text.toString().trim()
if (displayName.isEmpty()) { if (displayName.isEmpty()) {
Toast.makeText(this, R.string.activity_settings_display_name_missing_error, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.activity_settings_display_name_missing_error, Toast.LENGTH_SHORT).show()
return false return false

View File

@ -13,12 +13,12 @@ import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.dialog_share_logs.view.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.BuildConfig import network.loki.messenger.BuildConfig
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.DialogShareLogsBinding
import org.session.libsignal.utilities.ExternalStorageUtil import org.session.libsignal.utilities.ExternalStorageUtil
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
@ -26,7 +26,7 @@ import org.thoughtcrime.securesms.util.StreamUtil
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.util.* import java.util.Objects
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class ShareLogsDialog : BaseDialog() { class ShareLogsDialog : BaseDialog() {
@ -34,16 +34,15 @@ class ShareLogsDialog : BaseDialog() {
private var shareJob: Job? = null private var shareJob: Job? = null
override fun setContentView(builder: AlertDialog.Builder) { override fun setContentView(builder: AlertDialog.Builder) {
val contentView = val binding = DialogShareLogsBinding.inflate(LayoutInflater.from(requireContext()))
LayoutInflater.from(requireContext()).inflate(R.layout.dialog_share_logs, null) binding.cancelButton.setOnClickListener {
contentView.cancelButton.setOnClickListener {
dismiss() dismiss()
} }
contentView.shareButton.setOnClickListener { binding.shareButton.setOnClickListener {
// start the export and share // start the export and share
shareLogs() shareLogs()
} }
builder.setView(contentView) builder.setView(binding.root)
builder.setCancelable(false) builder.setCancelable(false)
} }

View File

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

View File

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

View File

@ -2,39 +2,40 @@ package org.thoughtcrime.securesms.util
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.android.synthetic.main.fragment_scan_qr_code.* import androidx.fragment.app.Fragment
import network.loki.messenger.R import network.loki.messenger.databinding.FragmentScanQrCodeBinding
import org.thoughtcrime.securesms.qr.ScanListener import org.thoughtcrime.securesms.qr.ScanListener
import org.thoughtcrime.securesms.qr.ScanningThread import org.thoughtcrime.securesms.qr.ScanningThread
class ScanQRCodeFragment : Fragment() { class ScanQRCodeFragment : Fragment() {
private lateinit var binding: FragmentScanQrCodeBinding
private val scanningThread = ScanningThread() private val scanningThread = ScanningThread()
var scanListener: ScanListener? = null var scanListener: ScanListener? = null
set(value) { field = value; scanningThread.setScanListener(scanListener) } set(value) { field = value; scanningThread.setScanListener(scanListener) }
var message: CharSequence = "" var message: CharSequence = ""
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? { override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View {
return layoutInflater.inflate(R.layout.fragment_scan_qr_code, viewGroup, false) binding = FragmentScanQrCodeBinding.inflate(layoutInflater, viewGroup, false)
return binding.root
} }
override fun onViewCreated(view: View, bundle: Bundle?) { override fun onViewCreated(view: View, bundle: Bundle?) {
super.onViewCreated(view, bundle) super.onViewCreated(view, bundle)
when (resources.configuration.orientation) { when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> overlayView.orientation = LinearLayout.HORIZONTAL Configuration.ORIENTATION_LANDSCAPE -> binding.overlayView.orientation = LinearLayout.HORIZONTAL
else -> overlayView.orientation = LinearLayout.VERTICAL else -> binding.overlayView.orientation = LinearLayout.VERTICAL
} }
messageTextView.text = message binding.messageTextView.text = message
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
cameraView.onResume() binding.cameraView.onResume()
cameraView.setPreviewCallback(scanningThread) binding.cameraView.setPreviewCallback(scanningThread)
try { try {
scanningThread.start() scanningThread.start()
} catch (exception: Exception) { } catch (exception: Exception) {
@ -45,18 +46,18 @@ class ScanQRCodeFragment : Fragment() {
override fun onConfigurationChanged(newConfiguration: Configuration) { override fun onConfigurationChanged(newConfiguration: Configuration) {
super.onConfigurationChanged(newConfiguration) super.onConfigurationChanged(newConfiguration)
this.cameraView.onPause() binding.cameraView.onPause()
when (newConfiguration.orientation) { when (newConfiguration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> overlayView.orientation = LinearLayout.HORIZONTAL Configuration.ORIENTATION_LANDSCAPE -> binding.overlayView.orientation = LinearLayout.HORIZONTAL
else -> overlayView.orientation = LinearLayout.VERTICAL else -> binding.overlayView.orientation = LinearLayout.VERTICAL
} }
cameraView.onResume() binding.cameraView.onResume()
cameraView.setPreviewCallback(scanningThread) binding.cameraView.setPreviewCallback(scanningThread)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
this.cameraView.onPause() this.binding.cameraView.onPause()
this.scanningThread.stopScanning() this.scanningThread.stopScanning()
} }
} }

View File

@ -1,23 +1,24 @@
package org.thoughtcrime.securesms.util package org.thoughtcrime.securesms.util
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_scan_qr_code_placeholder.* import androidx.fragment.app.Fragment
import network.loki.messenger.R import network.loki.messenger.databinding.FragmentScanQrCodePlaceholderBinding
class ScanQRCodePlaceholderFragment: Fragment() { class ScanQRCodePlaceholderFragment: Fragment() {
private lateinit var binding: FragmentScanQrCodePlaceholderBinding
var delegate: ScanQRCodePlaceholderFragmentDelegate? = null var delegate: ScanQRCodePlaceholderFragmentDelegate? = null
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? { override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View {
return layoutInflater.inflate(R.layout.fragment_scan_qr_code_placeholder, viewGroup, false) binding = FragmentScanQrCodePlaceholderBinding.inflate(layoutInflater, viewGroup, false)
return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
grantCameraAccessButton.setOnClickListener { delegate?.requestCameraAccess() } binding.grantCameraAccessButton.setOnClickListener { delegate?.requestCameraAccess() }
} }
} }

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/contentView" android:id="@+id/contentView"
@ -74,7 +73,7 @@
android:id="@+id/seedReminderStub" android:id="@+id/seedReminderStub"
android:layout="@layout/seed_reminder_stub" android:layout="@layout/seed_reminder_stub"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
@ -91,7 +90,9 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingBottom="172dp" android:paddingBottom="172dp"
android:clipToPadding="false" android:clipToPadding="false"
tools:listitem="@layout/view_conversation"/> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="6"
tools:listitem="@layout/view_conversation" />
<View <View
android:id="@+id/gradientView" android:id="@+id/gradientView"
@ -103,21 +104,22 @@
android:id="@+id/emptyStateContainer" android:id="@+id/emptyStateContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="32dp" android:layout_centerInParent="true"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical" android:orientation="vertical"
android:layout_centerInParent="true"> android:paddingBottom="32dp"
android:visibility="gone">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="@dimen/medium_font_size" android:text="@string/activity_home_empty_state_message"
android:textColor="@color/text" android:textColor="@color/text"
android:text="@string/activity_home_empty_state_message" /> android:textSize="@dimen/medium_font_size" />
<Button <Button
style="@style/Widget.Session.Button.Common.ProminentOutline"
android:id="@+id/createNewPrivateChatButton" android:id="@+id/createNewPrivateChatButton"
style="@style/Widget.Session.Button.Common.ProminentOutline"
android:layout_width="196dp" android:layout_width="196dp"
android:layout_height="@dimen/medium_button_height" android:layout_height="@dimen/medium_button_height"
android:layout_marginTop="@dimen/medium_spacing" android:layout_marginTop="@dimen/medium_spacing"

View File

@ -1,108 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="org.thoughtcrime.securesms.loki.views.MessageAudioView">
<LinearLayout android:id="@+id/audio_widget_container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal">
<org.thoughtcrime.securesms.components.AnimatingToggle
android:id="@+id/control_toggle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:gravity="center">
<ProgressBar
android:id="@+id/download_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:background="@drawable/circle_tintable_4dp_inset"
android:visibility="gone"
android:layout_gravity="center_vertical"
android:min="0"
android:max="100"
tools:visibility="gone"
tools:backgroundTint="@android:color/black"
tools:indeterminateTint="@android:color/white"/>
<ImageView android:id="@+id/play"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_baseline_play_circle_filled_48"
android:scaleType="centerInside"
android:contentDescription="@string/audio_view__play_accessibility_description"
tools:visibility="visible"/>
<ImageView android:id="@+id/pause"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_baseline_pause_circle_filled_48"
android:scaleType="centerInside"
android:contentDescription="@string/audio_view__pause_accessibility_description"
tools:visibility="gone"/>
<ImageView android:id="@+id/download"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_download_circle_filled_48"
android:contentDescription="@string/audio_view__download_accessibility_description"
tools:visibility="gone"/>
</org.thoughtcrime.securesms.components.AnimatingToggle>
<org.thoughtcrime.securesms.loki.views.WaveformSeekBar
android:id="@+id/seek"
android:layout_width="0dp"
android:layout_height="38dp"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
app:bar_gravity="center"
app:bar_width="4dp"
app:bar_corner_radius="2dp"
app:bar_gap="1dp"
tools:progress="0.5"
tools:bar_background_color="#bbb"
tools:bar_progress_color="?colorPrimary"/>
<TextView android:id="@+id/total_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/conversation_item_date_text_size"
android:fontFamily="sans-serif-light"
android:autoLink="none"
android:visibility="gone"
tools:text="0:05"
tools:visibility="visible"/>
</LinearLayout>
</LinearLayout>
</merge>

View File

@ -0,0 +1,15 @@
package org.thoughtcrime.securesms
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Rule
open class BaseCoroutineTest {
@get:Rule
var coroutinesTestRule = CoroutineTestRule()
protected fun runBlockingTest(test: suspend TestCoroutineScope.() -> Unit) =
coroutinesTestRule.runBlockingTest { test() }
}

View File

@ -0,0 +1,50 @@
package org.thoughtcrime.securesms
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
/**
* Sets the main coroutines dispatcher to a [TestCoroutineScope] for unit testing. A
* [TestCoroutineScope] provides control over the execution of coroutines.
*
* Declare it as a JUnit Rule:
*
* ```
* @get:Rule
* var coroutineTestRule = CoroutineTestRule()
* ```
*
* Use it directly as a [TestCoroutineScope]:
*
* ```
* coroutineTestRule.pauseDispatcher()
* ...
* coroutineTestRule.resumeDispatcher()
* ...
* coroutineTestRule.runBlockingTest { }
* ...
*
*/
class CoroutineTestRule(
val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}

View File

@ -0,0 +1,60 @@
package org.thoughtcrime.securesms
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
/**
* Observes a [LiveData] until the `block` is done executing.
*/
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
val observer = Observer<T> { }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}

View File

@ -0,0 +1,11 @@
package org.thoughtcrime.securesms
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.junit.Rule
open class BaseViewModelTest: BaseCoroutineTest() {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
}

View File

@ -0,0 +1,173 @@
package org.thoughtcrime.securesms.conversation.v2
import kotlinx.coroutines.flow.first
import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.anySet
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.repository.ResultOf
import org.mockito.Mockito.`when` as whenever
class ConversationViewModelTest: BaseViewModelTest() {
private val repository = mock(ConversationRepository::class.java)
private val threadId = 123L
private lateinit var recipient: Recipient
private val viewModel: ConversationViewModel by lazy {
ConversationViewModel(threadId, repository)
}
@Before
fun setUp() {
recipient = mock(Recipient::class.java)
whenever(repository.isOxenHostedOpenGroup(anyLong())).thenReturn(true)
whenever(repository.getRecipientForThreadId(anyLong())).thenReturn(recipient)
}
@Test
fun `should emit group type on init`() = runBlockingTest {
assertTrue(viewModel.uiState.first().isOxenHostedOpenGroup)
}
@Test
fun `should save draft message`() {
val draft = "Hi there"
viewModel.saveDraft(draft)
verify(repository).saveDraft(threadId, draft)
}
@Test
fun `should retrieve draft message`() {
val draft = "Hi there"
whenever(repository.getDraft(anyLong())).thenReturn(draft)
val result = viewModel.getDraft()
verify(repository).getDraft(threadId)
assertThat(result, equalTo(draft))
}
@Test
fun `should invite contacts`() {
val contacts = listOf<Recipient>()
viewModel.inviteContacts(contacts)
verify(repository).inviteContacts(threadId, contacts)
}
@Test
fun `should unblock contact recipient`() {
whenever(recipient.isContactRecipient).thenReturn(true)
viewModel.unblock()
verify(repository).unblock(recipient)
}
@Test
fun `should delete locally`() {
val message = mock(MessageRecord::class.java)
viewModel.deleteLocally(message)
verify(repository).deleteLocally(recipient, message)
}
@Test
fun `should emit error message on failure to delete a message for everyone`() = runBlockingTest {
val message = mock(MessageRecord::class.java)
val error = Throwable()
whenever(repository.deleteForEveryone(anyLong(), any(), any()))
.thenReturn(ResultOf.Failure(error))
viewModel.deleteForEveryone(message)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
}
@Test
fun `should emit error message on failure to delete messages without unsend request`() =
runBlockingTest {
val message = mock(MessageRecord::class.java)
val error = Throwable()
whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet()))
.thenReturn(ResultOf.Failure(error))
viewModel.deleteMessagesWithoutUnsendRequest(setOf(message))
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
}
@Test
fun `should emit error message on ban user failure`() = runBlockingTest {
val error = Throwable()
whenever(repository.banUser(anyLong(), any())).thenReturn(ResultOf.Failure(error))
viewModel.banUser(recipient)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
}
@Test
fun `should emit a message on ban user success`() = runBlockingTest {
whenever(repository.banUser(anyLong(), any())).thenReturn(ResultOf.Success(Unit))
viewModel.banUser(recipient)
assertThat(
viewModel.uiState.first().uiMessages.first().message,
equalTo("Successfully banned user")
)
}
@Test
fun `should emit error message on ban user and delete all failure`() = runBlockingTest {
val error = Throwable()
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Failure(error))
viewModel.banAndDeleteAll(recipient)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
}
@Test
fun `should emit a message on ban user and delete all success`() = runBlockingTest {
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Success(Unit))
viewModel.banAndDeleteAll(recipient)
assertThat(
viewModel.uiState.first().uiMessages.first().message,
equalTo("Successfully banned user and deleted all their messages")
)
}
@Test
fun `should remove shown message`() = runBlockingTest {
// Given that a message is generated
whenever(repository.banUser(anyLong(), any())).thenReturn(ResultOf.Success(Unit))
viewModel.banUser(recipient)
assertThat(viewModel.uiState.value.uiMessages.size, equalTo(1))
// When the message is shown
viewModel.messageShown(viewModel.uiState.first().uiMessages.first().id)
// Then it should be removed
assertThat(viewModel.uiState.value.uiMessages.size, equalTo(0))
}
}

View File

@ -1,12 +1,20 @@
package org.thoughtcrime.securesms.jobs; package org.thoughtcrime.securesms.jobs;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.junit.Test; import org.junit.Test;
import org.session.libsession.messaging.utilities.Data;
import org.thoughtcrime.securesms.database.JobDatabase; import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec; import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec; import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
@ -17,14 +25,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class FastJobStorageTest { public class FastJobStorageTest {
private static final JsonDataSerializer serializer = new JsonDataSerializer(); private static final JsonDataSerializer serializer = new JsonDataSerializer();

View File

@ -1,7 +1,8 @@
package org.thoughtcrime.securesms.recipients; package org.thoughtcrime.securesms.recipients;
import android.content.Intent; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import android.provider.ContactsContract; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import junit.framework.TestCase; import junit.framework.TestCase;
@ -10,45 +11,14 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner; import org.mockito.runners.MockitoJUnitRunner;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.recipients.Recipient;
import static android.provider.ContactsContract.Intents.Insert.EMAIL; import org.session.libsession.utilities.recipients.RecipientExporter;
import static android.provider.ContactsContract.Intents.Insert.NAME;
import static android.provider.ContactsContract.Intents.Insert.PHONE;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
//FIXME AC: This test group is outdated. //FIXME AC: This test group is outdated.
@Ignore("This test group uses outdated instrumentation and needs a migration to modern tools.") @Ignore("This test group uses outdated instrumentation and needs a migration to modern tools.")
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public final class RecipientExporterTest extends TestCase { public final class RecipientExporterTest extends TestCase {
@Test
public void asAddContactIntent_with_phone_number() {
Recipient recipient = givenRecipient("Alice", givenPhoneNumber("+1555123456"));
Intent intent = RecipientExporter.export(recipient).asAddContactIntent();
assertEquals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction());
assertEquals(ContactsContract.Contacts.CONTENT_ITEM_TYPE, intent.getType());
assertEquals("Alice", intent.getStringExtra(NAME));
assertEquals("+1555123456", intent.getStringExtra(PHONE));
assertNull(intent.getStringExtra(EMAIL));
}
@Test
public void asAddContactIntent_with_email() {
Recipient recipient = givenRecipient("Bob", givenEmail("bob@signal.org"));
Intent intent = RecipientExporter.export(recipient).asAddContactIntent();
assertEquals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction());
assertEquals(ContactsContract.Contacts.CONTENT_ITEM_TYPE, intent.getType());
assertEquals("Bob", intent.getStringExtra(NAME));
assertEquals("bob@signal.org", intent.getStringExtra(EMAIL));
assertNull(intent.getStringExtra(PHONE));
}
@Test @Test
public void asAddContactIntent_with_neither_email_nor_phone() { public void asAddContactIntent_with_neither_email_nor_phone() {
RecipientExporter exporter = RecipientExporter.export(givenRecipient("Bob", mock(Address.class))); RecipientExporter exporter = RecipientExporter.export(givenRecipient("Bob", mock(Address.class)));
@ -64,19 +34,4 @@ public final class RecipientExporterTest extends TestCase {
return recipient; return recipient;
} }
private Address givenPhoneNumber(String phoneNumber) {
Address address = mock(Address.class);
when(address.isPhone()).thenReturn(true);
when(address.toPhoneString()).thenReturn(phoneNumber);
when(address.toEmailString()).thenThrow(new RuntimeException());
return address;
}
private Address givenEmail(String email) {
Address address = mock(Address.class);
when(address.isEmail()).thenReturn(true);
when(address.toEmailString()).thenReturn(email);
when(address.toPhoneString()).thenThrow(new RuntimeException());
return address;
}
} }

View File

@ -1,61 +0,0 @@
package org.thoughtcrime.securesms.service;
import org.junit.Before;
import org.junit.Test;
import org.thoughtcrime.securesms.BaseUnitTest;
import org.session.libsignal.utilities.guava.Optional;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.contains;
import static org.mockito.Mockito.when;
public class VerificationCodeParserTest extends BaseUnitTest {
private static Map<String, String> CHALLENGES = new HashMap<String,String>() {{
put("Your TextSecure verification code: 337-337", "337337");
put("XXX\nYour TextSecure verification code: 1337-1337", "13371337");
put("Your TextSecure verification code: 337-1337", "3371337");
put("Your TextSecure verification code: 1337-337", "1337337");
put("Your TextSecure verification code: 1337-1337", "13371337");
put("XXXYour TextSecure verification code: 1337-1337", "13371337");
put("Your TextSecure verification code: 1337-1337XXX", "13371337");
put("Your TextSecure verification code 1337-1337", "13371337");
put("Your Signal verification code: 337-337", "337337");
put("XXX\nYour Signal verification code: 1337-1337", "13371337");
put("Your Signal verification code: 337-1337", "3371337");
put("Your Signal verification code: 1337-337", "1337337");
put("Your Signal verification code: 1337-1337", "13371337");
put("XXXYour Signal verification code: 1337-1337", "13371337");
put("Your Signal verification code: 1337-1337XXX", "13371337");
put("Your Signal verification code 1337-1337", "13371337");
put("<#>Your Signal verification code: 1337-1337 aAbBcCdDeEf", "13371337");
put("<#> Your Signal verification code: 1337-1337 aAbBcCdDeEf", "13371337");
put("<#>Your Signal verification code: 1337-1337\naAbBcCdDeEf", "13371337");
put("<#> Your Signal verification code: 1337-1337\naAbBcCdDeEf", "13371337");
put("<#> Your Signal verification code: 1337-1337\n\naAbBcCdDeEf", "13371337");
}};
@Before
@Override
public void setUp() throws Exception {
super.setUp();
when(sharedPreferences.getBoolean(contains("pref_verifying"), anyBoolean())).thenReturn(true);
}
@Test
public void testChallenges() {
for (Entry<String,String> challenge : CHALLENGES.entrySet()) {
Optional<String> result = VerificationCodeParser.parse(context, challenge.getKey());
assertTrue(result.isPresent());
assertEquals(result.get(), challenge.getValue());
}
}
}

View File

@ -1,56 +0,0 @@
package org.thoughtcrime.securesms.util;
import org.junit.Before;
import org.junit.Test;
import static junit.framework.Assert.assertEquals;
public class DelimiterUtilTest {
@Before
public void setup() {}
@Test
public void testEscape() {
assertEquals(DelimiterUtil.escape("MTV Music", ' '), "MTV\\ Music");
assertEquals(DelimiterUtil.escape("MTV Music", ' '), "MTV\\ \\ Music");
assertEquals(DelimiterUtil.escape("MTV,Music", ','), "MTV\\,Music");
assertEquals(DelimiterUtil.escape("MTV,,Music", ','), "MTV\\,\\,Music");
assertEquals(DelimiterUtil.escape("MTV Music", '+'), "MTV Music");
}
@Test
public void testSplit() {
String[] parts = DelimiterUtil.split("MTV\\ Music", ' ');
assertEquals(parts.length, 1);
assertEquals(parts[0], "MTV\\ Music");
parts = DelimiterUtil.split("MTV Music", ' ');
assertEquals(parts.length, 2);
assertEquals(parts[0], "MTV");
assertEquals(parts[1], "Music");
}
@Test
public void testEscapeSplit() {
String input = "MTV Music";
String intermediate = DelimiterUtil.escape(input, ' ');
String[] parts = DelimiterUtil.split(intermediate, ' ');
assertEquals(parts.length, 1);
assertEquals(parts[0], "MTV\\ Music");
assertEquals(DelimiterUtil.unescape(parts[0], ' '), "MTV Music");
input = "MTV\\ Music";
intermediate = DelimiterUtil.escape(input, ' ');
parts = DelimiterUtil.split(intermediate, ' ');
assertEquals(parts.length, 1);
assertEquals(parts[0], "MTV\\\\ Music");
assertEquals(DelimiterUtil.unescape(parts[0], ' '), "MTV\\ Music");
}
}

View File

@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.util
import org.junit.Assert.assertEquals
import org.junit.Test
import org.session.libsession.utilities.DelimiterUtil
class DelimiterUtilTest {
@Test
fun testEscape() {
assertEquals(DelimiterUtil.escape("MTV Music", ' '), "MTV\\ Music")
assertEquals(DelimiterUtil.escape("MTV Music", ' '), "MTV\\ \\ Music")
assertEquals(DelimiterUtil.escape("MTV,Music", ','), "MTV\\,Music")
assertEquals(DelimiterUtil.escape("MTV,,Music", ','), "MTV\\,\\,Music")
assertEquals(DelimiterUtil.escape("MTV Music", '+'), "MTV Music")
}
@Test
fun testSplit() {
var parts = DelimiterUtil.split("MTV\\ Music", ' ')
assertEquals(parts.size, 1)
assertEquals(parts[0], "MTV\\ Music")
parts = DelimiterUtil.split("MTV Music", ' ')
assertEquals(parts.size, 2)
assertEquals(parts[0], "MTV")
assertEquals(parts[1], "Music")
}
@Test
fun testEscapeSplit() {
var input = "MTV Music"
var intermediate = DelimiterUtil.escape(input, ' ')
var parts = DelimiterUtil.split(intermediate, ' ')
assertEquals(parts.size, 1)
assertEquals(parts[0], "MTV\\ Music")
assertEquals(DelimiterUtil.unescape(parts[0], ' '), "MTV Music")
input = "MTV\\ Music"
intermediate = DelimiterUtil.escape(input, ' ')
parts = DelimiterUtil.split(intermediate, ' ')
assertEquals(parts.size, 1)
assertEquals(parts[0], "MTV\\\\ Music")
assertEquals(DelimiterUtil.unescape(parts[0], ' '), "MTV\\ Music")
}
}

View File

@ -1,67 +0,0 @@
package org.thoughtcrime.securesms.util;
import junit.framework.AssertionFailedError;
import org.junit.Test;
import org.thoughtcrime.securesms.BaseUnitTest;
import org.session.libsignal.service.api.util.InvalidNumberException;
import org.session.libsignal.service.api.util.PhoneNumberFormatter;
import static org.assertj.core.api.Assertions.assertThat;
public class PhoneNumberFormatterTest extends BaseUnitTest {
private static final String LOCAL_NUMBER_US = "+15555555555";
private static final String NUMBER_CH = "+41446681800";
private static final String NUMBER_UK = "+442079460018";
private static final String NUMBER_DE = "+4930123456";
private static final String NUMBER_MOBILE_DE = "+49171123456";
private static final String COUNTRY_CODE_CH = "41";
private static final String COUNTRY_CODE_UK = "44";
private static final String COUNTRY_CODE_DE = "49";
@Test
public void testFormatNumber() throws Exception, InvalidNumberException {
assertThat(PhoneNumberFormatter.formatNumber("(555) 555-5555", LOCAL_NUMBER_US)).isEqualTo(LOCAL_NUMBER_US);
assertThat(PhoneNumberFormatter.formatNumber("555-5555", LOCAL_NUMBER_US)).isEqualTo(LOCAL_NUMBER_US);
assertThat(PhoneNumberFormatter.formatNumber("(123) 555-5555", LOCAL_NUMBER_US)).isNotEqualTo(LOCAL_NUMBER_US);
}
@Test
public void testFormatNumberEmail() throws Exception {
try {
PhoneNumberFormatter.formatNumber("person@domain.com", LOCAL_NUMBER_US);
throw new AssertionFailedError("should have thrown on email");
} catch (InvalidNumberException ine) {
// success
}
}
@Test
public void testFormatNumberE164() throws Exception, InvalidNumberException {
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "(020) 7946 0018")).isEqualTo(NUMBER_UK);
// assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "044 20 7946 0018")).isEqualTo(NUMBER_UK);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "+442079460018")).isEqualTo(NUMBER_UK);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_CH, "+41 44 668 18 00")).isEqualTo(NUMBER_CH);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_CH, "+41 (044) 6681800")).isEqualTo(NUMBER_CH);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049 030 123456")).isEqualTo(NUMBER_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049 (0)30123456")).isEqualTo(NUMBER_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049((0)30)123456")).isEqualTo(NUMBER_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "+49 (0) 30 1 2 3 45 6 ")).isEqualTo(NUMBER_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "030 123456")).isEqualTo(NUMBER_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0171123456")).isEqualTo(NUMBER_MOBILE_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0171/123456")).isEqualTo(NUMBER_MOBILE_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "+490171/123456")).isEqualTo(NUMBER_MOBILE_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "00490171/123456")).isEqualTo(NUMBER_MOBILE_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049171/123456")).isEqualTo(NUMBER_MOBILE_DE);
}
@Test
public void testFormatRemoteNumberE164() throws Exception, InvalidNumberException {
assertThat(PhoneNumberFormatter.formatNumber("+4402079460018", LOCAL_NUMBER_US)).isEqualTo(NUMBER_UK);
}
}

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util.dynamiclanguage;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import org.session.libsession.utilities.dynamiclanguage.LanguageString;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;

View File

@ -7,6 +7,8 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import network.loki.messenger.BuildConfig; import network.loki.messenger.BuildConfig;
import java.util.Arrays; import java.util.Arrays;

View File

@ -4,9 +4,9 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.google.gms:google-services:4.3.4" classpath "com.google.gms:google-services:4.3.10"
classpath files('libs/gradle-witness.jar') classpath files('libs/gradle-witness.jar')
} }
} }

View File

@ -2,9 +2,15 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
org.gradle.jvmargs=-Xmx2048m org.gradle.jvmargs=-Xmx2048m
kotlinVersion=1.4.32 kotlinVersion=1.6.0
coroutinesVersion=1.6.0
kotlinxJsonVersion=1.3.0
lifecycleVersion=2.3.1
daggerVersion=2.40.1
glideVersion=4.11.0
kovenantVersion=3.3.0 kovenantVersion=3.3.0
curve25519Version=0.5.0 curve25519Version=0.5.0
protobufVersion=2.5.0 protobufVersion=2.5.0
okhttpVersion=3.12.1 okhttpVersion=3.12.1
jacksonDatabindVersion=2.9.8 jacksonDatabindVersion=2.9.8
mockitoKotlinVersion=4.0.0

View File

@ -1,5 +1,6 @@
#Thu Dec 30 07:09:53 SAST 2021
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@ -23,12 +23,13 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.material:material:1.2.1'
implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion"
testImplementation 'junit:junit:4.+' implementation "com.google.dagger:hilt-android:$daggerVersion"
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation "com.github.bumptech.glide:glide:$glideVersion"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.annimon:stream:1.1.8' implementation 'com.annimon:stream:1.1.8'
implementation 'com.makeramen:roundedimageview:2.1.0' implementation 'com.makeramen:roundedimageview:2.1.0'
@ -39,7 +40,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
testImplementation "junit:junit:3.8.2" testImplementation "junit:junit:3.8.2"
testImplementation "org.assertj:assertj-core:1.7.1" testImplementation "org.assertj:assertj-core:1.7.1"

View File

@ -1,11 +1,18 @@
package org.session.libsession.messaging.jobs package org.session.libsession.messaging.jobs
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import java.util.* import java.util.Timer
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -25,7 +32,10 @@ class JobQueue : JobDelegate {
val timer = Timer() val timer = Timer()
private fun CoroutineScope.processWithDispatcher(channel: Channel<Job>, dispatcher: CoroutineDispatcher) = launch(dispatcher) { private fun CoroutineScope.processWithDispatcher(
channel: Channel<Job>,
dispatcher: CoroutineDispatcher
) = launch(dispatcher) {
for (job in channel) { for (job in channel) {
if (!isActive) break if (!isActive) break
job.delegate = this@JobQueue job.delegate = this@JobQueue
@ -74,7 +84,7 @@ class JobQueue : JobDelegate {
fun add(job: Job) { fun add(job: Job) {
addWithoutExecuting(job) addWithoutExecuting(job)
queue.offer(job) // offer always called on unlimited capacity queue.trySend(job) // offer always called on unlimited capacity
} }
private fun addWithoutExecuting(job: Job) { private fun addWithoutExecuting(job: Job) {
@ -97,7 +107,7 @@ class JobQueue : JobDelegate {
Log.e("Loki","tried to re-queue pending/in-progress job") Log.e("Loki","tried to re-queue pending/in-progress job")
return return
} }
queue.offer(job) queue.trySend(job)
Log.d("Loki", "resumed pending send message $id") Log.d("Loki", "resumed pending send message $id")
} }
@ -114,7 +124,7 @@ class JobQueue : JobDelegate {
} }
pendingJobs.sortedBy { it.id }.forEach { job -> pendingJobs.sortedBy { it.id }.forEach { job ->
Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.") Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.")
queue.offer(job) // Offer always called on unlimited capacity queue.trySend(job) // Offer always called on unlimited capacity
} }
} }
@ -170,7 +180,7 @@ class JobQueue : JobDelegate {
Log.i("Loki", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).") Log.i("Loki", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
timer.schedule(delay = retryInterval) { timer.schedule(delay = retryInterval) {
Log.i("Loki", "Retrying ${job::class.simpleName}.") Log.i("Loki", "Retrying ${job::class.simpleName}.")
queue.offer(job) queue.trySend(job)
} }
} }
} }