mirror of
https://github.com/oxen-io/session-android.git
synced 2025-03-24 13:20:51 +00:00
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:
parent
366b5abdc8
commit
c113a447cf
@ -4,18 +4,17 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
classpath files('libs/gradle-witness.jar')
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||
classpath "com.google.gms:google-services:4.3.3"
|
||||
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
|
||||
classpath "com.google.gms:google-services:4.3.10"
|
||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'witness'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
@ -32,16 +31,16 @@ dependencies {
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
||||
implementation 'androidx.activity:activity-ktx:1.2.2'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.2'
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
@ -62,9 +61,9 @@ dependencies {
|
||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
||||
implementation 'commons-net:commons-net:3.7.2'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
@ -72,8 +71,8 @@ dependencies {
|
||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||
implementation "com.google.dagger:hilt-android:2.38.1"
|
||||
kapt "com.google.dagger:hilt-compiler:2.38.1"
|
||||
implementation "com.google.dagger:hilt-android:$daggerVersion"
|
||||
kapt "com.google.dagger:hilt-compiler:$daggerVersion"
|
||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||
@ -103,7 +102,7 @@ dependencies {
|
||||
}
|
||||
implementation project(":libsignal")
|
||||
implementation project(":libsession")
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
||||
implementation "org.whispersystems:curve25519-java:$curve25519Version"
|
||||
implementation 'com.goterl:lazysodium-android:5.0.2@aar'
|
||||
implementation "net.java.dev.jna:jna:5.8.0@aar"
|
||||
@ -111,7 +110,7 @@ dependencies {
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
|
||||
implementation "com.github.lelloman:android-identicons:v11"
|
||||
@ -122,12 +121,15 @@ dependencies {
|
||||
implementation "com.opencsv:opencsv:4.6"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.8'
|
||||
testImplementation "org.mockito:mockito-inline:4.0.0"
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||
testImplementation 'androidx.test:core:1.3.0'
|
||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||
// Core library
|
||||
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 {
|
||||
release {
|
||||
minifyEnabled false
|
||||
@ -279,6 +287,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
dataBinding true
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,13 +7,14 @@ import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.RelativeLayout
|
||||
import kotlinx.android.synthetic.main.view_separator.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewSeparatorBinding
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
|
||||
class LabeledSeparatorView : RelativeLayout {
|
||||
|
||||
private lateinit var binding: ViewSeparatorBinding
|
||||
private val path = Path()
|
||||
|
||||
private val paint: Paint by lazy {
|
||||
@ -43,10 +44,9 @@ class LabeledSeparatorView : RelativeLayout {
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_separator, null)
|
||||
binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context))
|
||||
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
addView(contentView, layoutParams)
|
||||
addView(binding.root, layoutParams)
|
||||
setWillNotDraw(false)
|
||||
}
|
||||
// endregion
|
||||
@ -59,9 +59,9 @@ class LabeledSeparatorView : RelativeLayout {
|
||||
val hMargin = toPx(16, resources).toFloat()
|
||||
path.reset()
|
||||
path.moveTo(0.0f, h / 2)
|
||||
path.lineTo(titleTextView.left - hMargin, h / 2)
|
||||
path.addRoundRect(titleTextView.left - hMargin, toPx(1, resources).toFloat(), titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
|
||||
path.moveTo(titleTextView.right + hMargin, h / 2)
|
||||
path.lineTo(binding.titleTextView.left - hMargin, h / 2)
|
||||
path.addRoundRect(binding.titleTextView.left - hMargin, toPx(1, resources).toFloat(), binding.titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
|
||||
path.moveTo(binding.titleTextView.right + hMargin, h / 2)
|
||||
path.lineTo(w, h / 2)
|
||||
path.close()
|
||||
c.drawPath(path, paint)
|
||||
|
@ -8,8 +8,8 @@ import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.DimenRes
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import kotlinx.android.synthetic.main.view_profile_picture.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
||||
import org.session.libsession.avatars.ProfileContactPhoto
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.Address
|
||||
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
|
||||
|
||||
class ProfilePictureView : RelativeLayout {
|
||||
private lateinit var binding: ViewProfilePictureBinding
|
||||
lateinit var glide: GlideRequests
|
||||
var publicKey: String? = null
|
||||
var displayName: String? = null
|
||||
@ -35,14 +36,12 @@ class ProfilePictureView : RelativeLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_profile_picture, null)
|
||||
addView(contentView)
|
||||
binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun update(recipient: Recipient, threadID: Long) {
|
||||
fun update(recipient: Recipient) {
|
||||
fun getUserDisplayName(publicKey: String): String {
|
||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||
@ -75,27 +74,27 @@ class ProfilePictureView : RelativeLayout {
|
||||
val publicKey = publicKey ?: return
|
||||
val additionalPublicKey = additionalPublicKey
|
||||
if (additionalPublicKey != null) {
|
||||
setProfilePictureIfNeeded(doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
|
||||
setProfilePictureIfNeeded(doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
|
||||
doubleModeImageViewContainer.visibility = View.VISIBLE
|
||||
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
|
||||
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
|
||||
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
|
||||
} else {
|
||||
glide.clear(doubleModeImageView1)
|
||||
glide.clear(doubleModeImageView2)
|
||||
doubleModeImageViewContainer.visibility = View.INVISIBLE
|
||||
glide.clear(binding.doubleModeImageView1)
|
||||
glide.clear(binding.doubleModeImageView2)
|
||||
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
|
||||
}
|
||||
if (additionalPublicKey == null && !isLarge) {
|
||||
setProfilePictureIfNeeded(singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size)
|
||||
singleModeImageView.visibility = View.VISIBLE
|
||||
setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size)
|
||||
binding.singleModeImageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
glide.clear(singleModeImageView)
|
||||
singleModeImageView.visibility = View.INVISIBLE
|
||||
glide.clear(binding.singleModeImageView)
|
||||
binding.singleModeImageView.visibility = View.INVISIBLE
|
||||
}
|
||||
if (additionalPublicKey == null && isLarge) {
|
||||
setProfilePictureIfNeeded(largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size)
|
||||
largeSingleModeImageView.visibility = View.VISIBLE
|
||||
setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size)
|
||||
binding.largeSingleModeImageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
glide.clear(largeSingleModeImageView)
|
||||
largeSingleModeImageView.visibility = View.INVISIBLE
|
||||
glide.clear(binding.largeSingleModeImageView)
|
||||
binding.largeSingleModeImageView.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,12 @@
|
||||
package org.thoughtcrime.securesms.contacts
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.contact_selection_list_divider.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.contacts.UserView
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import network.loki.messenger.databinding.ContactSelectionListDividerBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
lateinit var glide: GlideRequests
|
||||
@ -24,7 +21,15 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
|
||||
}
|
||||
|
||||
class UserViewHolder(val view: UserView) : RecyclerView.ViewHolder(view)
|
||||
class DividerViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||
class DividerViewHolder(
|
||||
private val binding: ContactSelectionListDividerBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: ContactSelectionListItem.Header) {
|
||||
with(binding){
|
||||
label.text = item.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
@ -41,8 +46,9 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
|
||||
return if (viewType == ViewType.Contact) {
|
||||
UserViewHolder(UserView(context))
|
||||
} else {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false)
|
||||
DividerViewHolder(view)
|
||||
DividerViewHolder(
|
||||
ContactSelectionListDividerBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,8 +64,7 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
|
||||
if (multiSelect) UserView.ActionIndicator.Tick else UserView.ActionIndicator.None,
|
||||
isSelected)
|
||||
} else if (viewHolder is DividerViewHolder) {
|
||||
item as ContactSelectionListItem.Header
|
||||
viewHolder.view.label.text = item.name
|
||||
viewHolder.bind(item as ContactSelectionListItem.Header)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,21 @@
|
||||
package org.thoughtcrime.securesms.contacts
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.contact_selection_list_fragment.*
|
||||
import network.loki.messenger.R
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||
import network.loki.messenger.databinding.ContactSelectionListFragmentBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListLoader
|
||||
|
||||
class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener {
|
||||
private lateinit var binding: ContactSelectionListFragmentBinding
|
||||
private var cursorFilter: String? = null
|
||||
var onContactSelectedListener: OnContactSelectedListener? = null
|
||||
|
||||
@ -46,20 +44,21 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
||||
fun onContactDeselected(number: String?)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||
recyclerView.adapter = listAdapter
|
||||
swipeRefreshLayout.isEnabled = requireActivity().intent.getBooleanExtra(REFRESHABLE, true)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.contact_selection_list_fragment, container, false)
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = ContactSelectionListFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||
binding.recyclerView.adapter = listAdapter
|
||||
binding.swipeRefreshLayout.isEnabled = requireActivity().intent.getBooleanExtra(REFRESHABLE, true)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
@ -74,15 +73,15 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
||||
|
||||
fun resetQueryFilter() {
|
||||
setQueryFilter(null)
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
fun setRefreshing(refreshing: Boolean) {
|
||||
swipeRefreshLayout.isRefreshing = refreshing
|
||||
binding.swipeRefreshLayout.isRefreshing = refreshing
|
||||
}
|
||||
|
||||
fun setOnRefreshListener(onRefreshListener: OnRefreshListener?) {
|
||||
swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
|
||||
binding.swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
|
||||
}
|
||||
|
||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> {
|
||||
@ -107,8 +106,8 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
||||
return
|
||||
}
|
||||
listAdapter.items = items
|
||||
mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
|
||||
emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
|
||||
binding.emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun onContactClick(contact: Recipient) {
|
||||
|
@ -9,16 +9,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.activity_create_closed_group.emptyStateContainer
|
||||
import kotlinx.android.synthetic.main.activity_create_closed_group.mainContentContainer
|
||||
import kotlinx.android.synthetic.main.activity_select_contacts.*
|
||||
import kotlinx.android.synthetic.main.activity_select_contacts.recyclerView
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivitySelectContactsBinding
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
|
||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
||||
class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||
private lateinit var binding: ActivitySelectContactsBinding
|
||||
private var members = listOf<String>()
|
||||
set(value) { field = value; selectContactsAdapter.members = value }
|
||||
private lateinit var usersToExclude: Set<String>
|
||||
@ -36,18 +33,18 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
|
||||
setContentView(R.layout.activity_select_contacts)
|
||||
binding = ActivitySelectContactsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
|
||||
|
||||
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
|
||||
val emptyStateText = intent.getStringExtra(emptyStateTextKey)
|
||||
if (emptyStateText != null) {
|
||||
emptyStateMessageTextView.text = emptyStateText
|
||||
binding.emptyStateMessageTextView.text = emptyStateText
|
||||
}
|
||||
|
||||
recyclerView.adapter = selectContactsAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = selectContactsAdapter
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||
}
|
||||
@ -73,8 +70,8 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
||||
|
||||
private fun update(members: List<String>) {
|
||||
this.members = members
|
||||
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
// endregion
|
||||
|
@ -5,9 +5,8 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_conversation.view.profilePictureView
|
||||
import kotlinx.android.synthetic.main.view_user.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewUserBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
|
||||
@ -15,6 +14,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class UserView : LinearLayout {
|
||||
private lateinit var binding: ViewUserBinding
|
||||
var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly
|
||||
|
||||
enum class ActionIndicator {
|
||||
@ -41,9 +41,7 @@ class UserView : LinearLayout {
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_user, null)
|
||||
addView(contentView)
|
||||
binding = ViewUserBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -56,28 +54,32 @@ class UserView : LinearLayout {
|
||||
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
|
||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
|
||||
val address = user.address.serialize()
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.update(user, threadID)
|
||||
actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||
nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
||||
binding.profilePictureView.glide = glide
|
||||
binding.profilePictureView.update(user)
|
||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
||||
when (actionIndicator) {
|
||||
ActionIndicator.None -> {
|
||||
actionIndicatorImageView.visibility = View.GONE
|
||||
binding.actionIndicatorImageView.visibility = View.GONE
|
||||
}
|
||||
ActionIndicator.Menu -> {
|
||||
actionIndicatorImageView.visibility = View.VISIBLE
|
||||
actionIndicatorImageView.setImageResource(R.drawable.ic_more_horiz_white)
|
||||
binding.actionIndicatorImageView.visibility = View.VISIBLE
|
||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_more_horiz_white)
|
||||
}
|
||||
ActionIndicator.Tick -> {
|
||||
actionIndicatorImageView.visibility = View.VISIBLE
|
||||
actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle)
|
||||
binding.actionIndicatorImageView.visibility = View.VISIBLE
|
||||
binding.actionIndicatorImageView.setImageResource(
|
||||
if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleCheckbox(isSelected: Boolean = false) {
|
||||
actionIndicatorImageView.visibility = View.VISIBLE
|
||||
actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle)
|
||||
binding.actionIndicatorImageView.visibility = View.VISIBLE
|
||||
binding.actionIndicatorImageView.setImageResource(
|
||||
if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle
|
||||
)
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,9 +4,7 @@ import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import kotlinx.android.synthetic.main.view_visible_message.view.*
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||
@ -49,15 +47,9 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val viewType = ViewType.allValues[viewType]
|
||||
when (viewType) {
|
||||
ViewType.Visible -> {
|
||||
val view = VisibleMessageView(context)
|
||||
return VisibleMessageViewHolder(view)
|
||||
}
|
||||
ViewType.Control -> {
|
||||
val view = ControlMessageView(context)
|
||||
return ControlMessageViewHolder(view)
|
||||
}
|
||||
return when (viewType) {
|
||||
ViewType.Visible -> VisibleMessageViewHolder(VisibleMessageView(context))
|
||||
ViewType.Control -> ControlMessageViewHolder(ControlMessageView(context))
|
||||
else -> throw IllegalStateException("Unexpected view type: $viewType.")
|
||||
}
|
||||
}
|
||||
@ -71,7 +63,6 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
val view = viewHolder.view
|
||||
val isSelected = selectedItems.contains(message)
|
||||
view.snIsSelected = isSelected
|
||||
view.messageTimestampTextView.isVisible = isSelected
|
||||
view.indexInAdapter = position
|
||||
view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery)
|
||||
if (!message.isDeleted) {
|
||||
|
@ -2,16 +2,12 @@ package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.VelocityTracker
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
class ConversationRecyclerView : RecyclerView {
|
||||
private val maxLongPressVelocityY = toPx(10, resources)
|
||||
@ -37,10 +33,10 @@ class ConversationRecyclerView : RecyclerView {
|
||||
if (abs(vy) > maxLongPressVelocityY && abs(vx) < minSwipeVelocityX) { return super.onInterceptTouchEvent(e) }
|
||||
// Return false if abs(v.x) > abs(v.y) so that only swipes that are more horizontal than vertical
|
||||
// get passed on to the message view
|
||||
if (abs(vx) > abs(vy)) {
|
||||
return false
|
||||
return if (abs(vx) > abs(vy)) {
|
||||
false
|
||||
} else {
|
||||
return super.onInterceptTouchEvent(e)
|
||||
super.onInterceptTouchEvent(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,130 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import java.util.UUID
|
||||
|
||||
class ConversationViewModel(
|
||||
val threadId: Long,
|
||||
private val repository: ConversationRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(ConversationUiState())
|
||||
val uiState: StateFlow<ConversationUiState> = _uiState
|
||||
|
||||
val recipient: Recipient by lazy {
|
||||
repository.getRecipientForThreadId(threadId)
|
||||
}
|
||||
|
||||
init {
|
||||
_uiState.update {
|
||||
it.copy(isOxenHostedOpenGroup = repository.isOxenHostedOpenGroup(threadId))
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDraft(text: String) {
|
||||
repository.saveDraft(threadId, text)
|
||||
}
|
||||
|
||||
fun getDraft(): String? {
|
||||
return repository.getDraft(threadId)
|
||||
}
|
||||
|
||||
fun inviteContacts(contacts: List<Recipient>) {
|
||||
repository.inviteContacts(threadId, contacts)
|
||||
}
|
||||
|
||||
fun unblock() {
|
||||
if (recipient.isContactRecipient) {
|
||||
repository.unblock(recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteLocally(message: MessageRecord) {
|
||||
repository.deleteLocally(recipient, message)
|
||||
}
|
||||
|
||||
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
|
||||
repository.deleteForEveryone(threadId, recipient, message)
|
||||
.onFailure {
|
||||
showMessage("Couldn't delete message due to error: $it")
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMessagesWithoutUnsendRequest(messages: Set<MessageRecord>) = viewModelScope.launch {
|
||||
repository.deleteMessageWithoutUnsendRequest(threadId, messages)
|
||||
.onFailure {
|
||||
showMessage("Couldn't delete message due to error: $it")
|
||||
}
|
||||
}
|
||||
|
||||
fun banUser(recipient: Recipient) = viewModelScope.launch {
|
||||
repository.banUser(threadId, recipient)
|
||||
.onSuccess {
|
||||
showMessage("Successfully banned user")
|
||||
}
|
||||
.onFailure {
|
||||
showMessage("Couldn't ban user due to error: $it")
|
||||
}
|
||||
}
|
||||
|
||||
fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch {
|
||||
repository.banAndDeleteAll(threadId, recipient)
|
||||
.onSuccess {
|
||||
showMessage("Successfully banned user and deleted all their messages")
|
||||
}
|
||||
.onFailure {
|
||||
showMessage("Couldn't execute request due to error: $it")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMessage(message: String) {
|
||||
_uiState.update { currentUiState ->
|
||||
val messages = currentUiState.uiMessages + UiMessage(
|
||||
id = UUID.randomUUID().mostSignificantBits,
|
||||
message = message
|
||||
)
|
||||
currentUiState.copy(uiMessages = messages)
|
||||
}
|
||||
}
|
||||
|
||||
fun messageShown(messageId: Long) {
|
||||
_uiState.update { currentUiState ->
|
||||
val messages = currentUiState.uiMessages.filterNot { it.id == messageId }
|
||||
currentUiState.copy(uiMessages = messages)
|
||||
}
|
||||
}
|
||||
|
||||
@dagger.assisted.AssistedFactory
|
||||
interface AssistedFactory {
|
||||
fun create(threadId: Long): Factory
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class Factory @AssistedInject constructor(
|
||||
@Assisted private val threadId: Long,
|
||||
private val repository: ConversationRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ConversationViewModel(threadId, repository) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class UiMessage(val id: Long, val message: String)
|
||||
|
||||
data class ConversationUiState(
|
||||
val isOxenHostedOpenGroup: Boolean = false,
|
||||
val uiMessages: List<UiMessage> = emptyList()
|
||||
)
|
@ -7,8 +7,8 @@ import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.fragment_delete_message_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentDeleteMessageBottomSheetBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||
@ -22,6 +22,7 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
||||
lateinit var contactDatabase: SessionContactDatabase
|
||||
|
||||
lateinit var recipient: Recipient
|
||||
private lateinit var binding: FragmentDeleteMessageBottomSheetBinding
|
||||
val contact by lazy {
|
||||
val senderId = recipient.address.serialize()
|
||||
// this dialog won't show for open group contacts
|
||||
@ -37,15 +38,16 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_delete_message_bottom_sheet, container, false)
|
||||
): View {
|
||||
binding = FragmentDeleteMessageBottomSheetBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
when (v) {
|
||||
deleteForMeTextView -> onDeleteForMeTapped?.invoke()
|
||||
deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke()
|
||||
cancelTextView -> onCancelTapped?.invoke()
|
||||
binding.deleteForMeTextView -> onDeleteForMeTapped?.invoke()
|
||||
binding.deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke()
|
||||
binding.cancelTextView -> onCancelTapped?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,13 +57,13 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
||||
return dismiss()
|
||||
}
|
||||
if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) {
|
||||
deleteForEveryoneTextView.text =
|
||||
binding.deleteForEveryoneTextView.text =
|
||||
resources.getString(R.string.delete_message_for_me_and_recipient, contact)
|
||||
}
|
||||
deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
|
||||
deleteForMeTextView.setOnClickListener(this)
|
||||
deleteForEveryoneTextView.setOnClickListener(this)
|
||||
cancelTextView.setOnClickListener(this)
|
||||
binding.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
|
||||
binding.deleteForMeTextView.setOnClickListener(this)
|
||||
binding.deleteForEveryoneTextView.setOnClickListener(this)
|
||||
binding.cancelTextView.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.activity_message_detail.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityMessageDetailBinding
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
@ -13,11 +13,11 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMessageDetailBinding
|
||||
var messageRecord: MessageRecord? = null
|
||||
|
||||
// region Settings
|
||||
@ -29,7 +29,8 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
setContentView(R.layout.activity_message_detail)
|
||||
binding = ActivityMessageDetailBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
title = resources.getString(R.string.conversation_context__menu_message_details)
|
||||
val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
||||
// We only show this screen for messages fail to send,
|
||||
@ -37,7 +38,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
||||
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
|
||||
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author)
|
||||
updateContent()
|
||||
resend_button.setOnClickListener {
|
||||
binding.resendButton.setOnClickListener {
|
||||
ResendMessageUtilities.resend(messageRecord!!)
|
||||
finish()
|
||||
}
|
||||
@ -46,20 +47,20 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
||||
fun updateContent() {
|
||||
val dateLocale = Locale.getDefault()
|
||||
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
|
||||
sent_time.text = dateFormatter.format(Date(messageRecord!!.dateSent))
|
||||
binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
|
||||
|
||||
val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) ?: "Message failed to send."
|
||||
error_message.text = errorMessage
|
||||
binding.errorMessage.text = errorMessage
|
||||
|
||||
if (messageRecord!!.getExpiresIn() <= 0 || messageRecord!!.getExpireStarted() <= 0) {
|
||||
expires_container.visibility = View.GONE
|
||||
if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
|
||||
binding.expiresContainer.visibility = View.GONE
|
||||
} else {
|
||||
expires_container.visibility = View.VISIBLE
|
||||
binding.expiresContainer.visibility = View.VISIBLE
|
||||
val elapsed = System.currentTimeMillis() - messageRecord!!.expireStarted
|
||||
val remaining = messageRecord!!.expiresIn - elapsed
|
||||
|
||||
val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
|
||||
expires_in.text = duration
|
||||
binding.expiresIn.text = duration
|
||||
}
|
||||
}
|
||||
}
|
@ -15,14 +15,16 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.android.synthetic.main.fragment_modal_url_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
|
||||
class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_modal_url_bottom_sheet, container, false)
|
||||
private lateinit var binding: FragmentModalUrlBottomSheetBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View {
|
||||
binding = FragmentModalUrlBottomSheetBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@ -31,10 +33,10 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(url)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
openURLExplanationTextView.text = spannable
|
||||
cancelButton.setOnClickListener(this)
|
||||
copyButton.setOnClickListener(this)
|
||||
openURLButton.setOnClickListener(this)
|
||||
binding.openURLExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener(this)
|
||||
binding.copyButton.setOnClickListener(this)
|
||||
binding.openURLButton.setOnClickListener(this)
|
||||
}
|
||||
|
||||
private fun open() {
|
||||
@ -64,9 +66,9 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
when (v) {
|
||||
openURLButton -> open()
|
||||
copyButton -> copy()
|
||||
cancelButton -> dismiss()
|
||||
binding.openURLButton -> open()
|
||||
binding.copyButton -> copy()
|
||||
binding.cancelButton -> dismiss()
|
||||
}
|
||||
}
|
||||
}
|
@ -11,8 +11,8 @@ import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.album_thumbnail_view.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
@ -32,6 +32,8 @@ import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AlbumThumbnailView : FrameLayout {
|
||||
|
||||
private lateinit var binding: AlbumThumbnailViewBinding
|
||||
|
||||
companion object {
|
||||
const val MAX_ALBUM_DISPLAY_SIZE = 5
|
||||
@ -55,7 +57,7 @@ class AlbumThumbnailView : FrameLayout {
|
||||
private var slideSize: Int = 0
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this)
|
||||
binding = AlbumThumbnailViewBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas?) {
|
||||
@ -73,7 +75,7 @@ class AlbumThumbnailView : FrameLayout {
|
||||
// Z-check in specific order
|
||||
val testRect = Rect()
|
||||
// test "Read More"
|
||||
albumCellBodyTextReadMore.getGlobalVisibleRect(testRect)
|
||||
binding.albumCellBodyTextReadMore.getGlobalVisibleRect(testRect)
|
||||
if (testRect.contains(eventRect)) {
|
||||
// dispatch to activity view
|
||||
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
||||
@ -81,15 +83,15 @@ class AlbumThumbnailView : FrameLayout {
|
||||
}
|
||||
return
|
||||
}
|
||||
val intersectedSpans = albumCellBodyText.getIntersectedModalSpans(eventRect)
|
||||
val intersectedSpans = binding.albumCellBodyText.getIntersectedModalSpans(eventRect)
|
||||
if (intersectedSpans.isNotEmpty()) {
|
||||
intersectedSpans.forEach { span ->
|
||||
span.onClick(albumCellBodyText)
|
||||
span.onClick(binding.albumCellBodyText)
|
||||
}
|
||||
return
|
||||
}
|
||||
// test each album child
|
||||
albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
|
||||
binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
|
||||
child.getGlobalVisibleRect(testRect)
|
||||
if (testRect.contains(eventRect)) {
|
||||
// hit intersects with this particular child
|
||||
@ -122,10 +124,10 @@ class AlbumThumbnailView : FrameLayout {
|
||||
|
||||
// recreate cell views if different size to what we have already (for recycling)
|
||||
if (slides.size != this.slideSize) {
|
||||
albumCellContainer.removeAllViews()
|
||||
LayoutInflater.from(context).inflate(layoutRes(slides.size), albumCellContainer)
|
||||
binding.albumCellContainer.removeAllViews()
|
||||
LayoutInflater.from(context).inflate(layoutRes(slides.size), binding.albumCellContainer)
|
||||
val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE
|
||||
albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
|
||||
binding.albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
|
||||
// overflowText will be null if !overflowed
|
||||
overflowText.isVisible = overflowed // more than max album size
|
||||
overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE)
|
||||
@ -137,17 +139,17 @@ class AlbumThumbnailView : FrameLayout {
|
||||
val thumbnailView = getThumbnailView(position)
|
||||
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)
|
||||
albumCellBodyText.text = body
|
||||
binding.albumCellBodyText.text = body
|
||||
post {
|
||||
// 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) }
|
||||
?: 0
|
||||
// show read more text if at least one line is ellipsized
|
||||
ViewUtil.setPaddingTop(albumCellBodyTextParent, if (maxEllipsis > 0) resources.getDimension(R.dimen.small_spacing).roundToInt() else resources.getDimension(R.dimen.medium_spacing).roundToInt())
|
||||
albumCellBodyTextReadMore.isVisible = maxEllipsis > 0
|
||||
ViewUtil.setPaddingTop(binding.albumCellBodyTextParent, if (maxEllipsis > 0) resources.getDimension(R.dimen.small_spacing).roundToInt() else resources.getDimension(R.dimen.medium_spacing).roundToInt())
|
||||
binding.albumCellBodyTextReadMore.isVisible = maxEllipsis > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -165,11 +167,11 @@ class AlbumThumbnailView : FrameLayout {
|
||||
}
|
||||
|
||||
fun getThumbnailView(position: Int): KThumbnailView = when (position) {
|
||||
0 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
|
||||
1 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
|
||||
2 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
|
||||
3 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
|
||||
4 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
|
||||
0 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
|
||||
1 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
|
||||
2 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
|
||||
3 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
|
||||
4 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
|
||||
else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position")
|
||||
}
|
||||
|
||||
|
@ -5,14 +5,14 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_link_preview_draft.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewLinkPreviewDraftBinding
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
|
||||
class LinkPreviewDraftView : LinearLayout {
|
||||
private lateinit var binding: ViewLinkPreviewDraftBinding
|
||||
var delegate: LinkPreviewDraftViewDelegate? = null
|
||||
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
@ -21,22 +21,22 @@ class LinkPreviewDraftView : LinearLayout {
|
||||
|
||||
private fun initialize() {
|
||||
// Start out with the loader showing and the content view hidden
|
||||
LayoutInflater.from(context).inflate(R.layout.view_link_preview_draft, this)
|
||||
linkPreviewDraftContainer.isVisible = false
|
||||
thumbnailImageView.clipToOutline = true
|
||||
linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
||||
binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.linkPreviewDraftContainer.isVisible = false
|
||||
binding.thumbnailImageView.clipToOutline = true
|
||||
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
||||
}
|
||||
|
||||
fun update(glide: GlideRequests, linkPreview: LinkPreview) {
|
||||
// Hide the loader and show the content view
|
||||
linkPreviewDraftContainer.isVisible = true
|
||||
linkPreviewDraftLoader.isVisible = false
|
||||
thumbnailImageView.radius = toPx(4, resources)
|
||||
binding.linkPreviewDraftContainer.isVisible = true
|
||||
binding.linkPreviewDraftLoader.isVisible = false
|
||||
binding.thumbnailImageView.radius = toPx(4, resources)
|
||||
if (linkPreview.getThumbnail().isPresent) {
|
||||
// This internally fetches the thumbnail
|
||||
thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false)
|
||||
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false)
|
||||
}
|
||||
linkPreviewDraftTitleTextView.text = linkPreview.title
|
||||
binding.linkPreviewDraftTitleTextView.text = linkPreview.title
|
||||
}
|
||||
|
||||
private fun cancel() {
|
||||
|
@ -45,7 +45,7 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
||||
}
|
||||
|
||||
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
|
||||
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent)
|
||||
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context)
|
||||
val mentionCandidate = getItem(position)
|
||||
cell.glide = glide
|
||||
cell.mentionCandidate = mentionCandidate
|
||||
|
@ -4,32 +4,29 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewMentionCandidateBinding
|
||||
import org.session.libsession.messaging.mentions.Mention
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
class MentionCandidateView : LinearLayout {
|
||||
private lateinit var binding: ViewMentionCandidateBinding
|
||||
var mentionCandidate = Mention("", "")
|
||||
set(newValue) { field = newValue; update() }
|
||||
var glide: GlideRequests? = null
|
||||
var openGroupServer: String? = null
|
||||
var openGroupRoom: String? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
companion object {
|
||||
|
||||
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView {
|
||||
return layoutInflater.inflate(R.layout.view_mention_candidate, parent, false) as MentionCandidateView
|
||||
}
|
||||
private fun initialize() {
|
||||
binding = ViewMentionCandidateBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
private fun update() = with(binding) {
|
||||
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
||||
profilePictureView.publicKey = mentionCandidate.publicKey
|
||||
profilePictureView.displayName = mentionCandidate.displayName
|
||||
|
@ -5,8 +5,7 @@ import android.content.Intent
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import kotlinx.android.synthetic.main.view_open_group_guidelines.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewOpenGroupGuidelinesBinding
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
@ -18,13 +17,12 @@ class OpenGroupGuidelinesView : FrameLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null)
|
||||
addView(contentView)
|
||||
readButton.setOnClickListener {
|
||||
val activity = context as ConversationActivityV2
|
||||
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
|
||||
activity.push(intent)
|
||||
ViewOpenGroupGuidelinesBinding.inflate(LayoutInflater.from(context), this, true).apply {
|
||||
readButton.setOnClickListener {
|
||||
val activity = context as ConversationActivityV2
|
||||
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
|
||||
activity.push(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,22 +4,22 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_conversation_typing_container.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewConversationTypingContainerBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
|
||||
class TypingIndicatorViewContainer : LinearLayout {
|
||||
private lateinit var binding: ViewConversationTypingContainerBinding
|
||||
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_conversation_typing_container, this)
|
||||
binding = ViewConversationTypingContainerBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
|
||||
fun setTypists(typists: List<Recipient>) {
|
||||
if (typists.isEmpty()) { typingIndicator.stopAnimation(); return }
|
||||
typingIndicator.startAnimation()
|
||||
if (typists.isEmpty()) { binding.typingIndicator.stopAnimation(); return }
|
||||
binding.typingIndicator.startAnimation()
|
||||
}
|
||||
}
|
@ -6,8 +6,8 @@ import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_blocked.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogBlockedBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
@ -17,21 +17,21 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_blocked, null)
|
||||
val binding = DialogBlockedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
|
||||
val sessionID = recipient.address.toString()
|
||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
val title = resources.getString(R.string.dialog_blocked_title, name)
|
||||
contentView.blockedTitleTextView.text = title
|
||||
binding.blockedTitleTextView.text = title
|
||||
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
contentView.blockedExplanationTextView.text = spannable
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.unblockButton.setOnClickListener { unblock() }
|
||||
builder.setView(contentView)
|
||||
binding.blockedExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.unblockButton.setOnClickListener { unblock() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun unblock() {
|
||||
|
@ -7,8 +7,8 @@ import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.dialog_download.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogDownloadBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
@ -26,20 +26,20 @@ class DownloadDialog(private val recipient: Recipient) : BaseDialog() {
|
||||
@Inject lateinit var contactDB: SessionContactDatabase
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_download, null)
|
||||
val binding = DialogDownloadBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
val sessionID = recipient.address.toString()
|
||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
val title = resources.getString(R.string.dialog_download_title, name)
|
||||
contentView.downloadTitleTextView.text = title
|
||||
binding.downloadTitleTextView.text = title
|
||||
val explanation = resources.getString(R.string.dialog_download_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
contentView.downloadExplanationTextView.text = spannable
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.downloadButton.setOnClickListener { trust() }
|
||||
builder.setView(contentView)
|
||||
binding.downloadExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.downloadButton.setOnClickListener { trust() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun trust() {
|
||||
|
@ -7,8 +7,8 @@ import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogJoinOpenGroupBinding
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
@ -19,17 +19,17 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_join_open_group, null)
|
||||
val binding = DialogJoinOpenGroupBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
val title = resources.getString(R.string.dialog_join_open_group_title, name)
|
||||
contentView.joinOpenGroupTitleTextView.text = title
|
||||
binding.joinOpenGroupTitleTextView.text = title
|
||||
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
contentView.joinOpenGroupExplanationTextView.text = spannable
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.joinButton.setOnClickListener { join() }
|
||||
builder.setView(contentView)
|
||||
binding.joinOpenGroupExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.joinButton.setOnClickListener { join() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun join() {
|
||||
|
@ -2,8 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_link_preview.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogLinkPreviewBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
|
||||
@ -12,10 +11,10 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_link_preview, null)
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.enableLinkPreviewsButton.setOnClickListener { enable() }
|
||||
builder.setView(contentView)
|
||||
val binding = DialogLinkPreviewBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.enableLinkPreviewsButton.setOnClickListener { enable() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun enable() {
|
||||
|
@ -2,18 +2,17 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_send_seed.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogSendSeedBinding
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
|
||||
/** Shown if the user is about to send their recovery phrase to someone. */
|
||||
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_send_seed, null)
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.sendSeedButton.setOnClickListener { send() }
|
||||
builder.setView(contentView)
|
||||
val binding = DialogSendSeedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.sendSeedButton.setOnClickListener { send() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun send() {
|
||||
|
@ -4,13 +4,14 @@ import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.text.InputType
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_input_bar.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewInputBarBinding
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
@ -27,6 +28,7 @@ import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate {
|
||||
private lateinit var binding: ViewInputBarBinding
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private val vMargin by lazy { toDp(4, resources) }
|
||||
private val minHeight by lazy { toPx(56, resources) }
|
||||
@ -39,8 +41,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
set(value) { field = value; showOrHideInputIfNeeded() }
|
||||
|
||||
var text: String
|
||||
get() { return inputBarEditText.text?.toString() ?: "" }
|
||||
set(value) { inputBarEditText.setText(value) }
|
||||
get() { return binding.inputBarEditText.text?.toString() ?: "" }
|
||||
set(value) { binding.inputBarEditText.setText(value) }
|
||||
|
||||
val attachmentButtonsContainerHeight: Int
|
||||
get() = binding.attachmentsButtonContainer.height
|
||||
|
||||
private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) }
|
||||
private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone) }
|
||||
@ -52,36 +57,36 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_input_bar, this)
|
||||
binding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
// Attachments button
|
||||
attachmentsButtonContainer.addView(attachmentsButton)
|
||||
attachmentsButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
binding.attachmentsButtonContainer.addView(attachmentsButton)
|
||||
attachmentsButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
attachmentsButton.onPress = { toggleAttachmentOptions() }
|
||||
// Microphone button
|
||||
microphoneOrSendButtonContainer.addView(microphoneButton)
|
||||
microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
binding.microphoneOrSendButtonContainer.addView(microphoneButton)
|
||||
microphoneButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
microphoneButton.onLongPress = { startRecordingVoiceMessage() }
|
||||
microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
|
||||
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
|
||||
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }
|
||||
// Send button
|
||||
microphoneOrSendButtonContainer.addView(sendButton)
|
||||
sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
binding.microphoneOrSendButtonContainer.addView(sendButton)
|
||||
sendButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
sendButton.isVisible = false
|
||||
sendButton.onUp = { delegate?.sendMessage() }
|
||||
// Edit text
|
||||
val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0
|
||||
inputBarEditText.imeOptions = inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled
|
||||
inputBarEditText.inputType = inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
inputBarEditText.delegate = this
|
||||
binding.inputBarEditText.imeOptions = binding.inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled
|
||||
binding.inputBarEditText.inputType = binding.inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
binding.inputBarEditText.delegate = this
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region General
|
||||
private fun setHeight(newHeight: Int) {
|
||||
val layoutParams = inputBarLinearLayout.layoutParams as LayoutParams
|
||||
val layoutParams = binding.inputBarLinearLayout.layoutParams as LayoutParams
|
||||
layoutParams.height = newHeight
|
||||
inputBarLinearLayout.layoutParams = layoutParams
|
||||
binding.inputBarLinearLayout.layoutParams = layoutParams
|
||||
delegate?.inputBarHeightChanged(newHeight)
|
||||
}
|
||||
// endregion
|
||||
@ -94,7 +99,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -117,10 +122,10 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
quote = message
|
||||
linkPreview = null
|
||||
linkPreviewDraftView = null
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||
val quoteView = QuoteView(context, QuoteView.Mode.Draft)
|
||||
quoteView.delegate = this
|
||||
inputBarAdditionalContentContainer.addView(quoteView)
|
||||
binding.inputBarAdditionalContentContainer.addView(quoteView)
|
||||
val attachments = (message as? MmsMessageRecord)?.slideDeck
|
||||
// The max content width is the screen width - 2 times the horizontal input bar padding - the
|
||||
// quote view content area's start and end margins. This unfortunately has to be calculated manually
|
||||
@ -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
|
||||
// intrinsic height calculation.
|
||||
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
|
||||
setHeight(newHeight)
|
||||
}
|
||||
|
||||
override fun cancelQuoteDraft() {
|
||||
quote = null
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
|
||||
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||
val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight)
|
||||
additionalContentHeight = 0
|
||||
setHeight(newHeight)
|
||||
}
|
||||
@ -148,12 +153,12 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
fun draftLinkPreview() {
|
||||
quote = null
|
||||
val linkPreviewDraftHeight = toPx(88, resources)
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||
val linkPreviewDraftView = LinkPreviewDraftView(context)
|
||||
linkPreviewDraftView.delegate = this
|
||||
this.linkPreviewDraftView = linkPreviewDraftView
|
||||
inputBarAdditionalContentContainer.addView(linkPreviewDraftView)
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + linkPreviewDraftHeight
|
||||
binding.inputBarAdditionalContentContainer.addView(linkPreviewDraftView)
|
||||
val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight) + linkPreviewDraftHeight
|
||||
additionalContentHeight = linkPreviewDraftHeight
|
||||
setHeight(newHeight)
|
||||
}
|
||||
@ -167,24 +172,32 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
override fun cancelLinkPreviewDraft() {
|
||||
if (quote != null) { return }
|
||||
linkPreview = null
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
|
||||
binding.inputBarAdditionalContentContainer.removeAllViews()
|
||||
val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight)
|
||||
additionalContentHeight = 0
|
||||
setHeight(newHeight)
|
||||
}
|
||||
|
||||
private fun showOrHideInputIfNeeded() {
|
||||
if (showInput) {
|
||||
setOf( inputBarEditText, attachmentsButton ).forEach { it.isVisible = true }
|
||||
setOf( binding.inputBarEditText, attachmentsButton ).forEach { it.isVisible = true }
|
||||
microphoneButton.isVisible = text.isEmpty()
|
||||
sendButton.isVisible = text.isNotEmpty()
|
||||
} else {
|
||||
cancelQuoteDraft()
|
||||
cancelLinkPreviewDraft()
|
||||
val views = setOf( inputBarEditText, attachmentsButton, microphoneButton, sendButton )
|
||||
val views = setOf( binding.inputBarEditText, attachmentsButton, microphoneButton, sendButton )
|
||||
views.forEach { it.isVisible = false }
|
||||
}
|
||||
}
|
||||
|
||||
fun addTextChangedListener(textWatcher: TextWatcher) {
|
||||
binding.inputBarEditText.addTextChangedListener(textWatcher)
|
||||
}
|
||||
|
||||
fun setSelection(index: Int) {
|
||||
binding.inputBarEditText.setSelection(index)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
|
@ -45,8 +45,8 @@ class InputBarEditText : AppCompatEditText {
|
||||
delegate?.inputBarEditTextHeightChanged(constrainedHeight.roundToInt())
|
||||
}
|
||||
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||
val ic: InputConnection = super.onCreateInputConnection(editorInfo)
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? {
|
||||
val ic = super.onCreateInputConnection(editorInfo) ?: return null
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png", "image/gif", "image/jpg"))
|
||||
|
||||
val callback =
|
||||
|
@ -8,40 +8,56 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.animateSizeChange
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
class InputBarRecordingView : RelativeLayout {
|
||||
private lateinit var binding: ViewInputBarRecordingBinding
|
||||
private var startTimestamp = 0L
|
||||
private val snHandler = Handler(Looper.getMainLooper())
|
||||
private var dotViewAnimation: ValueAnimator? = null
|
||||
private var pulseAnimation: ValueAnimator? = null
|
||||
var delegate: InputBarRecordingViewDelegate? = null
|
||||
|
||||
val lockView: LinearLayout
|
||||
get() = binding.lockView
|
||||
|
||||
val chevronImageView: ImageView
|
||||
get() = binding.inputBarChevronImageView
|
||||
|
||||
val slideToCancelTextView: TextView
|
||||
get() = binding.inputBarSlideToCancelTextView
|
||||
|
||||
val recordButtonOverlay: RelativeLayout
|
||||
get() = binding.recordButtonOverlay
|
||||
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_input_bar_recording, this)
|
||||
inputBarMiddleContentContainer.disableClipping()
|
||||
inputBarCancelButton.setOnClickListener { hide() }
|
||||
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.inputBarMiddleContentContainer.disableClipping()
|
||||
binding.inputBarCancelButton.setOnClickListener { hide() }
|
||||
}
|
||||
|
||||
fun show() {
|
||||
startTimestamp = Date().time
|
||||
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
||||
inputBarCancelButton.alpha = 0.0f
|
||||
inputBarMiddleContentContainer.alpha = 1.0f
|
||||
lockView.alpha = 1.0f
|
||||
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
||||
binding.inputBarCancelButton.alpha = 0.0f
|
||||
binding.inputBarMiddleContentContainer.alpha = 1.0f
|
||||
binding.lockView.alpha = 1.0f
|
||||
isVisible = true
|
||||
alpha = 0.0f
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||
@ -77,7 +93,7 @@ class InputBarRecordingView : RelativeLayout {
|
||||
dotViewAnimation = animation
|
||||
animation.duration = 500L
|
||||
animation.addUpdateListener { animator ->
|
||||
dotView.alpha = animator.animatedValue as Float
|
||||
binding.dotView.alpha = animator.animatedValue as Float
|
||||
}
|
||||
animation.repeatCount = ValueAnimator.INFINITE
|
||||
animation.repeatMode = ValueAnimator.REVERSE
|
||||
@ -87,12 +103,12 @@ class InputBarRecordingView : RelativeLayout {
|
||||
private fun pulse() {
|
||||
val collapsedSize = toPx(80.0f, resources)
|
||||
val expandedSize = toPx(104.0f, resources)
|
||||
pulseView.animateSizeChange(collapsedSize, expandedSize, 1000)
|
||||
binding.pulseView.animateSizeChange(collapsedSize, expandedSize, 1000)
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.5, 0.0f)
|
||||
pulseAnimation = animation
|
||||
animation.duration = 1000L
|
||||
animation.addUpdateListener { animator ->
|
||||
pulseView.alpha = animator.animatedValue as Float
|
||||
binding.pulseView.alpha = animator.animatedValue as Float
|
||||
if (animator.animatedFraction == 1.0f && isVisible) { pulse() }
|
||||
}
|
||||
animation.start()
|
||||
@ -101,21 +117,21 @@ class InputBarRecordingView : RelativeLayout {
|
||||
private fun animateLockViewUp() {
|
||||
val startMarginBottom = toPx(32, resources)
|
||||
val endMarginBottom = toPx(72, resources)
|
||||
val layoutParams = lockView.layoutParams as LayoutParams
|
||||
val layoutParams = binding.lockView.layoutParams as LayoutParams
|
||||
layoutParams.bottomMargin = startMarginBottom
|
||||
lockView.layoutParams = layoutParams
|
||||
binding.lockView.layoutParams = layoutParams
|
||||
val animation = ValueAnimator.ofObject(IntEvaluator(), startMarginBottom, endMarginBottom)
|
||||
animation.duration = 250L
|
||||
animation.addUpdateListener { animator ->
|
||||
layoutParams.bottomMargin = animator.animatedValue as Int
|
||||
lockView.layoutParams = layoutParams
|
||||
binding.lockView.layoutParams = layoutParams
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
private fun updateTimer() {
|
||||
val duration = (Date().time - startTimestamp) / 1000L
|
||||
recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||
snHandler.postDelayed({ updateTimer() }, 500)
|
||||
}
|
||||
|
||||
@ -123,19 +139,19 @@ class InputBarRecordingView : RelativeLayout {
|
||||
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||
fadeOutAnimation.duration = 250L
|
||||
fadeOutAnimation.addUpdateListener { animator ->
|
||||
inputBarMiddleContentContainer.alpha = animator.animatedValue as Float
|
||||
lockView.alpha = animator.animatedValue as Float
|
||||
binding.inputBarMiddleContentContainer.alpha = animator.animatedValue as Float
|
||||
binding.lockView.alpha = animator.animatedValue as Float
|
||||
}
|
||||
fadeOutAnimation.start()
|
||||
val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||
fadeInAnimation.duration = 250L
|
||||
fadeInAnimation.addUpdateListener { animator ->
|
||||
inputBarCancelButton.alpha = animator.animatedValue as Float
|
||||
binding.inputBarCancelButton.alpha = animator.animatedValue as Float
|
||||
}
|
||||
fadeInAnimation.start()
|
||||
recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
|
||||
recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
|
||||
inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }
|
||||
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
|
||||
binding.recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
|
||||
binding.inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,33 +4,29 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewMentionCandidateV2Binding
|
||||
import org.session.libsession.messaging.mentions.Mention
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : RelativeLayout(context, attrs, defStyleAttr) {
|
||||
class MentionCandidateView : RelativeLayout {
|
||||
private lateinit var binding: ViewMentionCandidateV2Binding
|
||||
var candidate = Mention("", "")
|
||||
set(newValue) { field = newValue; update() }
|
||||
var glide: GlideRequests? = null
|
||||
var openGroupServer: String? = null
|
||||
var openGroupRoom: String? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
companion object {
|
||||
|
||||
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView {
|
||||
return layoutInflater.inflate(R.layout.view_mention_candidate_v2, parent, false) as MentionCandidateView
|
||||
}
|
||||
private fun initialize() {
|
||||
binding = ViewMentionCandidateV2Binding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
private fun update() = with(binding) {
|
||||
mentionCandidateNameTextView.text = candidate.displayName
|
||||
profilePictureView.publicKey = candidate.publicKey
|
||||
profilePictureView.displayName = candidate.displayName
|
||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
@ -42,7 +41,7 @@ class MentionCandidatesView(context: Context, attrs: AttributeSet?, defStyleAttr
|
||||
override fun getItem(position: Int): Mention { return candidates[position] }
|
||||
|
||||
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
|
||||
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent)
|
||||
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context)
|
||||
val mentionCandidate = getItem(position)
|
||||
cell.glide = glide
|
||||
cell.candidate = mentionCandidate
|
||||
|
@ -12,7 +12,6 @@ import android.os.AsyncTask
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
@ -24,7 +23,6 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
@ -35,7 +33,12 @@ import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.*
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.ExpirationDialog
|
||||
import org.thoughtcrime.securesms.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.MuteDialog
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.ShortcutLauncherActivity
|
||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
|
||||
@ -101,15 +104,12 @@ object ConversationMenuHelper {
|
||||
val searchViewItem = menu.findItem(R.id.menu_search)
|
||||
(context as ConversationActivityV2).searchViewItem = searchViewItem
|
||||
val searchView = searchViewItem.actionView as SearchView
|
||||
val searchViewModel = context.searchViewModel!!
|
||||
val queryListener = object : OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
searchViewModel.onQueryUpdated(query, threadId)
|
||||
context.searchBottomBar.showLoading()
|
||||
context.onSearchQueryUpdated(query)
|
||||
return true
|
||||
}
|
||||
@ -117,10 +117,7 @@ object ConversationMenuHelper {
|
||||
searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
searchView.setOnQueryTextListener(queryListener)
|
||||
searchViewModel.onSearchOpened()
|
||||
context.searchBottomBar.visibility = View.VISIBLE
|
||||
context.searchBottomBar.setData(0, 0)
|
||||
context.inputBar.visibility = View.GONE
|
||||
context.onSearchOpened()
|
||||
for (i in 0 until menu.size()) {
|
||||
if (menu.getItem(i) != searchViewItem) {
|
||||
menu.getItem(i).isVisible = false
|
||||
@ -131,11 +128,7 @@ object ConversationMenuHelper {
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
searchView.setOnQueryTextListener(null)
|
||||
searchViewModel.onSearchClosed()
|
||||
context.searchBottomBar.visibility = View.GONE
|
||||
context.inputBar.visibility = View.VISIBLE
|
||||
context.onSearchQueryUpdated(null)
|
||||
context.invalidateOptionsMenu()
|
||||
context.onSearchClosed()
|
||||
return true
|
||||
}
|
||||
})
|
||||
@ -169,7 +162,7 @@ object ConversationMenuHelper {
|
||||
}
|
||||
|
||||
private fun search(context: Context) {
|
||||
val searchViewModel = (context as ConversationActivityV2).searchViewModel!!
|
||||
val searchViewModel = (context as ConversationActivityV2).searchViewModel
|
||||
searchViewModel.onSearchOpened()
|
||||
}
|
||||
|
||||
|
@ -7,35 +7,41 @@ import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.view_control_message.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewControlMessageBinding
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
class ControlMessageView : LinearLayout {
|
||||
|
||||
private lateinit var binding: ViewControlMessageBinding
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_control_message, this)
|
||||
binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, previous: MessageRecord?) {
|
||||
dateBreakTextView.showDateBreak(message, previous)
|
||||
iconImageView.visibility = View.GONE
|
||||
binding.dateBreakTextView.showDateBreak(message, previous)
|
||||
binding.iconImageView.visibility = View.GONE
|
||||
if (message.isExpirationTimerUpdate) {
|
||||
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme))
|
||||
iconImageView.visibility = View.VISIBLE
|
||||
binding.iconImageView.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)
|
||||
)
|
||||
binding.iconImageView.visibility = View.VISIBLE
|
||||
} else if (message.isMediaSavedNotification) {
|
||||
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme))
|
||||
iconImageView.visibility = View.VISIBLE
|
||||
binding.iconImageView.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme)
|
||||
)
|
||||
binding.iconImageView.visibility = View.VISIBLE
|
||||
}
|
||||
textView.text = message.getDisplayBody(context)
|
||||
binding.textView.text = message.getDisplayBody(context)
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
|
@ -6,32 +6,28 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.view.*
|
||||
import kotlinx.android.synthetic.main.view_deleted_message.view.*
|
||||
import kotlinx.android.synthetic.main.view_document.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewDeletedMessageBinding
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import java.util.*
|
||||
|
||||
class DeletedMessageView : LinearLayout {
|
||||
|
||||
private lateinit var binding: ViewDeletedMessageBinding
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_deleted_message, this)
|
||||
binding = ViewDeletedMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||
assert(message.isDeleted)
|
||||
deleteTitleTextView.text = context.getString(R.string.deleted_message)
|
||||
deleteTitleTextView.setTextColor(textColor)
|
||||
deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
binding.deleteTitleTextView.text = context.getString(R.string.deleted_message)
|
||||
binding.deleteTitleTextView.setTextColor(textColor)
|
||||
binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -6,29 +6,27 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import kotlinx.android.synthetic.main.view_document.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import network.loki.messenger.databinding.ViewDocumentBinding
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
|
||||
class DocumentView : LinearLayout {
|
||||
|
||||
private lateinit var binding: ViewDocumentBinding
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_document, this)
|
||||
binding = ViewDocumentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MmsMessageRecord, @ColorInt textColor: Int) {
|
||||
val document = message.slideDeck.documentSlide!!
|
||||
documentTitleTextView.text = document.fileName.or("Untitled File")
|
||||
documentTitleTextView.setTextColor(textColor)
|
||||
documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
binding.documentTitleTextView.text = document.fileName.or("Untitled File")
|
||||
binding.documentTitleTextView.setTextColor(textColor)
|
||||
binding.documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -11,8 +11,8 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_link_preview.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewLinkPreviewBinding
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
|
||||
class LinkPreviewView : LinearLayout {
|
||||
private lateinit var binding: ViewLinkPreviewBinding
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var url: String? = null
|
||||
lateinit var bodyTextView: TextView
|
||||
@ -33,7 +34,7 @@ class LinkPreviewView : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_link_preview, this)
|
||||
binding = ViewLinkPreviewBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -44,20 +45,20 @@ class LinkPreviewView : LinearLayout {
|
||||
// Thumbnail
|
||||
if (linkPreview.getThumbnail().isPresent) {
|
||||
// This internally fetches the thumbnail
|
||||
thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
||||
thumbnailImageView.loadIndicator.isVisible = false
|
||||
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
||||
binding.thumbnailImageView.loadIndicator.isVisible = false
|
||||
}
|
||||
// Title
|
||||
titleTextView.text = linkPreview.title
|
||||
binding.titleTextView.text = linkPreview.title
|
||||
val textColorID = if (message.isOutgoing && UiModeUtilities.isDayUiMode(context)) {
|
||||
R.color.white
|
||||
} else {
|
||||
if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.white
|
||||
}
|
||||
titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
|
||||
binding.titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
|
||||
// Body
|
||||
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
mainLinkPreviewContainer.addView(bodyTextView)
|
||||
binding.mainLinkPreviewContainer.addView(bodyTextView)
|
||||
// Corner radii
|
||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||
@ -78,7 +79,7 @@ class LinkPreviewView : LinearLayout {
|
||||
val rawYInt = event.rawY.toInt()
|
||||
val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||
val previewRect = Rect()
|
||||
mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
|
||||
binding.mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
|
||||
if (previewRect.contains(hitRect)) {
|
||||
openURL()
|
||||
return
|
||||
|
@ -6,15 +6,15 @@ import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.android.synthetic.main.view_open_group_invitation.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
import network.loki.messenger.databinding.ViewOpenGroupInvitationBinding
|
||||
import org.session.libsession.messaging.utilities.UpdateMessageData
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
class OpenGroupInvitationView : LinearLayout {
|
||||
private lateinit var binding: ViewOpenGroupInvitationBinding
|
||||
private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null
|
||||
|
||||
constructor(context: Context): super(context) { initialize() }
|
||||
@ -22,7 +22,7 @@ class OpenGroupInvitationView : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_open_group_invitation, this)
|
||||
binding = ViewOpenGroupInvitationBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
|
||||
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||
@ -31,12 +31,14 @@ class OpenGroupInvitationView : LinearLayout {
|
||||
val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation
|
||||
this.data = data
|
||||
val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus
|
||||
openGroupInvitationIconImageView.setImageResource(iconID)
|
||||
openGroupTitleTextView.text = data.groupName
|
||||
openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl)
|
||||
openGroupTitleTextView.setTextColor(textColor)
|
||||
openGroupJoinMessageTextView.setTextColor(textColor)
|
||||
openGroupURLTextView.setTextColor(textColor)
|
||||
with(binding){
|
||||
openGroupInvitationIconImageView.setImageResource(iconID)
|
||||
openGroupTitleTextView.text = data.groupName
|
||||
openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl)
|
||||
openGroupTitleTextView.setTextColor(textColor)
|
||||
openGroupJoinMessageTextView.setTextColor(textColor)
|
||||
openGroupURLTextView.setTextColor(textColor)
|
||||
}
|
||||
}
|
||||
|
||||
fun joinOpenGroup() {
|
||||
|
@ -11,8 +11,8 @@ import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.view_quote.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewQuoteBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||
@ -39,6 +39,7 @@ class QuoteView : LinearLayout {
|
||||
|
||||
@Inject lateinit var contactDb: SessionContactDatabase
|
||||
|
||||
private lateinit var binding: ViewQuoteBinding
|
||||
private lateinit var mode: Mode
|
||||
private val vPadding by lazy { toPx(6, resources) }
|
||||
var delegate: QuoteViewDelegate? = null
|
||||
@ -52,19 +53,19 @@ class QuoteView : LinearLayout {
|
||||
|
||||
constructor(context: Context, mode: Mode) : super(context) {
|
||||
this.mode = mode
|
||||
LayoutInflater.from(context).inflate(R.layout.view_quote, this)
|
||||
// Add padding here (not on mainQuoteViewContainer) to get a bit of a top inset while avoiding
|
||||
binding = ViewQuoteBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
// Add padding here (not on binding.mainQuoteViewContainer) to get a bit of a top inset while avoiding
|
||||
// the clipping issue described in getIntrinsicHeight(maxContentWidth:).
|
||||
setPadding(0, toPx(6, resources), 0, 0)
|
||||
when (mode) {
|
||||
Mode.Draft -> quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
|
||||
Mode.Draft -> binding.quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() }
|
||||
Mode.Regular -> {
|
||||
quoteViewCancelButton.isVisible = false
|
||||
mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme))
|
||||
val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
|
||||
binding.quoteViewCancelButton.isVisible = false
|
||||
binding.mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme))
|
||||
val quoteViewMainContentContainerLayoutParams = binding.quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
|
||||
// Since we're not showing the cancel button we can shorten the end margin
|
||||
quoteViewMainContentContainerLayoutParams.marginEnd = resources.getDimension(R.dimen.medium_spacing).roundToInt()
|
||||
quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
|
||||
binding.quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,19 +74,19 @@ class QuoteView : LinearLayout {
|
||||
// region General
|
||||
fun getIntrinsicContentHeight(maxContentWidth: Int): Int {
|
||||
// If we're showing an attachment thumbnail, just constrain to the height of that
|
||||
if (quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) }
|
||||
if (binding.quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) }
|
||||
var result = 0
|
||||
var authorTextViewIntrinsicHeight = 0
|
||||
if (quoteViewAuthorTextView.isVisible) {
|
||||
val author = quoteViewAuthorTextView.text
|
||||
authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, quoteViewAuthorTextView.paint, maxContentWidth)
|
||||
if (binding.quoteViewAuthorTextView.isVisible) {
|
||||
val author = binding.quoteViewAuthorTextView.text
|
||||
authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, binding.quoteViewAuthorTextView.paint, maxContentWidth)
|
||||
result += authorTextViewIntrinsicHeight
|
||||
}
|
||||
val body = quoteViewBodyTextView.text
|
||||
val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, quoteViewBodyTextView.paint, maxContentWidth)
|
||||
val staticLayout = TextUtilities.getIntrinsicLayout(body, quoteViewBodyTextView.paint, maxContentWidth)
|
||||
val body = binding.quoteViewBodyTextView.text
|
||||
val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
|
||||
val staticLayout = TextUtilities.getIntrinsicLayout(body, binding.quoteViewBodyTextView.paint, maxContentWidth)
|
||||
result += bodyTextViewIntrinsicHeight
|
||||
if (!quoteViewAuthorTextView.isVisible) {
|
||||
if (!binding.quoteViewAuthorTextView.isVisible) {
|
||||
// We want to at least be as high as the cancel button 36DP, and no higher than 3 lines of text.
|
||||
// Height from intrinsic layout is the height of the text before truncation so we shorten
|
||||
// proportionally to our max lines setting.
|
||||
@ -115,82 +116,90 @@ class QuoteView : LinearLayout {
|
||||
// Reduce the max body text view line count to 2 if this is a group thread because
|
||||
// we'll be showing the author text view and we don't want the overall quote view height
|
||||
// to get too big.
|
||||
quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3
|
||||
binding.quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3
|
||||
// Author
|
||||
if (thread.isGroupRecipient) {
|
||||
val author = contactDb.getContactWithSessionID(authorPublicKey)
|
||||
val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey
|
||||
quoteViewAuthorTextView.text = authorDisplayName
|
||||
quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
binding.quoteViewAuthorTextView.text = authorDisplayName
|
||||
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
}
|
||||
quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
||||
binding.quoteViewAuthorTextView.isVisible = thread.isGroupRecipient
|
||||
// Body
|
||||
quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context);
|
||||
quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context);
|
||||
binding.quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
// Accent line / attachment preview
|
||||
val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing
|
||||
quoteViewAccentLine.isVisible = !hasAttachments
|
||||
quoteViewAttachmentPreviewContainer.isVisible = hasAttachments
|
||||
binding.quoteViewAccentLine.isVisible = !hasAttachments
|
||||
binding.quoteViewAttachmentPreviewContainer.isVisible = hasAttachments
|
||||
if (!hasAttachments) {
|
||||
val accentLineLayoutParams = quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams
|
||||
val accentLineLayoutParams = binding.quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams
|
||||
accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height
|
||||
quoteViewAccentLine.layoutParams = accentLineLayoutParams
|
||||
quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage))
|
||||
binding.quoteViewAccentLine.layoutParams = accentLineLayoutParams
|
||||
binding.quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage))
|
||||
} else if (attachments != null) {
|
||||
quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme))
|
||||
binding.quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme))
|
||||
val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent
|
||||
val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme)
|
||||
quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
||||
quoteViewAttachmentPreviewImageView.isVisible = false
|
||||
quoteViewAttachmentThumbnailImageView.isVisible = false
|
||||
if (attachments.audioSlide != null) {
|
||||
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||
quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
|
||||
} else if (attachments.documentSlide != null) {
|
||||
quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
||||
quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
quoteViewBodyTextView.text = resources.getString(R.string.document)
|
||||
} else if (attachments.thumbnailSlide != null) {
|
||||
val slide = attachments.thumbnailSlide!!
|
||||
// This internally fetches the thumbnail
|
||||
quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources)
|
||||
quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false)
|
||||
quoteViewAttachmentThumbnailImageView.isVisible = true
|
||||
quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
|
||||
binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
||||
binding.quoteViewAttachmentPreviewImageView.isVisible = false
|
||||
binding.quoteViewAttachmentThumbnailImageView.isVisible = false
|
||||
when {
|
||||
attachments.audioSlide != null -> {
|
||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
binding.quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
|
||||
}
|
||||
attachments.documentSlide != null -> {
|
||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
||||
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
binding.quoteViewBodyTextView.text = resources.getString(R.string.document)
|
||||
}
|
||||
attachments.thumbnailSlide != null -> {
|
||||
val slide = attachments.thumbnailSlide!!
|
||||
// This internally fetches the thumbnail
|
||||
binding.quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources)
|
||||
binding.quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false)
|
||||
binding.quoteViewAttachmentThumbnailImageView.isVisible = true
|
||||
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
|
||||
}
|
||||
}
|
||||
}
|
||||
mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth))
|
||||
val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams
|
||||
binding.mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth))
|
||||
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
|
||||
quoteViewMainContentContainerLayoutParams.marginStart = if (!hasAttachments) toPx(16, resources) else toPx(48, resources)
|
||||
quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
|
||||
binding.quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
@ColorInt private fun getLineColor(isOutgoingMessage: Boolean): Int {
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||
if ((mode == Mode.Regular && isLightMode) || (mode == Mode.Draft && isLightMode)) {
|
||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
} else if (mode == Mode.Regular && !isLightMode) {
|
||||
if (isOutgoingMessage) {
|
||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
} else {
|
||||
return ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
||||
return when {
|
||||
mode == Mode.Regular && isLightMode || mode == Mode.Draft && isLightMode -> {
|
||||
ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
}
|
||||
mode == Mode.Regular && !isLightMode -> {
|
||||
if (isOutgoingMessage) {
|
||||
ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
} else {
|
||||
ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
||||
}
|
||||
}
|
||||
else -> { // Draft & dark mode
|
||||
ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
||||
}
|
||||
} else { // Draft & dark mode
|
||||
return ResourcesCompat.getColor(resources, R.color.accent, context.theme)
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt private fun getTextColor(isOutgoingMessage: Boolean): Int {
|
||||
if (mode == Mode.Draft) { return ResourcesCompat.getColor(resources, R.color.text, context.theme) }
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
||||
if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) {
|
||||
return ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
return if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) {
|
||||
ResourcesCompat.getColor(resources, R.color.black, context.theme)
|
||||
} else {
|
||||
return ResourcesCompat.getColor(resources, R.color.white, context.theme)
|
||||
ResourcesCompat.getColor(resources, R.color.white, context.theme)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
@ -6,15 +6,15 @@ import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.android.synthetic.main.view_untrusted_attachment.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
|
||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
class UntrustedAttachmentView: LinearLayout {
|
||||
|
||||
private lateinit var binding: ViewUntrustedAttachmentBinding
|
||||
enum class AttachmentType {
|
||||
AUDIO,
|
||||
DOCUMENT,
|
||||
@ -27,7 +27,7 @@ class UntrustedAttachmentView: LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_untrusted_attachment, this)
|
||||
binding = ViewUntrustedAttachmentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -42,8 +42,8 @@ class UntrustedAttachmentView: LinearLayout {
|
||||
iconDrawable.mutate().setTint(textColor)
|
||||
val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT))
|
||||
|
||||
untrustedAttachmentIcon.setImageDrawable(iconDrawable)
|
||||
untrustedAttachmentTitle.text = text
|
||||
binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable)
|
||||
binding.untrustedAttachmentTitle.text = text
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
@ -23,8 +23,9 @@ import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.toSpannable
|
||||
import kotlinx.android.synthetic.main.view_visible_message_content.view.*
|
||||
import androidx.core.view.children
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
@ -40,14 +41,14 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.SearchUtil
|
||||
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.util.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VisibleMessageContentView : LinearLayout {
|
||||
private lateinit var binding: ViewVisibleMessageContentBinding
|
||||
var onContentClick: ((event: MotionEvent) -> Unit)? = null
|
||||
var onContentDoubleTap: (() -> Unit)? = null
|
||||
var delegate: VisibleMessageContentViewDelegate? = null
|
||||
@ -59,7 +60,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_visible_message_content, this)
|
||||
binding = ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -74,17 +75,17 @@ class VisibleMessageContentView : LinearLayout {
|
||||
background.colorFilter = filter
|
||||
setBackground(background)
|
||||
// Body
|
||||
mainContainer.removeAllViews()
|
||||
binding.mainContainer.removeAllViews()
|
||||
onContentClick = null
|
||||
onContentDoubleTap = null
|
||||
if (message.isDeleted) {
|
||||
val deletedMessageView = DeletedMessageView(context)
|
||||
deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message))
|
||||
mainContainer.addView(deletedMessageView)
|
||||
deletedMessageView.bind(message, getTextColor(context,message))
|
||||
binding.mainContainer.addView(deletedMessageView)
|
||||
} else if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
||||
val linkPreviewView = LinkPreviewView(context)
|
||||
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
|
||||
mainContainer.addView(linkPreviewView)
|
||||
binding.mainContainer.addView(linkPreviewView)
|
||||
onContentClick = { event -> linkPreviewView.calculateHit(event) }
|
||||
// Body text view is inside the link preview for layout convenience
|
||||
} else if (message is MmsMessageRecord && message.quote != null) {
|
||||
@ -102,10 +103,10 @@ class VisibleMessageContentView : LinearLayout {
|
||||
quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
|
||||
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId,
|
||||
quote.isOriginalMissing, glide)
|
||||
mainContainer.addView(quoteView)
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
binding.mainContainer.addView(quoteView)
|
||||
val bodyTextView = getBodyTextView(context, message, searchQuery)
|
||||
ViewUtil.setPaddingTop(bodyTextView, 0)
|
||||
mainContainer.addView(bodyTextView)
|
||||
binding.mainContainer.addView(bodyTextView)
|
||||
onContentClick = { event ->
|
||||
val r = Rect()
|
||||
quoteView.getGlobalVisibleRect(r)
|
||||
@ -124,34 +125,34 @@ class VisibleMessageContentView : LinearLayout {
|
||||
voiceMessageView.indexInAdapter = indexInAdapter
|
||||
voiceMessageView.delegate = context as? ConversationActivityV2
|
||||
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
|
||||
// message view) so as to not interfere with all the other gestures.
|
||||
onContentClick = { voiceMessageView.togglePlayback() }
|
||||
onContentDoubleTap = { voiceMessageView.handleDoubleTap() }
|
||||
} else {
|
||||
val untrustedView = UntrustedAttachmentView(context)
|
||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message))
|
||||
mainContainer.addView(untrustedView)
|
||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, getTextColor(context,message))
|
||||
binding.mainContainer.addView(untrustedView)
|
||||
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
|
||||
// Document attachment
|
||||
if (contactIsTrusted || message.isOutgoing) {
|
||||
val documentView = DocumentView(context)
|
||||
documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
mainContainer.addView(documentView)
|
||||
documentView.bind(message, getTextColor(context, message))
|
||||
binding.mainContainer.addView(documentView)
|
||||
} else {
|
||||
val untrustedView = UntrustedAttachmentView(context)
|
||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message))
|
||||
mainContainer.addView(untrustedView)
|
||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, getTextColor(context,message))
|
||||
binding.mainContainer.addView(untrustedView)
|
||||
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
} else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) {
|
||||
// Images/Video attachment
|
||||
if (contactIsTrusted || message.isOutgoing) {
|
||||
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
|
||||
// bind after add view because views are inflated and calculated during bind
|
||||
albumThumbnailView.bind(
|
||||
@ -165,18 +166,18 @@ class VisibleMessageContentView : LinearLayout {
|
||||
}
|
||||
} else {
|
||||
val untrustedView = UntrustedAttachmentView(context)
|
||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
||||
mainContainer.addView(untrustedView)
|
||||
untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, getTextColor(context,message))
|
||||
binding.mainContainer.addView(untrustedView)
|
||||
onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) }
|
||||
}
|
||||
} else if (message.isOpenGroupInvitation) {
|
||||
val openGroupInvitationView = OpenGroupInvitationView(context)
|
||||
openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
mainContainer.addView(openGroupInvitationView)
|
||||
openGroupInvitationView.bind(message, getTextColor(context, message))
|
||||
binding.mainContainer.addView(openGroupInvitationView)
|
||||
onContentClick = { openGroupInvitationView.joinOpenGroup() }
|
||||
} else {
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
mainContainer.addView(bodyTextView)
|
||||
val bodyTextView = getBodyTextView(context, message, searchQuery)
|
||||
binding.mainContainer.addView(bodyTextView)
|
||||
onContentClick = { event ->
|
||||
// intersectedModalSpans should only be a list of one item
|
||||
bodyTextView.getIntersectedModalSpans(event).forEach { span ->
|
||||
@ -188,21 +189,33 @@ class VisibleMessageContentView : LinearLayout {
|
||||
|
||||
private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable {
|
||||
val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster)
|
||||
@DrawableRes val backgroundID: Int
|
||||
if (isSingleMessage) {
|
||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
|
||||
} else if (isStartOfMessageCluster) {
|
||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start
|
||||
} else if (isEndOfMessageCluster) {
|
||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end
|
||||
} else {
|
||||
backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle
|
||||
@DrawableRes val backgroundID = when {
|
||||
isSingleMessage -> {
|
||||
if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
|
||||
}
|
||||
isStartOfMessageCluster -> {
|
||||
if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start
|
||||
}
|
||||
isEndOfMessageCluster -> {
|
||||
if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end
|
||||
}
|
||||
else -> {
|
||||
if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle
|
||||
}
|
||||
}
|
||||
return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!!
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
mainContainer.removeAllViews()
|
||||
binding.mainContainer.removeAllViews()
|
||||
}
|
||||
|
||||
fun playVoiceMessage() {
|
||||
binding.mainContainer.children.forEach { view ->
|
||||
if (view is VoiceMessageView) {
|
||||
return@forEach view.togglePlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -227,8 +240,10 @@ class VisibleMessageContentView : LinearLayout {
|
||||
var body = message.body.toSpannable()
|
||||
|
||||
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context)
|
||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
|
||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
|
||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(),
|
||||
{ BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
|
||||
body = SearchUtil.getHighlightedSpan(Locale.getDefault(),
|
||||
{ ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
|
||||
|
||||
Linkify.addLinks(body, Linkify.WEB_URLS)
|
||||
|
||||
|
@ -5,12 +5,15 @@ import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.*
|
||||
import android.view.Gravity
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@ -19,25 +22,35 @@ import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.view_visible_message.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.*
|
||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.*
|
||||
import java.util.*
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.toDp
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class VisibleMessageView : LinearLayout {
|
||||
|
||||
@ -48,6 +61,7 @@ class VisibleMessageView : LinearLayout {
|
||||
@Inject lateinit var smsDb: SmsDatabase
|
||||
@Inject lateinit var mmsDb: MmsDatabase
|
||||
|
||||
private lateinit var binding: ViewVisibleMessageBinding
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
||||
private val swipeToReplyIconRect = Rect()
|
||||
@ -60,7 +74,11 @@ class VisibleMessageView : LinearLayout {
|
||||
private var onDoubleTap: (() -> Unit)? = null
|
||||
var indexInAdapter: Int = -1
|
||||
var snIsSelected = false
|
||||
set(value) { field = value; handleIsSelectedChanged()}
|
||||
set(value) {
|
||||
field = value
|
||||
binding.messageTimestampTextView.isVisible = isSelected
|
||||
handleIsSelectedChanged()
|
||||
}
|
||||
var onPress: ((event: MotionEvent) -> Unit)? = null
|
||||
var onSwipeToReply: (() -> Unit)? = null
|
||||
var onLongPress: (() -> Unit)? = null
|
||||
@ -68,7 +86,7 @@ class VisibleMessageView : LinearLayout {
|
||||
|
||||
companion object {
|
||||
const val swipeToReplyThreshold = 64.0f // dp
|
||||
const val longPressMovementTreshold = 10.0f // dp
|
||||
const val longPressMovementThreshold = 10.0f // dp
|
||||
const val longPressDurationThreshold = 250L // ms
|
||||
const val maxDoubleTapInterval = 200L
|
||||
}
|
||||
@ -79,12 +97,12 @@ class VisibleMessageView : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_visible_message, this)
|
||||
binding = ViewVisibleMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
isHapticFeedbackEnabled = true
|
||||
setWillNotDraw(false)
|
||||
expirationTimerViewContainer.disableClipping()
|
||||
messageContentContainer.disableClipping()
|
||||
binding.expirationTimerViewContainer.disableClipping()
|
||||
binding.messageContentContainer.disableClipping()
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -101,47 +119,46 @@ class VisibleMessageView : LinearLayout {
|
||||
// Show profile picture and sender name if this is a group thread AND
|
||||
// the message is incoming
|
||||
if (isGroupThread && !message.isOutgoing) {
|
||||
profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
|
||||
profilePictureView.publicKey = senderSessionID
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.update(message.individualRecipient, threadID)
|
||||
profilePictureView.setOnClickListener {
|
||||
binding.profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE
|
||||
binding.profilePictureView.publicKey = senderSessionID
|
||||
binding.profilePictureView.glide = glide
|
||||
binding.profilePictureView.update(message.individualRecipient)
|
||||
binding.profilePictureView.setOnClickListener {
|
||||
showUserDetails(senderSessionID, threadID)
|
||||
}
|
||||
if (thread.isOpenGroupRecipient) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||
val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server)
|
||||
moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
|
||||
binding.moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
|
||||
} else {
|
||||
moderatorIconImageView.visibility = View.INVISIBLE
|
||||
binding.moderatorIconImageView.visibility = View.INVISIBLE
|
||||
}
|
||||
senderNameTextView.isVisible = isStartOfMessageCluster
|
||||
binding.senderNameTextView.isVisible = isStartOfMessageCluster
|
||||
val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||
senderNameTextView.text = contact?.displayName(context) ?: senderSessionID
|
||||
binding.senderNameTextView.text = contact?.displayName(context) ?: senderSessionID
|
||||
} else {
|
||||
profilePictureContainer.visibility = View.GONE
|
||||
senderNameTextView.visibility = View.GONE
|
||||
binding.profilePictureContainer.visibility = View.GONE
|
||||
binding.senderNameTextView.visibility = View.GONE
|
||||
}
|
||||
// Date break
|
||||
dateBreakTextView.showDateBreak(message, previous)
|
||||
binding.dateBreakTextView.showDateBreak(message, previous)
|
||||
// Timestamp
|
||||
messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
|
||||
binding.messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
|
||||
// Margins
|
||||
val startPadding: Int
|
||||
if (isGroupThread) {
|
||||
startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() else 0
|
||||
val startPadding = if (isGroupThread) {
|
||||
if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() else 0
|
||||
} else {
|
||||
startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt()
|
||||
else resources.getDimension(R.dimen.medium_spacing).toInt()
|
||||
if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt()
|
||||
else 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()
|
||||
messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0)
|
||||
binding.messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0)
|
||||
// Set inter-message spacing
|
||||
setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
// Gravity
|
||||
val gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
||||
mainContainer.gravity = gravity or Gravity.BOTTOM
|
||||
binding.mainContainer.gravity = gravity or Gravity.BOTTOM
|
||||
// Message status indicator
|
||||
val (iconID, iconColor) = getMessageStatusImage(message)
|
||||
if (iconID != null) {
|
||||
@ -149,24 +166,24 @@ class VisibleMessageView : LinearLayout {
|
||||
if (iconColor != null) {
|
||||
drawable?.setTint(iconColor)
|
||||
}
|
||||
messageStatusImageView.setImageDrawable(drawable)
|
||||
binding.messageStatusImageView.setImageDrawable(drawable)
|
||||
}
|
||||
if (message.isOutgoing) {
|
||||
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
||||
messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
|
||||
binding.messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID
|
||||
} else {
|
||||
messageStatusImageView.isVisible = false
|
||||
binding.messageStatusImageView.isVisible = false
|
||||
}
|
||||
// Expiration timer
|
||||
updateExpirationTimer(message)
|
||||
// Calculate max message bubble width
|
||||
var maxWidth = screenWidth - startPadding - endPadding
|
||||
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
|
||||
if (binding.profilePictureContainer.visibility != View.GONE) { maxWidth -= binding.profilePictureContainer.width }
|
||||
// Populate content view
|
||||
messageContentView.indexInAdapter = indexInAdapter
|
||||
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false))
|
||||
messageContentView.delegate = contentViewDelegate
|
||||
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
|
||||
binding.messageContentView.indexInAdapter = indexInAdapter
|
||||
binding.messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false))
|
||||
binding.messageContentView.delegate = contentViewDelegate
|
||||
onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() }
|
||||
}
|
||||
|
||||
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||
@ -207,7 +224,7 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
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 ruleToRemove = if (message.isOutgoing) RelativeLayout.ALIGN_END else RelativeLayout.ALIGN_START
|
||||
expirationTimerViewLayoutParams.removeRule(ruleToRemove)
|
||||
@ -216,20 +233,20 @@ class VisibleMessageView : LinearLayout {
|
||||
val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt()
|
||||
expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0
|
||||
expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize)
|
||||
expirationTimerView.layoutParams = expirationTimerViewLayoutParams
|
||||
binding.expirationTimerView.layoutParams = expirationTimerViewLayoutParams
|
||||
if (message.expiresIn > 0 && !message.isPending) {
|
||||
expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
|
||||
expirationTimerView.isVisible = true
|
||||
expirationTimerView.setPercentComplete(0.0f)
|
||||
binding.expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme))
|
||||
binding.expirationTimerView.isVisible = true
|
||||
binding.expirationTimerView.setPercentComplete(0.0f)
|
||||
if (message.expireStarted > 0) {
|
||||
expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
||||
expirationTimerView.startAnimation()
|
||||
binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
||||
binding.expirationTimerView.startAnimation()
|
||||
if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) {
|
||||
ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule()
|
||||
}
|
||||
} else if (!message.isMediaPending) {
|
||||
expirationTimerView.setPercentComplete(0.0f)
|
||||
expirationTimerView.stopAnimation()
|
||||
binding.expirationTimerView.setPercentComplete(0.0f)
|
||||
binding.expirationTimerView.stopAnimation()
|
||||
ThreadUtils.queue {
|
||||
val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager
|
||||
val id = message.getId()
|
||||
@ -238,11 +255,11 @@ class VisibleMessageView : LinearLayout {
|
||||
expirationManager.scheduleDeletion(id, mms, message.expiresIn)
|
||||
}
|
||||
} else {
|
||||
expirationTimerView.stopAnimation()
|
||||
expirationTimerView.setPercentComplete(0.0f)
|
||||
binding.expirationTimerView.stopAnimation()
|
||||
binding.expirationTimerView.setPercentComplete(0.0f)
|
||||
}
|
||||
} else {
|
||||
expirationTimerView.isVisible = false
|
||||
binding.expirationTimerView.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,14 +272,14 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
if (translationX < 0 && !expirationTimerView.isVisible) {
|
||||
if (translationX < 0 && !binding.expirationTimerView.isVisible) {
|
||||
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
||||
val threshold = VisibleMessageView.swipeToReplyThreshold
|
||||
val threshold = swipeToReplyThreshold
|
||||
val iconSize = toPx(24, context.resources)
|
||||
val bottomVOffset = paddingBottom + messageStatusImageView.height + (messageContentView.height - iconSize) / 2
|
||||
swipeToReplyIconRect.left = messageContentContainer.right - messageContentContainer.paddingEnd + spacing
|
||||
val bottomVOffset = paddingBottom + binding.messageStatusImageView.height + (binding.messageContentView.height - iconSize) / 2
|
||||
swipeToReplyIconRect.left = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + spacing
|
||||
swipeToReplyIconRect.top = height - bottomVOffset - iconSize
|
||||
swipeToReplyIconRect.right = messageContentContainer.right - messageContentContainer.paddingEnd + iconSize + spacing
|
||||
swipeToReplyIconRect.right = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + iconSize + spacing
|
||||
swipeToReplyIconRect.bottom = height - bottomVOffset
|
||||
swipeToReplyIcon.bounds = swipeToReplyIconRect
|
||||
swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt()
|
||||
@ -274,8 +291,8 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
profilePictureView.recycle()
|
||||
messageContentView.recycle()
|
||||
binding.profilePictureView.recycle()
|
||||
binding.messageContentView.recycle()
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -296,13 +313,13 @@ class VisibleMessageView : LinearLayout {
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
val newLongPressCallback = Runnable { onLongPress() }
|
||||
this.longPressCallback = newLongPressCallback
|
||||
gestureHandler.postDelayed(newLongPressCallback, VisibleMessageView.longPressDurationThreshold)
|
||||
gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold)
|
||||
onDownTimestamp = Date().time
|
||||
}
|
||||
|
||||
private fun onMove(event: MotionEvent) {
|
||||
val translationX = toDp(event.rawX + dx, context.resources)
|
||||
if (abs(translationX) < VisibleMessageView.longPressMovementTreshold || snIsSelected) {
|
||||
if (abs(translationX) < longPressMovementThreshold || snIsSelected) {
|
||||
return
|
||||
} else {
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
@ -313,20 +330,16 @@ class VisibleMessageView : LinearLayout {
|
||||
val sign = -1.0f
|
||||
val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign
|
||||
this.translationX = x
|
||||
this.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving
|
||||
binding.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving
|
||||
postInvalidate() // Ensure onDraw(canvas:) is called
|
||||
if (abs(x) > VisibleMessageView.swipeToReplyThreshold && abs(previousTranslationX) < VisibleMessageView.swipeToReplyThreshold) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||
} else {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
}
|
||||
if (abs(x) > swipeToReplyThreshold && abs(previousTranslationX) < swipeToReplyThreshold) {
|
||||
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||
}
|
||||
previousTranslationX = x
|
||||
}
|
||||
|
||||
private fun onCancel(event: MotionEvent) {
|
||||
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) {
|
||||
if (abs(translationX) > swipeToReplyThreshold) {
|
||||
onSwipeToReply?.invoke()
|
||||
}
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
@ -334,9 +347,9 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
private fun onUp(event: MotionEvent) {
|
||||
if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) {
|
||||
if (abs(translationX) > swipeToReplyThreshold) {
|
||||
onSwipeToReply?.invoke()
|
||||
} else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) {
|
||||
} else if ((Date().time - onDownTimestamp) < longPressDurationThreshold) {
|
||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||
val pressCallback = this.pressCallback
|
||||
if (pressCallback != null) {
|
||||
@ -363,7 +376,7 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
.start()
|
||||
// Bit of a hack to keep the date break text view from moving
|
||||
dateBreakTextView.animate()
|
||||
binding.dateBreakTextView.animate()
|
||||
.translationX(0.0f)
|
||||
.setDuration(150)
|
||||
.start()
|
||||
@ -375,7 +388,7 @@ class VisibleMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
fun onContentClick(event: MotionEvent) {
|
||||
messageContentView.onContentClick?.invoke(event)
|
||||
binding.messageContentView.onContentClick?.invoke(event)
|
||||
}
|
||||
|
||||
private fun onPress(event: MotionEvent) {
|
||||
@ -393,5 +406,9 @@ class VisibleMessageView : LinearLayout {
|
||||
val activity = context as AppCompatActivity
|
||||
userDetailsBottomSheet.show(activity.supportFragmentManager, userDetailsBottomSheet.tag)
|
||||
}
|
||||
|
||||
fun playVoiceMessage() {
|
||||
binding.messageContentView.playVoiceMessage()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.view_voice_message.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVoiceMessageBinding
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
@ -26,6 +26,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
|
||||
@Inject lateinit var attachmentDb: AttachmentDatabase
|
||||
|
||||
private lateinit var binding: ViewVoiceMessageBinding
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var isPlaying = false
|
||||
set(value) {
|
||||
@ -44,8 +45,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_voice_message, this)
|
||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
binding = ViewVoiceMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(0),
|
||||
TimeUnit.MILLISECONDS.toSeconds(0))
|
||||
}
|
||||
@ -54,7 +55,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
// region Updating
|
||||
fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
|
||||
val audio = message.slideDeck.audioSlide!!
|
||||
voiceMessageViewLoader.isVisible = audio.isInProgress
|
||||
binding.voiceMessageViewLoader.isVisible = audio.isInProgress
|
||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||
cornerMask.setTopRightRadius(cornerRadii[1])
|
||||
@ -74,8 +75,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras ->
|
||||
if (audioExtras.durationMs > 0) {
|
||||
duration = audioExtras.durationMs
|
||||
voiceMessageViewDurationTextView.visibility = View.VISIBLE
|
||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
binding.voiceMessageViewDurationTextView.visibility = View.VISIBLE
|
||||
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
|
||||
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs))
|
||||
}
|
||||
@ -99,12 +100,12 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
|
||||
private fun handleProgressChanged(progress: Double) {
|
||||
this.progress = progress
|
||||
voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()),
|
||||
TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong()))
|
||||
val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams
|
||||
val layoutParams = binding.progressView.layoutParams as RelativeLayout.LayoutParams
|
||||
layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
|
||||
progressView.layoutParams = layoutParams
|
||||
binding.progressView.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
override fun onPlayerStop(player: AudioSlidePlayer) {
|
||||
@ -118,7 +119,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
|
||||
|
||||
private fun renderIcon() {
|
||||
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
|
||||
voiceMessagePlaybackImageView.setImageResource(iconID)
|
||||
binding.voiceMessagePlaybackImageView.setImageResource(iconID)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
@ -5,11 +5,12 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_search_bottom_bar.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewSearchBottomBarBinding
|
||||
|
||||
|
||||
class SearchBottomBar : LinearLayout {
|
||||
private lateinit var binding: ViewSearchBottomBarBinding
|
||||
private var eventListener: EventListener? = null
|
||||
|
||||
// region Lifecycle
|
||||
@ -18,10 +19,10 @@ class SearchBottomBar : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_search_bottom_bar, this)
|
||||
binding = ViewSearchBottomBarBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
|
||||
fun setData(position: Int, count: Int) {
|
||||
fun setData(position: Int, count: Int) = with(binding) {
|
||||
searchProgressWheel.visibility = GONE
|
||||
searchUp.setOnClickListener { v: View? ->
|
||||
if (eventListener != null) {
|
||||
@ -43,7 +44,7 @@ class SearchBottomBar : LinearLayout {
|
||||
}
|
||||
|
||||
fun showLoading() {
|
||||
searchProgressWheel.visibility = VISIBLE
|
||||
binding.searchProgressWheel.visibility = VISIBLE
|
||||
}
|
||||
|
||||
private fun setViewEnabled(view: View, enabled: Boolean) {
|
||||
|
@ -5,6 +5,7 @@ import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
@ -13,8 +14,8 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import kotlinx.android.synthetic.main.thumbnail_view.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ThumbnailViewBinding
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.utilities.Util.equals
|
||||
import org.session.libsignal.utilities.ListenableFuture
|
||||
@ -22,11 +23,13 @@ import org.session.libsignal.utilities.SettableFuture
|
||||
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
|
||||
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.*
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
|
||||
open class KThumbnailView: FrameLayout {
|
||||
|
||||
private lateinit var binding: ThumbnailViewBinding
|
||||
companion object {
|
||||
private const val WIDTH = 0
|
||||
private const val HEIGHT = 1
|
||||
@ -37,10 +40,10 @@ open class KThumbnailView: FrameLayout {
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
||||
|
||||
private val image by lazy { thumbnail_image }
|
||||
private val playOverlay by lazy { play_overlay }
|
||||
val loadIndicator: View by lazy { thumbnail_load_indicator }
|
||||
val downloadIndicator: View by lazy { thumbnail_download_icon }
|
||||
private val image by lazy { binding.thumbnailImage }
|
||||
private val playOverlay by lazy { binding.playOverlay }
|
||||
val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
|
||||
val downloadIndicator: View by lazy { binding.thumbnailDownloadIcon }
|
||||
|
||||
private val dimensDelegate = ThumbnailDimensDelegate()
|
||||
|
||||
@ -48,7 +51,7 @@ open class KThumbnailView: FrameLayout {
|
||||
private var radius: Int = 0
|
||||
|
||||
private fun initialize(attrs: AttributeSet?) {
|
||||
inflate(context, R.layout.thumbnail_view, this)
|
||||
binding = ThumbnailViewBinding.inflate(LayoutInflater.from(context), this)
|
||||
if (attrs != null) {
|
||||
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
|
||||
|
||||
|
@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.session.libsession.utilities.AppTextSecurePreferences
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import org.thoughtcrime.securesms.repository.DefaultConversationRepository
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class AppModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindTextSecurePreferences(preferences: AppTextSecurePreferences): TextSecurePreferences
|
||||
|
||||
@Binds
|
||||
abstract fun bindConversationRepository(repository: DefaultConversationRepository): ConversationRepository
|
||||
|
||||
}
|
@ -9,16 +9,18 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import kotlinx.android.synthetic.main.activity_create_private_chat.*
|
||||
import kotlinx.android.synthetic.main.fragment_enter_public_key.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityCreatePrivateChatBinding
|
||||
import network.loki.messenger.databinding.FragmentEnterPublicKeyBinding
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
@ -27,13 +29,13 @@ import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
|
||||
class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityCreatePrivateChatBinding
|
||||
private val adapter = CreatePrivateChatActivityAdapter(this)
|
||||
private var isKeyboardShowing = false
|
||||
set(value) {
|
||||
@ -47,37 +49,36 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityCreatePrivateChatBinding.inflate(layoutInflater)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_create_private_chat)
|
||||
setContentView(binding.root)
|
||||
// Set title
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_create_private_chat_title)
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
rootLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
val diff = rootLayout.rootView.height - rootLayout.height
|
||||
val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics
|
||||
val estimatedKeyboardHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics)
|
||||
this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight)
|
||||
}
|
||||
})
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
binding.rootLayout.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val diff = binding.rootLayout.rootView.height - binding.rootLayout.height
|
||||
val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics
|
||||
val estimatedKeyboardHeight =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics)
|
||||
this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun showLoader() {
|
||||
loader.visibility = View.VISIBLE
|
||||
loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
binding.loader.visibility = View.VISIBLE
|
||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
private fun hideLoader() {
|
||||
loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
loader.visibility = View.GONE
|
||||
binding.loader.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -156,6 +157,8 @@ private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatAc
|
||||
|
||||
// region Enter Public Key Fragment
|
||||
class EnterPublicKeyFragment : Fragment() {
|
||||
private lateinit var binding: FragmentEnterPublicKeyBinding
|
||||
|
||||
var isKeyboardShowing = false
|
||||
set(value) { field = value; handleIsKeyboardShowingChanged() }
|
||||
|
||||
@ -165,32 +168,34 @@ class EnterPublicKeyFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_enter_public_key, container, false)
|
||||
binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
createPrivateChatIfPossible()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
with(binding) {
|
||||
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
createPrivateChatIfPossible()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
shareButton.setOnClickListener { sharePublicKey() }
|
||||
createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() }
|
||||
}
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
shareButton.setOnClickListener { sharePublicKey() }
|
||||
createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() }
|
||||
}
|
||||
|
||||
private fun handleIsKeyboardShowingChanged() {
|
||||
val optionalContentContainer = optionalContentContainer ?: return
|
||||
optionalContentContainer.isVisible = !isKeyboardShowing
|
||||
binding.optionalContentContainer.isVisible = !isKeyboardShowing
|
||||
}
|
||||
|
||||
private fun copyPublicKey() {
|
||||
@ -209,7 +214,7 @@ class EnterPublicKeyFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun createPrivateChatIfPossible() {
|
||||
val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString()
|
||||
val hexEncodedPublicKey = binding.publicKeyEditText.text?.trim().toString()
|
||||
val activity = requireActivity() as CreatePrivateChatActivity
|
||||
activity.createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
|
@ -1,22 +1,23 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.fragment_closed_group_edit_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import network.loki.messenger.databinding.FragmentClosedGroupEditBottomSheetBinding
|
||||
|
||||
public class ClosedGroupEditingOptionsBottomSheet : BottomSheetDialogFragment() {
|
||||
class ClosedGroupEditingOptionsBottomSheet : BottomSheetDialogFragment() {
|
||||
private lateinit var binding: FragmentClosedGroupEditBottomSheetBinding
|
||||
var onRemoveTapped: (() -> Unit)? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_closed_group_edit_bottom_sheet, container, false)
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentClosedGroupEditBottomSheetBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
removeFromGroup.setOnClickListener { onRemoveTapped?.invoke() }
|
||||
binding.removeFromGroup.setOnClickListener { onRemoveTapped?.invoke() }
|
||||
}
|
||||
}
|
@ -10,8 +10,8 @@ import android.widget.Toast
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.activity_create_closed_group.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityCreateClosedGroupBinding
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
@ -28,8 +28,8 @@ import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.fadeIn
|
||||
import org.thoughtcrime.securesms.util.fadeOut
|
||||
|
||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
||||
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||
private lateinit var binding: ActivityCreateClosedGroupBinding
|
||||
private var isLoading = false
|
||||
set(newValue) { field = newValue; invalidateOptionsMenu() }
|
||||
private var members = listOf<String>()
|
||||
@ -50,11 +50,12 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
setContentView(R.layout.activity_create_closed_group)
|
||||
binding = ActivityCreateClosedGroupBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_create_closed_group_title)
|
||||
recyclerView.adapter = this.selectContactsAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
binding.recyclerView.adapter = this.selectContactsAdapter
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||
}
|
||||
|
||||
@ -80,8 +81,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
private fun update(members: List<String>) {
|
||||
//if there is a Note to self conversation, it loads self in the list, so we need to remove it here
|
||||
this.members = members.minus(publicKey)
|
||||
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
// endregion
|
||||
@ -95,12 +96,12 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
}
|
||||
|
||||
private fun createNewPrivateChat() {
|
||||
setResult(Companion.closedGroupCreatedResultCode)
|
||||
setResult(closedGroupCreatedResultCode)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun createClosedGroup() {
|
||||
val name = nameEditText.text.trim()
|
||||
val name = binding.nameEditText.text.trim()
|
||||
if (name.isEmpty()) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@ -116,9 +117,9 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
}
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
|
||||
isLoading = true
|
||||
loaderContainer.fadeIn()
|
||||
binding.loaderContainer.fadeIn()
|
||||
MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
loaderContainer.fadeOut()
|
||||
binding.loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
val threadID = DatabaseComponent.get(this).threadDatabase().getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
if (!isFinishing) {
|
||||
@ -126,7 +127,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
finish()
|
||||
}
|
||||
}.failUi {
|
||||
loaderContainer.fadeOut()
|
||||
binding.loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
@ -8,12 +8,14 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.activity_settings.*
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.task
|
||||
|
@ -13,62 +13,63 @@ import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.*
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.android.synthetic.main.activity_join_public_chat.*
|
||||
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
|
||||
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.groups.DefaultGroupsViewModel
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.State
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityJoinPublicChatBinding
|
||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityJoinPublicChatBinding.inflate(layoutInflater)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_join_public_chat)
|
||||
setContentView(binding.root)
|
||||
// Set title
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_join_public_chat_title)
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun showLoader() {
|
||||
loader.visibility = View.VISIBLE
|
||||
loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
binding.loader.visibility = View.VISIBLE
|
||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
private fun hideLoader() {
|
||||
loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
loader.visibility = View.GONE
|
||||
binding.loader.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -166,26 +167,28 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
|
||||
|
||||
// region Enter Chat URL Fragment
|
||||
class EnterChatURLFragment : Fragment() {
|
||||
private lateinit var binding: FragmentEnterChatUrlBinding
|
||||
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
|
||||
binding = FragmentEnterChatUrlBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||
binding.chatURLEditText.imeOptions = binding.chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
binding.joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
||||
defaultRoomsContainer.isVisible = state is State.Success
|
||||
defaultRoomsLoaderContainer.isVisible = state is State.Loading
|
||||
defaultRoomsLoader.isVisible = state is State.Loading
|
||||
binding.defaultRoomsContainer.isVisible = state is State.Success
|
||||
binding.defaultRoomsLoaderContainer.isVisible = state is State.Loading
|
||||
binding.defaultRoomsLoader.isVisible = state is State.Loading
|
||||
when (state) {
|
||||
State.Loading -> {
|
||||
// TODO: Show a loader
|
||||
// TODO: Show a binding.loader
|
||||
}
|
||||
is State.Error -> {
|
||||
// TODO: Hide the loader
|
||||
// TODO: Hide the binding.loader
|
||||
}
|
||||
is State.Success -> {
|
||||
populateDefaultGroups(state.value)
|
||||
@ -195,10 +198,10 @@ class EnterChatURLFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
|
||||
defaultRoomsGridLayout.removeAllViews()
|
||||
defaultRoomsGridLayout.useDefaultMargins = false
|
||||
binding.defaultRoomsGridLayout.removeAllViews()
|
||||
binding.defaultRoomsGridLayout.useDefaultMargins = false
|
||||
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 bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size)
|
||||
RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
|
||||
@ -210,18 +213,18 @@ class EnterChatURLFragment : Fragment() {
|
||||
chip.setOnClickListener {
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
|
||||
}
|
||||
defaultRoomsGridLayout.addView(chip)
|
||||
binding.defaultRoomsGridLayout.addView(chip)
|
||||
}
|
||||
if ((groups.size and 1) != 0) { // This checks that the number of rooms is even
|
||||
layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout)
|
||||
layoutInflater.inflate(R.layout.grid_layout_filler, binding.defaultRoomsGridLayout)
|
||||
}
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
private fun joinPublicChatIfPossible() {
|
||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
|
||||
val chatURL = chatURLEditText.text.trim().toString().toLowerCase(Locale.US)
|
||||
inputMethodManager.hideSoftInputFromWindow(binding.chatURLEditText.windowToken, 0)
|
||||
val chatURL = binding.chatURLEditText.text.trim().toString().toLowerCase(Locale.US)
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
||||
}
|
||||
// endregion
|
||||
|
@ -1,16 +1,16 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.os.Bundle
|
||||
import kotlinx.android.synthetic.main.activity_open_group_guidelines.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityOpenGroupGuidelinesBinding
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
|
||||
class OpenGroupGuidelinesActivity : BaseActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_open_group_guidelines)
|
||||
communityGuidelinesTextView.text = """
|
||||
val binding = ActivityOpenGroupGuidelinesBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.communityGuidelinesTextView.text = """
|
||||
Welcome to Oxen.
|
||||
|
||||
Oxen believes privacy is an important part of our future. People have been safeguarding the right to privacy since the dawn of humanity, but the digital world has turned privacy into a privilege. Enough is enough. We're taking it back. For you. For us. For everyone.
|
||||
|
@ -6,13 +6,12 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
|
||||
public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
|
||||
|
||||
class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
|
||||
private lateinit var binding: FragmentConversationBottomSheetBinding
|
||||
//FIXME AC: Supplying a threadRecord directly into the field from an activity
|
||||
// is not the best idea. It doesn't survive configuration change.
|
||||
// We should be dealing with IDs and all sorts of serializable data instead
|
||||
@ -29,20 +28,21 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.
|
||||
var onSetMuteTapped: ((Boolean) -> Unit)? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_conversation_bottom_sheet, container, false)
|
||||
binding = FragmentConversationBottomSheetBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
when (v) {
|
||||
detailsTextView -> onViewDetailsTapped?.invoke()
|
||||
pinTextView -> onPinTapped?.invoke()
|
||||
unpinTextView -> onUnpinTapped?.invoke()
|
||||
blockTextView -> onBlockTapped?.invoke()
|
||||
unblockTextView -> onUnblockTapped?.invoke()
|
||||
deleteTextView -> onDeleteTapped?.invoke()
|
||||
notificationsTextView -> onNotificationTapped?.invoke()
|
||||
unMuteNotificationsTextView -> onSetMuteTapped?.invoke(false)
|
||||
muteNotificationsTextView -> onSetMuteTapped?.invoke(true)
|
||||
binding.detailsTextView -> onViewDetailsTapped?.invoke()
|
||||
binding.pinTextView -> onPinTapped?.invoke()
|
||||
binding.unpinTextView -> onUnpinTapped?.invoke()
|
||||
binding.blockTextView -> onBlockTapped?.invoke()
|
||||
binding.unblockTextView -> onUnblockTapped?.invoke()
|
||||
binding.deleteTextView -> onDeleteTapped?.invoke()
|
||||
binding.notificationsTextView -> onNotificationTapped?.invoke()
|
||||
binding.unMuteNotificationsTextView -> onSetMuteTapped?.invoke(false)
|
||||
binding.muteNotificationsTextView -> onSetMuteTapped?.invoke(true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,26 +51,26 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.
|
||||
if (!this::thread.isInitialized) { return dismiss() }
|
||||
val recipient = thread.recipient
|
||||
if (!recipient.isGroupRecipient && !recipient.isLocalNumber) {
|
||||
detailsTextView.visibility = View.VISIBLE
|
||||
unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE
|
||||
blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE
|
||||
detailsTextView.setOnClickListener(this)
|
||||
blockTextView.setOnClickListener(this)
|
||||
unblockTextView.setOnClickListener(this)
|
||||
binding.detailsTextView.visibility = View.VISIBLE
|
||||
binding.unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE
|
||||
binding.blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE
|
||||
binding.detailsTextView.setOnClickListener(this)
|
||||
binding.blockTextView.setOnClickListener(this)
|
||||
binding.unblockTextView.setOnClickListener(this)
|
||||
} else {
|
||||
detailsTextView.visibility = View.GONE
|
||||
binding.detailsTextView.visibility = View.GONE
|
||||
}
|
||||
unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
||||
muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
||||
unMuteNotificationsTextView.setOnClickListener(this)
|
||||
muteNotificationsTextView.setOnClickListener(this)
|
||||
notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
|
||||
notificationsTextView.setOnClickListener(this)
|
||||
deleteTextView.setOnClickListener(this)
|
||||
pinTextView.isVisible = !thread.isPinned
|
||||
unpinTextView.isVisible = thread.isPinned
|
||||
pinTextView.setOnClickListener(this)
|
||||
unpinTextView.setOnClickListener(this)
|
||||
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
||||
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
||||
binding.unMuteNotificationsTextView.setOnClickListener(this)
|
||||
binding.muteNotificationsTextView.setOnClickListener(this)
|
||||
binding.notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
|
||||
binding.notificationsTextView.setOnClickListener(this)
|
||||
binding.deleteTextView.setOnClickListener(this)
|
||||
binding.pinTextView.isVisible = !thread.isPinned
|
||||
binding.unpinTextView.isVisible = thread.isPinned
|
||||
binding.pinTextView.setOnClickListener(this)
|
||||
binding.unpinTextView.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -11,8 +11,8 @@ import android.widget.LinearLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.view_conversation.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewConversationBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
|
||||
class ConversationView : LinearLayout {
|
||||
private lateinit var binding: ViewConversationBinding
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
var thread: ThreadRecord? = null
|
||||
|
||||
@ -31,7 +32,7 @@ class ConversationView : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
private fun initialize() {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_conversation, this)
|
||||
binding = ViewConversationBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
// endregion
|
||||
@ -39,83 +40,83 @@ class ConversationView : LinearLayout {
|
||||
// region Updating
|
||||
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
|
||||
this.thread = thread
|
||||
if (thread.isPinned) {
|
||||
conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0)
|
||||
background = ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background)
|
||||
background = if (thread.isPinned) {
|
||||
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0)
|
||||
ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background)
|
||||
} else {
|
||||
conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
background = ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
||||
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
||||
}
|
||||
profilePictureView.glide = glide
|
||||
binding.profilePictureView.glide = glide
|
||||
val unreadCount = thread.unreadCount
|
||||
if (thread.recipient.isBlocked) {
|
||||
accentView.setBackgroundResource(R.color.destructive)
|
||||
accentView.visibility = View.VISIBLE
|
||||
binding.accentView.setBackgroundResource(R.color.destructive)
|
||||
binding.accentView.visibility = View.VISIBLE
|
||||
} else {
|
||||
accentView.setBackgroundResource(R.color.accent)
|
||||
binding.accentView.setBackgroundResource(R.color.accent)
|
||||
// Using thread.isRead we can determine if the last message was our own, and display it as 'read' even though previous messages may not be
|
||||
// This would also not trigger the disappearing message timer which may or may not be desirable
|
||||
accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE
|
||||
binding.accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
val formattedUnreadCount = if (thread.isRead) {
|
||||
null
|
||||
} else {
|
||||
if (unreadCount < 100) unreadCount.toString() else "99+"
|
||||
}
|
||||
unreadCountTextView.text = formattedUnreadCount
|
||||
binding.unreadCountTextView.text = formattedUnreadCount
|
||||
val textSize = if (unreadCount < 100) 12.0f else 9.0f
|
||||
unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
||||
unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL)
|
||||
unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead)
|
||||
binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
||||
binding.unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL)
|
||||
binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead)
|
||||
val senderDisplayName = getUserDisplayName(thread.recipient)
|
||||
?: thread.recipient.address.toString()
|
||||
conversationViewDisplayNameTextView.text = senderDisplayName
|
||||
timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||
binding.conversationViewDisplayNameTextView.text = senderDisplayName
|
||||
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||
val recipient = thread.recipient
|
||||
muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL
|
||||
binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL
|
||||
val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
|
||||
R.drawable.ic_outline_notifications_off_24
|
||||
} else {
|
||||
R.drawable.ic_notifications_mentions
|
||||
}
|
||||
muteIndicatorImageView.setImageResource(drawableRes)
|
||||
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
||||
val rawSnippet = thread.getDisplayBody(context)
|
||||
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
|
||||
snippetTextView.text = snippet
|
||||
snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||
binding.snippetTextView.text = snippet
|
||||
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||
if (isTyping) {
|
||||
typingIndicatorView.startAnimation()
|
||||
binding.typingIndicatorView.startAnimation()
|
||||
} else {
|
||||
typingIndicatorView.stopAnimation()
|
||||
binding.typingIndicatorView.stopAnimation()
|
||||
}
|
||||
typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE
|
||||
statusIndicatorImageView.visibility = View.VISIBLE
|
||||
binding.typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE
|
||||
binding.statusIndicatorImageView.visibility = View.VISIBLE
|
||||
when {
|
||||
!thread.isOutgoing -> statusIndicatorImageView.visibility = View.GONE
|
||||
!thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE
|
||||
thread.isFailed -> {
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_error)?.mutate()
|
||||
drawable?.setTint(ContextCompat.getColor(context, R.color.destructive))
|
||||
statusIndicatorImageView.setImageDrawable(drawable)
|
||||
binding.statusIndicatorImageView.setImageDrawable(drawable)
|
||||
}
|
||||
thread.isPending -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot)
|
||||
thread.isRead -> statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||
else -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
||||
thread.isPending -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot)
|
||||
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
||||
}
|
||||
post {
|
||||
profilePictureView.update(thread.recipient, thread.threadId)
|
||||
binding.profilePictureView.update(thread.recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
profilePictureView.recycle()
|
||||
binding.profilePictureView.recycle()
|
||||
}
|
||||
|
||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
||||
if (recipient.isLocalNumber) {
|
||||
return context.getString(R.string.note_to_self)
|
||||
return if (recipient.isLocalNumber) {
|
||||
context.getString(R.string.note_to_self)
|
||||
} else {
|
||||
return recipient.name // Internally uses the Contact API
|
||||
recipient.name // Internally uses the Contact API
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
@ -10,7 +10,6 @@ import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
@ -19,24 +18,22 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import kotlinx.android.synthetic.main.seed_reminder_stub.*
|
||||
import kotlinx.android.synthetic.main.seed_reminder_stub.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.utilities.*
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.ProfilePictureModifiedEvent
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
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.SeedReminderViewDelegate
|
||||
import org.thoughtcrime.securesms.preferences.SettingsActivity
|
||||
import org.thoughtcrime.securesms.util.*
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.IP2Country
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.show
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener,
|
||||
SeedReminderViewDelegate, NewConversationButtonSetViewDelegate, LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private lateinit var binding: ActivityHomeBinding
|
||||
private lateinit var glide: GlideRequests
|
||||
private var broadcastReceiver: BroadcastReceiver? = null
|
||||
|
||||
@ -75,57 +79,57 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
private val publicKey: String
|
||||
get() = TextSecurePreferences.getLocalNumber(this)!!
|
||||
|
||||
private val homeAdapter:HomeAdapter by lazy {
|
||||
HomeAdapter(this, threadDb.conversationList)
|
||||
private val homeAdapter: HomeAdapter by lazy {
|
||||
HomeAdapter(context = this, cursor = threadDb.conversationList, listener = this)
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_home)
|
||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
// Set custom toolbar
|
||||
setSupportActionBar(toolbar)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
// Set up Glide
|
||||
glide = GlideApp.with(this)
|
||||
// Set up toolbar buttons
|
||||
profileButton.glide = glide
|
||||
profileButton.setOnClickListener { openSettings() }
|
||||
pathStatusViewContainer.disableClipping()
|
||||
pathStatusViewContainer.setOnClickListener { showPath() }
|
||||
binding.profileButton.glide = glide
|
||||
binding.profileButton.setOnClickListener { openSettings() }
|
||||
binding.pathStatusViewContainer.disableClipping()
|
||||
binding.pathStatusViewContainer.setOnClickListener { showPath() }
|
||||
// Set up seed reminder view
|
||||
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
||||
if (!hasViewedSeed) {
|
||||
seedReminderStub.inflate().apply {
|
||||
val seedReminderView = this.seedReminderView
|
||||
binding.seedReminderStub.setOnInflateListener { _, inflated ->
|
||||
val stubBinding = SeedReminderStubBinding.bind(inflated)
|
||||
val seedReminderViewTitle = SpannableString("You're almost finished! 80%") // Intentionally not yet translated
|
||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
|
||||
seedReminderView.setProgress(80, false)
|
||||
seedReminderView.delegate = this@HomeActivity
|
||||
stubBinding.seedReminderView.title = seedReminderViewTitle
|
||||
stubBinding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
|
||||
stubBinding.seedReminderView.setProgress(80, false)
|
||||
stubBinding.seedReminderView.delegate = this@HomeActivity
|
||||
}
|
||||
binding.seedReminderStub.inflate()
|
||||
} else {
|
||||
seedReminderStub.isVisible = false
|
||||
binding.seedReminderStub.isVisible = false
|
||||
}
|
||||
// Set up recycler view
|
||||
homeAdapter.setHasStableIds(true)
|
||||
homeAdapter.glide = glide
|
||||
homeAdapter.conversationClickListener = this
|
||||
recyclerView.adapter = homeAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = homeAdapter
|
||||
// Set up empty state view
|
||||
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
// Set up new conversation button set
|
||||
newConversationButtonSet.delegate = this
|
||||
binding.newConversationButtonSet.delegate = this
|
||||
// Observe blocked contacts changed events
|
||||
val broadcastReceiver = object : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
this.broadcastReceiver = broadcastReceiver
|
||||
@ -138,7 +142,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
// Set up typing observer
|
||||
withContext(Dispatchers.Main) {
|
||||
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this@HomeActivity, Observer<Set<Long>> { threadIDs ->
|
||||
val adapter = recyclerView.adapter as HomeAdapter
|
||||
val adapter = binding.recyclerView.adapter as HomeAdapter
|
||||
adapter.typingThreadIDs = threadIDs ?: setOf()
|
||||
})
|
||||
updateProfileButton()
|
||||
@ -177,11 +181,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
|
||||
if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared
|
||||
IdentityKeyUtil.checkUpdate(this)
|
||||
profileButton.recycle() // clear cached image before update tje profilePictureView
|
||||
profileButton.update()
|
||||
binding.profileButton.recycle() // clear cached image before update tje profilePictureView
|
||||
binding.profileButton.update()
|
||||
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
||||
if (hasViewedSeed) {
|
||||
seedReminderView?.isVisible = false
|
||||
binding.seedReminderStub.isVisible = false
|
||||
}
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(this)) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
@ -214,8 +218,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
|
||||
// region Updating
|
||||
private fun updateEmptyState() {
|
||||
val threadCount = (recyclerView.adapter as HomeAdapter).itemCount
|
||||
emptyStateContainer.visibility = if (threadCount == 0) View.VISIBLE else View.GONE
|
||||
val threadCount = (binding.recyclerView.adapter as HomeAdapter).itemCount
|
||||
binding.emptyStateContainer.isVisible = threadCount == 0
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@ -226,10 +230,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
}
|
||||
|
||||
private fun updateProfileButton() {
|
||||
profileButton.publicKey = publicKey
|
||||
profileButton.displayName = TextSecurePreferences.getProfileName(this)
|
||||
profileButton.recycle()
|
||||
profileButton.update()
|
||||
binding.profileButton.publicKey = publicKey
|
||||
binding.profileButton.displayName = TextSecurePreferences.getProfileName(this)
|
||||
binding.profileButton.recycle()
|
||||
binding.profileButton.update()
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -239,13 +243,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
show(intent)
|
||||
}
|
||||
|
||||
override fun onConversationClick(view: ConversationView) {
|
||||
val thread = view.thread ?: return
|
||||
openConversation(thread)
|
||||
override fun onConversationClick(thread: ThreadRecord) {
|
||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
override fun onLongConversationClick(view: ConversationView) {
|
||||
val thread = view.thread ?: return
|
||||
override fun onLongConversationClick(thread: ThreadRecord) {
|
||||
val bottomSheet = ConversationOptionsBottomSheet()
|
||||
bottomSheet.thread = thread
|
||||
bottomSheet.onViewDetailsTapped = {
|
||||
@ -286,15 +290,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
}
|
||||
bottomSheet.onPinTapped = {
|
||||
bottomSheet.dismiss()
|
||||
if (!thread.isPinned) {
|
||||
pinConversation(thread)
|
||||
}
|
||||
setConversationPinned(thread.threadId, true)
|
||||
}
|
||||
bottomSheet.onUnpinTapped = {
|
||||
bottomSheet.dismiss()
|
||||
if (thread.isPinned) {
|
||||
unpinConversation(thread)
|
||||
}
|
||||
setConversationPinned(thread.threadId, false)
|
||||
}
|
||||
bottomSheet.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)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setBlocked(thread.recipient, true)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
@ -321,10 +321,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
.setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setBlocked(thread.recipient, false)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
@ -333,18 +333,18 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
|
||||
private fun setConversationMuted(thread: ThreadRecord, isMuted: Boolean) {
|
||||
if (!isMuted) {
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setMuted(thread.recipient, 0)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MuteDialog.show(this) { until: Long ->
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setMuted(thread.recipient, until)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -352,28 +352,19 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
}
|
||||
|
||||
private fun setNotifyType(thread: ThreadRecord, newNotifyType: Int) {
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
recipientDatabase.setNotifyType(thread.recipient, newNotifyType)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pinConversation(thread: ThreadRecord) {
|
||||
ThreadUtils.queue {
|
||||
threadDb.setPinned(thread.threadId, true)
|
||||
Util.runOnMain {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun unpinConversation(thread: ThreadRecord) {
|
||||
ThreadUtils.queue {
|
||||
threadDb.setPinned(thread.threadId, false)
|
||||
Util.runOnMain {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
threadDb.setPinned(threadId, pinned)
|
||||
withContext(Dispatchers.Main) {
|
||||
LoaderManager.getInstance(this@HomeActivity).restartLoader(0, null, this@HomeActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -381,16 +372,15 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
private fun deleteConversation(thread: ThreadRecord) {
|
||||
val threadID = thread.threadId
|
||||
val recipient = thread.recipient
|
||||
val message: String
|
||||
if (recipient.isGroupRecipient) {
|
||||
val message = if (recipient.isGroupRecipient) {
|
||||
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
|
||||
if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) {
|
||||
message = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
||||
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
||||
} else {
|
||||
message = resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||
resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||
}
|
||||
} else {
|
||||
message = resources.getString(R.string.activity_home_delete_conversation_dialog_message)
|
||||
resources.getString(R.string.activity_home_delete_conversation_dialog_message)
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
dialog.setMessage(message)
|
||||
@ -419,7 +409,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
if (v2OpenGroup != null) {
|
||||
OpenGroupManager.delete(v2OpenGroup.server, v2OpenGroup.room, this@HomeActivity)
|
||||
} else {
|
||||
ThreadUtils.queue {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
threadDb.deleteConversation(threadID)
|
||||
}
|
||||
}
|
||||
@ -436,12 +426,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
|
||||
dialog.create().show()
|
||||
}
|
||||
|
||||
private fun openConversation(thread: ThreadRecord) {
|
||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun openSettings() {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
show(intent, isForResult = true)
|
||||
|
@ -9,20 +9,23 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class HomeAdapter(context: Context, cursor: Cursor?) : CursorRecyclerViewAdapter<HomeAdapter.ViewHolder>(context, cursor) {
|
||||
class HomeAdapter(
|
||||
context: Context,
|
||||
cursor: Cursor?,
|
||||
val listener: ConversationClickListener
|
||||
) : CursorRecyclerViewAdapter<HomeAdapter.ViewHolder>(context, cursor) {
|
||||
private val threadDatabase = DatabaseComponent.get(context).threadDatabase()
|
||||
lateinit var glide: GlideRequests
|
||||
var typingThreadIDs = setOf<Long>()
|
||||
set(value) { field = value; notifyDataSetChanged() }
|
||||
var conversationClickListener: ConversationClickListener? = null
|
||||
|
||||
class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = ConversationView(context)
|
||||
view.setOnClickListener { conversationClickListener?.onConversationClick(view) }
|
||||
view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } }
|
||||
view.setOnLongClickListener {
|
||||
conversationClickListener?.onLongConversationClick(view)
|
||||
view.thread?.let { listener.onLongConversationClick(it) }
|
||||
true
|
||||
}
|
||||
return ViewHolder(view)
|
||||
@ -45,6 +48,6 @@ class HomeAdapter(context: Context, cursor: Cursor?) : CursorRecyclerViewAdapter
|
||||
}
|
||||
|
||||
interface ConversationClickListener {
|
||||
fun onConversationClick(view: ConversationView)
|
||||
fun onLongConversationClick(view: ConversationView)
|
||||
fun onConversationClick(thread: ThreadRecord)
|
||||
fun onLongConversationClick(thread: ThreadRecord)
|
||||
}
|
@ -17,26 +17,33 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.android.synthetic.main.activity_path.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityPathBinding
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.utilities.Snode
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.util.*
|
||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.util.IP2Country
|
||||
import org.thoughtcrime.securesms.util.PathDotView
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.util.animateSizeChange
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.fadeIn
|
||||
import org.thoughtcrime.securesms.util.fadeOut
|
||||
import org.thoughtcrime.securesms.util.getColorWithID
|
||||
|
||||
class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
private lateinit var binding: ActivityPathBinding
|
||||
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
setContentView(R.layout.activity_path)
|
||||
binding = ActivityPathBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_path_title)
|
||||
pathRowsContainer.disableClipping()
|
||||
learnMoreButton.setOnClickListener { learnMore() }
|
||||
binding.pathRowsContainer.disableClipping()
|
||||
binding.learnMoreButton.setOnClickListener { learnMore() }
|
||||
update(false)
|
||||
registerObservers()
|
||||
}
|
||||
@ -82,7 +89,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
private fun handleOnionRequestPathCountriesLoaded() { update(false) }
|
||||
|
||||
private fun update(isAnimated: Boolean) {
|
||||
pathRowsContainer.removeAllViews()
|
||||
binding.pathRowsContainer.removeAllViews()
|
||||
if (OnionRequestAPI.paths.isNotEmpty()) {
|
||||
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
|
||||
val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000
|
||||
@ -94,18 +101,18 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
val destinationRow = getPathRow(resources.getString(R.string.activity_path_destination_row_title), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
|
||||
val rows = listOf( youRow ) + pathRows + listOf( destinationRow )
|
||||
for (row in rows) {
|
||||
pathRowsContainer.addView(row)
|
||||
binding.pathRowsContainer.addView(row)
|
||||
}
|
||||
if (isAnimated) {
|
||||
spinner.fadeOut()
|
||||
binding.spinner.fadeOut()
|
||||
} else {
|
||||
spinner.alpha = 0.0f
|
||||
binding.spinner.alpha = 0.0f
|
||||
}
|
||||
} else {
|
||||
if (isAnimated) {
|
||||
spinner.fadeIn()
|
||||
binding.spinner.fadeIn()
|
||||
} else {
|
||||
spinner.alpha = 1.0f
|
||||
binding.spinner.alpha = 1.0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,10 +14,9 @@ import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.android.synthetic.main.fragment_user_details_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentUserDetailsBottomSheetBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.Address
|
||||
@ -34,13 +33,15 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
@Inject lateinit var threadDb: ThreadDatabase
|
||||
|
||||
private lateinit var binding: FragmentUserDetailsBottomSheetBinding
|
||||
companion object {
|
||||
const val ARGUMENT_PUBLIC_KEY = "publicKey"
|
||||
const val ARGUMENT_THREAD_ID = "threadId"
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_user_details_bottom_sheet, container, false)
|
||||
binding = FragmentUserDetailsBottomSheetBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@ -49,58 +50,62 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
||||
val threadID = arguments?.getLong(ARGUMENT_THREAD_ID) ?: return dismiss()
|
||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
|
||||
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
||||
profilePictureView.publicKey = publicKey
|
||||
profilePictureView.glide = GlideApp.with(this)
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update(recipient, -1)
|
||||
nameTextViewContainer.visibility = View.VISIBLE
|
||||
nameTextViewContainer.setOnClickListener {
|
||||
nameTextViewContainer.visibility = View.INVISIBLE
|
||||
nameEditTextContainer.visibility = View.VISIBLE
|
||||
nicknameEditText.text = null
|
||||
nicknameEditText.requestFocus()
|
||||
showSoftKeyboard()
|
||||
}
|
||||
cancelNicknameEditingButton.setOnClickListener {
|
||||
nicknameEditText.clearFocus()
|
||||
hideSoftKeyboard()
|
||||
with(binding) {
|
||||
profilePictureView.publicKey = publicKey
|
||||
profilePictureView.glide = GlideApp.with(this@UserDetailsBottomSheet)
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update(recipient)
|
||||
nameTextViewContainer.visibility = View.VISIBLE
|
||||
nameEditTextContainer.visibility = View.INVISIBLE
|
||||
}
|
||||
saveNicknameButton.setOnClickListener {
|
||||
saveNickName(recipient)
|
||||
}
|
||||
nicknameEditText.setOnEditorActionListener { _, actionId, _ ->
|
||||
when (actionId) {
|
||||
EditorInfo.IME_ACTION_DONE -> {
|
||||
saveNickName(recipient)
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
else -> return@setOnEditorActionListener false
|
||||
nameTextViewContainer.setOnClickListener {
|
||||
nameTextViewContainer.visibility = View.INVISIBLE
|
||||
nameEditTextContainer.visibility = View.VISIBLE
|
||||
nicknameEditText.text = null
|
||||
nicknameEditText.requestFocus()
|
||||
showSoftKeyboard()
|
||||
}
|
||||
}
|
||||
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
|
||||
cancelNicknameEditingButton.setOnClickListener {
|
||||
nicknameEditText.clearFocus()
|
||||
hideSoftKeyboard()
|
||||
nameTextViewContainer.visibility = View.VISIBLE
|
||||
nameEditTextContainer.visibility = View.INVISIBLE
|
||||
}
|
||||
saveNicknameButton.setOnClickListener {
|
||||
saveNickName(recipient)
|
||||
}
|
||||
nicknameEditText.setOnEditorActionListener { _, actionId, _ ->
|
||||
when (actionId) {
|
||||
EditorInfo.IME_ACTION_DONE -> {
|
||||
saveNickName(recipient)
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
else -> return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
|
||||
|
||||
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
|
||||
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient
|
||||
publicKeyTextView.text = publicKey
|
||||
publicKeyTextView.setOnLongClickListener {
|
||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", publicKey)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
messageButton.setOnClickListener {
|
||||
val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient)
|
||||
val intent = Intent(
|
||||
context,
|
||||
ConversationActivityV2::class.java
|
||||
)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1)
|
||||
startActivity(intent)
|
||||
dismiss()
|
||||
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
|
||||
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient
|
||||
publicKeyTextView.text = publicKey
|
||||
publicKeyTextView.setOnLongClickListener {
|
||||
val clipboard =
|
||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", publicKey)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
true
|
||||
}
|
||||
messageButton.setOnClickListener {
|
||||
val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient)
|
||||
val intent = Intent(
|
||||
context,
|
||||
ConversationActivityV2::class.java
|
||||
)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1)
|
||||
startActivity(intent)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,7 +116,7 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
||||
window.setDimAmount(if (isLightMode) 0.1f else 0.75f)
|
||||
}
|
||||
|
||||
fun saveNickName(recipient: Recipient) {
|
||||
fun saveNickName(recipient: Recipient) = with(binding) {
|
||||
nicknameEditText.clearFocus()
|
||||
hideSoftKeyboard()
|
||||
nameTextViewContainer.visibility = View.VISIBLE
|
||||
@ -131,11 +136,11 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
||||
@SuppressLint("ServiceCast")
|
||||
fun showSoftKeyboard() {
|
||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(nicknameEditText, 0)
|
||||
imm?.showSoftInput(binding.nicknameEditText, 0)
|
||||
}
|
||||
|
||||
fun hideSoftKeyboard() {
|
||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.hideSoftInputFromWindow(nicknameEditText.windowToken, 0)
|
||||
imm?.hideSoftInputFromWindow(binding.nicknameEditText.windowToken, 0)
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
@ -80,7 +80,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
|
||||
controller = (Controller) getActivity();
|
||||
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
|
||||
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
|
||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
@ -66,7 +66,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
||||
bucketId = getArguments().getString(KEY_BUCKET_ID);
|
||||
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
|
||||
maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
|
||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Rect;
|
||||
@ -313,7 +313,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
||||
}
|
||||
|
||||
private void initViewModel() {
|
||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
|
||||
viewModel.getSelectedMedia().observe(this, media -> {
|
||||
if (Util.isEmpty(media)) {
|
||||
|
@ -7,8 +7,8 @@ import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_display_name.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityDisplayNameBinding
|
||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
@ -16,28 +16,32 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
||||
class DisplayNameActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityDisplayNameBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo()
|
||||
setContentView(R.layout.activity_display_name)
|
||||
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
displayNameEditText.setOnEditorActionListener(
|
||||
OnEditorActionListener { _, actionID, event ->
|
||||
if (actionID == EditorInfo.IME_ACTION_SEARCH ||
|
||||
actionID == EditorInfo.IME_ACTION_DONE ||
|
||||
(event.action == KeyEvent.ACTION_DOWN &&
|
||||
event.keyCode == KeyEvent.KEYCODE_ENTER)) {
|
||||
this.register()
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
registerButton.setOnClickListener { register() }
|
||||
binding = ActivityDisplayNameBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
with(binding) {
|
||||
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
displayNameEditText.setOnEditorActionListener(
|
||||
OnEditorActionListener { _, actionID, event ->
|
||||
if (actionID == EditorInfo.IME_ACTION_SEARCH ||
|
||||
actionID == EditorInfo.IME_ACTION_DONE ||
|
||||
(event.action == KeyEvent.ACTION_DOWN &&
|
||||
event.keyCode == KeyEvent.KEYCODE_ENTER)) {
|
||||
register()
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
registerButton.setOnClickListener { register() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun register() {
|
||||
val displayName = displayNameEditText.text.toString().trim()
|
||||
val displayName = binding.displayNameEditText.text.toString().trim()
|
||||
if (displayName.isEmpty()) {
|
||||
return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -45,7 +49,7 @@ class DisplayNameActivity : BaseActionBarActivity() {
|
||||
return Toast.makeText(this, R.string.activity_display_name_display_name_too_long_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0)
|
||||
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
||||
TextSecurePreferences.setProfileName(this, displayName)
|
||||
val intent = Intent(this, PNModeActivity::class.java)
|
||||
push(intent)
|
||||
|
@ -3,19 +3,17 @@ package org.thoughtcrime.securesms.onboarding
|
||||
import android.animation.FloatEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.Context.LAYOUT_INFLATER_SERVICE
|
||||
import android.os.Handler
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import kotlinx.android.synthetic.main.view_fake_chat.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewFakeChatBinding
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
|
||||
class FakeChatView : ScrollView {
|
||||
|
||||
private lateinit var binding: ViewFakeChatBinding
|
||||
// region Settings
|
||||
private val spacing = context.resources.getDimension(R.dimen.medium_spacing)
|
||||
private val startDelay: Long = 1000
|
||||
@ -41,17 +39,15 @@ class FakeChatView : ScrollView {
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_fake_chat, null) as LinearLayout
|
||||
contentView.disableClipping()
|
||||
addView(contentView)
|
||||
binding = ViewFakeChatBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.root.disableClipping()
|
||||
isVerticalScrollBarEnabled = false
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Animation
|
||||
fun startAnimating() {
|
||||
listOf( bubble1, bubble2, bubble3, bubble4, bubble5 ).forEach { it.alpha = 0.0f }
|
||||
listOf( binding.bubble1, binding.bubble2, binding.bubble3, binding.bubble4, binding.bubble5 ).forEach { it.alpha = 0.0f }
|
||||
fun show(bubble: View) {
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||
animation.duration = animationDuration
|
||||
@ -61,18 +57,18 @@ class FakeChatView : ScrollView {
|
||||
animation.start()
|
||||
}
|
||||
Handler().postDelayed({
|
||||
show(bubble1)
|
||||
show(binding.bubble1)
|
||||
Handler().postDelayed({
|
||||
show(bubble2)
|
||||
show(binding.bubble2)
|
||||
Handler().postDelayed({
|
||||
show(bubble3)
|
||||
smoothScrollTo(0, (bubble1.height + spacing).toInt())
|
||||
show(binding.bubble3)
|
||||
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt())
|
||||
Handler().postDelayed({
|
||||
show(bubble4)
|
||||
smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt())
|
||||
show(binding.bubble4)
|
||||
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt())
|
||||
Handler().postDelayed({
|
||||
show(bubble5)
|
||||
smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt() + (bubble3.height + spacing).toInt())
|
||||
show(binding.bubble5)
|
||||
smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt() + (binding.bubble3.height + spacing).toInt())
|
||||
}, delayBetweenMessages)
|
||||
}, delayBetweenMessages)
|
||||
}, delayBetweenMessages)
|
||||
|
@ -2,25 +2,27 @@ package org.thoughtcrime.securesms.onboarding
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityLandingBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
|
||||
class LandingActivity : BaseActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_landing)
|
||||
val binding = ActivityLandingBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setUpActionBarSessionLogo(true)
|
||||
findViewById<FakeChatView>(R.id.fakeChatView).startAnimating()
|
||||
findViewById<View>(R.id.registerButton).setOnClickListener { register() }
|
||||
findViewById<View>(R.id.restoreButton).setOnClickListener { restore() }
|
||||
findViewById<View>(R.id.linkButton).setOnClickListener { link() }
|
||||
with(binding) {
|
||||
fakeChatView.startAnimating()
|
||||
registerButton.setOnClickListener { register() }
|
||||
restoreButton.setOnClickListener { restore() }
|
||||
linkButton.setOnClickListener { link() }
|
||||
}
|
||||
IdentityKeyUtil.generateIdentityKeyPair(this)
|
||||
TextSecurePreferences.setPasswordDisabled(this, true)
|
||||
// AC: This is a temporary workaround to trick the old code that the screen is unlocked.
|
||||
|
@ -4,7 +4,9 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
@ -13,14 +15,13 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_link_device.*
|
||||
import kotlinx.android.synthetic.main.fragment_recovery_phrase.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
|
||||
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.utilities.Hex
|
||||
@ -30,13 +31,14 @@ import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityLinkDeviceBinding
|
||||
private val adapter = LinkDeviceActivityAdapter(this)
|
||||
private var restoreJob: Job? = null
|
||||
|
||||
@ -55,9 +57,10 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
||||
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
||||
setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
|
||||
}
|
||||
setContentView(R.layout.activity_link_device)
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
binding = ActivityLinkDeviceBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -107,8 +110,8 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
||||
TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
||||
TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true)
|
||||
|
||||
loader.isVisible = true
|
||||
val snackBar = Snackbar.make(containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE)
|
||||
binding.loader.isVisible = true
|
||||
val snackBar = Snackbar.make(binding.containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.registration_activity__skip) { register(true) }
|
||||
|
||||
val skipJob = launch {
|
||||
@ -127,13 +130,13 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
||||
register(false)
|
||||
}
|
||||
|
||||
loader.isVisible = false
|
||||
binding.loader.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun register(skipped: Boolean) {
|
||||
restoreJob?.cancel()
|
||||
loader.isVisible = false
|
||||
binding.loader.isVisible = false
|
||||
TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis())
|
||||
val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
@ -175,30 +178,34 @@ private class LinkDeviceActivityAdapter(private val activity: LinkDeviceActivity
|
||||
|
||||
// region Recovery Phrase Fragment
|
||||
class RecoveryPhraseFragment : Fragment() {
|
||||
private lateinit var binding: FragmentRecoveryPhraseBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_recovery_phrase, container, false)
|
||||
binding = FragmentRecoveryPhraseBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
handleContinueButtonTapped()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
with(binding) {
|
||||
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
handleContinueButtonTapped()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
continueButton.setOnClickListener { handleContinueButtonTapped() }
|
||||
}
|
||||
continueButton.setOnClickListener { handleContinueButtonTapped() }
|
||||
}
|
||||
|
||||
private fun handleContinueButtonTapped() {
|
||||
val mnemonic = mnemonicEditText.text?.trim().toString()
|
||||
val mnemonic = binding.mnemonicEditText.text?.trim().toString()
|
||||
(requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic)
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,8 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import kotlinx.android.synthetic.main.activity_display_name.registerButton
|
||||
import kotlinx.android.synthetic.main.activity_pn_mode.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityPnModeBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
@ -28,6 +27,7 @@ import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.util.PNModeView
|
||||
|
||||
class PNModeActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityPnModeBinding
|
||||
private var selectedOptionView: PNModeView? = null
|
||||
|
||||
// region Lifecycle
|
||||
@ -35,15 +35,18 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo(true)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||
setContentView(R.layout.activity_pn_mode)
|
||||
contentView.disableClipping()
|
||||
fcmOptionView.setOnClickListener { toggleFCM() }
|
||||
fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
||||
fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
||||
backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() }
|
||||
backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
||||
backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
||||
registerButton.setOnClickListener { register() }
|
||||
binding = ActivityPnModeBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
with(binding) {
|
||||
contentView.disableClipping()
|
||||
fcmOptionView.setOnClickListener { toggleFCM() }
|
||||
fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
||||
fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
||||
backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() }
|
||||
backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
|
||||
backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
|
||||
registerButton.setOnClickListener { register() }
|
||||
}
|
||||
toggleFCM()
|
||||
}
|
||||
|
||||
@ -63,8 +66,7 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
|
||||
// region Interaction
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val id = item.itemId
|
||||
when(id) {
|
||||
when(item.itemId) {
|
||||
R.id.learnMoreButton -> learnMore()
|
||||
else -> { /* Do nothing */ }
|
||||
}
|
||||
@ -81,52 +83,52 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleFCM() {
|
||||
private fun toggleFCM() = with(binding) {
|
||||
when (selectedOptionView) {
|
||||
null -> {
|
||||
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
|
||||
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.transparent, R.color.accent)
|
||||
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.transparent, R.color.accent)
|
||||
animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent)
|
||||
selectedOptionView = fcmOptionView
|
||||
}
|
||||
fcmOptionView -> {
|
||||
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
|
||||
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.accent, R.color.transparent)
|
||||
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.accent, R.color.transparent)
|
||||
animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border)
|
||||
selectedOptionView = null
|
||||
}
|
||||
backgroundPollingOptionView -> {
|
||||
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
|
||||
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.transparent, R.color.accent)
|
||||
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.transparent, R.color.accent)
|
||||
animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent)
|
||||
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
|
||||
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.accent, R.color.transparent)
|
||||
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.accent, R.color.transparent)
|
||||
animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border)
|
||||
selectedOptionView = fcmOptionView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleBackgroundPolling() {
|
||||
private fun toggleBackgroundPolling() = with(binding) {
|
||||
when (selectedOptionView) {
|
||||
null -> {
|
||||
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
|
||||
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.transparent, R.color.accent)
|
||||
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.transparent, R.color.accent)
|
||||
animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent)
|
||||
selectedOptionView = backgroundPollingOptionView
|
||||
}
|
||||
backgroundPollingOptionView -> {
|
||||
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
|
||||
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.accent, R.color.transparent)
|
||||
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.accent, R.color.transparent)
|
||||
animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border)
|
||||
selectedOptionView = null
|
||||
}
|
||||
fcmOptionView -> {
|
||||
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
|
||||
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.transparent, R.color.accent)
|
||||
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.transparent, R.color.accent)
|
||||
animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent)
|
||||
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
|
||||
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.accent, R.color.transparent)
|
||||
GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.accent, R.color.transparent)
|
||||
animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border)
|
||||
selectedOptionView = backgroundPollingOptionView
|
||||
}
|
||||
@ -153,7 +155,7 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
dialog.create().show()
|
||||
return
|
||||
}
|
||||
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView))
|
||||
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView))
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.startPollingIfNeeded()
|
||||
application.registerForFCMIfNeeded(true)
|
||||
|
@ -11,8 +11,8 @@ import android.text.style.ClickableSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_recovery_phrase_restore.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.utilities.Hex
|
||||
@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
|
||||
private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -36,9 +36,10 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
||||
setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
||||
}
|
||||
setContentView(R.layout.activity_recovery_phrase_restore)
|
||||
mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
restoreButton.setOnClickListener { restore() }
|
||||
binding = ActivityRecoveryPhraseRestoreBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.mnemonicEditText.imeOptions = binding.mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
binding.restoreButton.setOnClickListener { restore() }
|
||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
|
||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsExplanation.setSpan(object : ClickableSpan() {
|
||||
@ -54,14 +55,14 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
openURL("https://getsession.org/privacy-policy/")
|
||||
}
|
||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
termsTextView.text = termsExplanation
|
||||
binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.termsTextView.text = termsExplanation
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
private fun restore() {
|
||||
val mnemonic = mnemonicEditText.text.toString()
|
||||
val mnemonic = binding.mnemonicEditText.text.toString()
|
||||
try {
|
||||
val loadFileContents: (String) -> String = { fileName ->
|
||||
MnemonicUtilities.loadFileContents(this, fileName)
|
||||
|
@ -16,8 +16,8 @@ import android.text.style.StyleSpan
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import kotlinx.android.synthetic.main.activity_register.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityRegisterBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
@ -26,9 +26,9 @@ import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
import java.util.*
|
||||
|
||||
class RegisterActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityRegisterBinding
|
||||
private var seed: ByteArray? = null
|
||||
private var ed25519KeyPair: KeyPair? = null
|
||||
private var x25519KeyPair: ECKeyPair? = null
|
||||
@ -37,7 +37,8 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_register)
|
||||
binding = ActivityRegisterBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setUpActionBarSessionLogo()
|
||||
TextSecurePreferences.apply {
|
||||
setHasViewedSeed(this@RegisterActivity, false)
|
||||
@ -45,8 +46,8 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
setRestorationTime(this@RegisterActivity, 0)
|
||||
setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis())
|
||||
}
|
||||
registerButton.setOnClickListener { register() }
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
binding.registerButton.setOnClickListener { register() }
|
||||
binding.copyButton.setOnClickListener { copyPublicKey() }
|
||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
|
||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsExplanation.setSpan(object : ClickableSpan() {
|
||||
@ -62,8 +63,8 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
openURL("https://getsession.org/privacy-policy/")
|
||||
}
|
||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
termsTextView.text = termsExplanation
|
||||
binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.termsTextView.text = termsExplanation
|
||||
updateKeyPair()
|
||||
}
|
||||
// endregion
|
||||
@ -94,12 +95,12 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
}
|
||||
count += 1
|
||||
if (count < limit) {
|
||||
publicKeyTextView.text = mangledHexEncodedPublicKey
|
||||
binding.publicKeyTextView.text = mangledHexEncodedPublicKey
|
||||
Handler().postDelayed({
|
||||
animate()
|
||||
}, 32)
|
||||
} else {
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
binding.publicKeyTextView.text = hexEncodedPublicKey
|
||||
}
|
||||
}
|
||||
animate()
|
||||
|
@ -9,8 +9,8 @@ import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_seed.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivitySeedBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
||||
@ -21,6 +21,8 @@ import org.thoughtcrime.securesms.util.getColorWithID
|
||||
|
||||
class SeedActivity : BaseActionBarActivity() {
|
||||
|
||||
private lateinit var binding: ActivitySeedBinding
|
||||
|
||||
private val seed by lazy {
|
||||
var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED)
|
||||
if (hexEncodedSeed == null) {
|
||||
@ -35,27 +37,30 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_seed)
|
||||
binding = ActivitySeedBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_seed_title)
|
||||
val seedReminderViewTitle = SpannableString("You're almost finished! 90%") // Intentionally not yet translated
|
||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2)
|
||||
seedReminderView.setProgress(90, false)
|
||||
seedReminderView.hideContinueButton()
|
||||
var redactedSeed = seed
|
||||
var index = 0
|
||||
for (character in seed) {
|
||||
if (character.isLetter()) {
|
||||
redactedSeed = redactedSeed.replaceRange(index, index + 1, "▆")
|
||||
with(binding) {
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2)
|
||||
seedReminderView.setProgress(90, false)
|
||||
seedReminderView.hideContinueButton()
|
||||
var redactedSeed = seed
|
||||
var index = 0
|
||||
for (character in seed) {
|
||||
if (character.isLetter()) {
|
||||
redactedSeed = redactedSeed.replaceRange(index, index + 1, "▆")
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
index += 1
|
||||
seedTextView.setTextColor(resources.getColorWithID(R.color.accent, theme))
|
||||
seedTextView.text = redactedSeed
|
||||
seedTextView.setOnLongClickListener { revealSeed(); true }
|
||||
revealButton.setOnLongClickListener { revealSeed(); true }
|
||||
copyButton.setOnClickListener { copySeed() }
|
||||
}
|
||||
seedTextView.setTextColor(resources.getColorWithID(R.color.accent, theme))
|
||||
seedTextView.text = redactedSeed
|
||||
seedTextView.setOnLongClickListener { revealSeed(); true }
|
||||
revealButton.setOnLongClickListener { revealSeed(); true }
|
||||
copyButton.setOnClickListener { copySeed() }
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -63,14 +68,16 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
private fun revealSeed() {
|
||||
val seedReminderViewTitle = SpannableString("Account secured! 100%") // Intentionally not yet translated
|
||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 17, 21, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3)
|
||||
seedReminderView.setProgress(100, true)
|
||||
val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams
|
||||
seedTextViewLayoutParams.height = seedTextView.height
|
||||
seedTextView.layoutParams = seedTextViewLayoutParams
|
||||
seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
|
||||
seedTextView.text = seed
|
||||
with(binding) {
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3)
|
||||
seedReminderView.setProgress(100, true)
|
||||
val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams
|
||||
seedTextViewLayoutParams.height = seedTextView.height
|
||||
seedTextView.layoutParams = seedTextViewLayoutParams
|
||||
seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
|
||||
seedTextView.text = seed
|
||||
}
|
||||
TextSecurePreferences.setHasViewedSeed(this, true)
|
||||
}
|
||||
// endregion
|
||||
|
@ -6,16 +6,17 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import kotlinx.android.synthetic.main.view_seed_reminder.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewSeedReminderBinding
|
||||
|
||||
class SeedReminderView : FrameLayout {
|
||||
private lateinit var binding: ViewSeedReminderBinding
|
||||
|
||||
var title: CharSequence
|
||||
get() = titleTextView.text
|
||||
set(value) { titleTextView.text = value }
|
||||
get() = binding.titleTextView.text
|
||||
set(value) { binding.titleTextView.text = value }
|
||||
var subtitle: CharSequence
|
||||
get() = subtitleTextView.text
|
||||
set(value) { subtitleTextView.text = value }
|
||||
get() = binding.subtitleTextView.text
|
||||
set(value) { binding.subtitleTextView.text = value }
|
||||
var delegate: SeedReminderViewDelegate? = null
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
@ -35,22 +36,20 @@ class SeedReminderView : FrameLayout {
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_seed_reminder, null)
|
||||
addView(contentView)
|
||||
button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() }
|
||||
binding = ViewSeedReminderBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() }
|
||||
}
|
||||
|
||||
fun setProgress(progress: Int, isAnimated: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
progressBar.setProgress(progress, isAnimated)
|
||||
binding.progressBar.setProgress(progress, isAnimated)
|
||||
} else {
|
||||
progressBar.progress = progress
|
||||
binding.progressBar.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
fun hideContinueButton() {
|
||||
button.visibility = View.GONE
|
||||
binding.button.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,12 @@ import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.android.synthetic.main.dialog_clear_all_data.*
|
||||
import kotlinx.android.synthetic.main.dialog_clear_all_data.view.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogClearAllDataBinding
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
@ -15,6 +17,7 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
|
||||
class ClearAllDataDialog : BaseDialog() {
|
||||
private lateinit var binding: DialogClearAllDataBinding
|
||||
|
||||
enum class Steps {
|
||||
INFO_PROMPT,
|
||||
@ -34,15 +37,15 @@ class ClearAllDataDialog : BaseDialog() {
|
||||
}
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null)
|
||||
contentView.cancelButton.setOnClickListener {
|
||||
binding = DialogClearAllDataBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener {
|
||||
if (step == Steps.NETWORK_PROMPT) {
|
||||
clearAllData(false)
|
||||
} else if (step != Steps.DELETING) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
contentView.clearAllDataButton.setOnClickListener {
|
||||
binding.clearAllDataButton.setOnClickListener {
|
||||
when(step) {
|
||||
Steps.INFO_PROMPT -> step = Steps.NETWORK_PROMPT
|
||||
Steps.NETWORK_PROMPT -> {
|
||||
@ -51,36 +54,33 @@ class ClearAllDataDialog : BaseDialog() {
|
||||
Steps.DELETING -> { /* do nothing intentionally */ }
|
||||
}
|
||||
}
|
||||
builder.setView(contentView)
|
||||
builder.setView(binding.root)
|
||||
builder.setCancelable(false)
|
||||
}
|
||||
|
||||
private fun updateUI() {
|
||||
|
||||
dialog?.let { view ->
|
||||
|
||||
dialog?.let {
|
||||
val isLoading = step == Steps.DELETING
|
||||
|
||||
when (step) {
|
||||
Steps.INFO_PROMPT -> {
|
||||
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation)
|
||||
view.cancelButton.setText(R.string.cancel)
|
||||
view.clearAllDataButton.setText(R.string.delete)
|
||||
binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation)
|
||||
binding.cancelButton.setText(R.string.cancel)
|
||||
binding.clearAllDataButton.setText(R.string.delete)
|
||||
}
|
||||
else -> {
|
||||
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation)
|
||||
view.cancelButton.setText(R.string.dialog_clear_all_data_local_only)
|
||||
view.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network)
|
||||
binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation)
|
||||
binding.cancelButton.setText(R.string.dialog_clear_all_data_local_only)
|
||||
binding.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network)
|
||||
}
|
||||
}
|
||||
|
||||
view.cancelButton.isVisible = !isLoading
|
||||
view.clearAllDataButton.isVisible = !isLoading
|
||||
view.progressBar.isVisible = isLoading
|
||||
binding.cancelButton.isVisible = !isLoading
|
||||
binding.clearAllDataButton.isVisible = !isLoading
|
||||
binding.progressBar.isVisible = isLoading
|
||||
|
||||
view.setCanceledOnTouchOutside(!isLoading)
|
||||
it.setCanceledOnTouchOutside(!isLoading)
|
||||
isCancelable = !isLoading
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,9 @@ import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import kotlinx.android.synthetic.main.activity_qr_code.*
|
||||
import kotlinx.android.synthetic.main.fragment_view_my_qr_code.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityQrCodeBinding
|
||||
import network.loki.messenger.databinding.FragmentViewMyQrCodeBinding
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
@ -20,23 +20,29 @@ import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.*
|
||||
import org.thoughtcrime.securesms.util.FileProviderUtil
|
||||
import org.thoughtcrime.securesms.util.QRCodeUtilities
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityQrCodeBinding
|
||||
private val adapter = QRCodeActivityAdapter(this)
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityQrCodeBinding.inflate(layoutInflater)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_qr_code)
|
||||
setContentView(binding.root)
|
||||
// Set title
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title)
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -91,6 +97,7 @@ private class QRCodeActivityAdapter(val activity: QRCodeActivity) : FragmentPage
|
||||
|
||||
// region View My QR Code Fragment
|
||||
class ViewMyQRCodeFragment : Fragment() {
|
||||
private lateinit var binding: FragmentViewMyQrCodeBinding
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
get() {
|
||||
@ -98,18 +105,19 @@ class ViewMyQRCodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_view_my_qr_code, container, false)
|
||||
binding = FragmentViewMyQrCodeBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val size = toPx(280, resources)
|
||||
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
|
||||
qrCodeImageView.setImageBitmap(qrCode)
|
||||
binding.qrCodeImageView.setImageBitmap(qrCode)
|
||||
// val explanation = SpannableStringBuilder("This is your unique public QR code. Other users can scan this to start a conversation with you.")
|
||||
// explanation.setSpan(StyleSpan(Typeface.BOLD), 8, 34, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation)
|
||||
shareButton.setOnClickListener { shareQRCode() }
|
||||
binding.explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation)
|
||||
binding.shareButton.setOnClickListener { shareQRCode() }
|
||||
}
|
||||
|
||||
private fun shareQRCode() {
|
||||
|
@ -6,8 +6,8 @@ import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_seed.view.*
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogSeedBinding
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
@ -28,11 +28,11 @@ class SeedDialog : BaseDialog() {
|
||||
}
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_seed, null)
|
||||
contentView.seedTextView.text = seed
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.copyButton.setOnClickListener { copySeed() }
|
||||
builder.setView(contentView)
|
||||
val binding = DialogSeedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.seedTextView.text = seed
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.copyButton.setOnClickListener { copySeed() }
|
||||
builder.setView(binding.root)
|
||||
}
|
||||
|
||||
private fun copySeed() {
|
||||
|
@ -7,7 +7,11 @@ import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@ -15,9 +19,9 @@ import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.activity_settings.*
|
||||
import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivitySettingsBinding
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.all
|
||||
import nl.komponents.kovenant.ui.alwaysUi
|
||||
@ -34,12 +38,17 @@ import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
||||
import org.thoughtcrime.securesms.util.*
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
private lateinit var binding: ActivitySettingsBinding
|
||||
private var displayNameEditActionMode: ActionMode? = null
|
||||
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
|
||||
private lateinit var glide: GlideRequests
|
||||
@ -59,33 +68,36 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
setContentView(R.layout.activity_settings)
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey
|
||||
glide = GlideApp.with(this)
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.publicKey = hexEncodedPublicKey
|
||||
profilePictureView.displayName = displayName
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update()
|
||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||
btnGroupNameDisplay.text = displayName
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
shareButton.setOnClickListener { sharePublicKey() }
|
||||
privacyButton.setOnClickListener { showPrivacySettings() }
|
||||
notificationsButton.setOnClickListener { showNotificationSettings() }
|
||||
chatsButton.setOnClickListener { showChatSettings() }
|
||||
sendInvitationButton.setOnClickListener { sendInvitation() }
|
||||
faqButton.setOnClickListener { showFAQ() }
|
||||
surveyButton.setOnClickListener { showSurvey() }
|
||||
helpTranslateButton.setOnClickListener { helpTranslate() }
|
||||
seedButton.setOnClickListener { showSeed() }
|
||||
clearAllDataButton.setOnClickListener { clearAllData() }
|
||||
debugLogButton.setOnClickListener { shareLogs() }
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(this)
|
||||
oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode)
|
||||
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
with(binding) {
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.publicKey = hexEncodedPublicKey
|
||||
profilePictureView.displayName = displayName
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update()
|
||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||
btnGroupNameDisplay.text = displayName
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
shareButton.setOnClickListener { sharePublicKey() }
|
||||
privacyButton.setOnClickListener { showPrivacySettings() }
|
||||
notificationsButton.setOnClickListener { showNotificationSettings() }
|
||||
chatsButton.setOnClickListener { showChatSettings() }
|
||||
sendInvitationButton.setOnClickListener { sendInvitation() }
|
||||
faqButton.setOnClickListener { showFAQ() }
|
||||
surveyButton.setOnClickListener { showSurvey() }
|
||||
helpTranslateButton.setOnClickListener { helpTranslate() }
|
||||
seedButton.setOnClickListener { showSeed() }
|
||||
clearAllDataButton.setOnClickListener { clearAllData() }
|
||||
debugLogButton.setOnClickListener { shareLogs() }
|
||||
val isLightMode = UiModeUtilities.isDayUiMode(this@SettingsActivity)
|
||||
oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode)
|
||||
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@ -152,22 +164,22 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
private fun handleDisplayNameEditActionModeChanged() {
|
||||
val isEditingDisplayName = this.displayNameEditActionMode !== null
|
||||
|
||||
btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
|
||||
displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
|
||||
binding.btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
|
||||
binding.displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
if (isEditingDisplayName) {
|
||||
displayNameEditText.setText(btnGroupNameDisplay.text)
|
||||
displayNameEditText.selectAll()
|
||||
displayNameEditText.requestFocus()
|
||||
inputMethodManager.showSoftInput(displayNameEditText, 0)
|
||||
binding.displayNameEditText.setText(binding.btnGroupNameDisplay.text)
|
||||
binding.displayNameEditText.selectAll()
|
||||
binding.displayNameEditText.requestFocus()
|
||||
inputMethodManager.showSoftInput(binding.displayNameEditText, 0)
|
||||
} else {
|
||||
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0)
|
||||
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfile(isUpdatingProfilePicture: Boolean) {
|
||||
loader.isVisible = true
|
||||
binding.loader.isVisible = true
|
||||
val promises = mutableListOf<Promise<*, Exception>>()
|
||||
val displayName = displayNameToBeUploaded
|
||||
if (displayName != null) {
|
||||
@ -192,15 +204,15 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
compoundPromise.alwaysUi {
|
||||
if (displayName != null) {
|
||||
btnGroupNameDisplay.text = displayName
|
||||
binding.btnGroupNameDisplay.text = displayName
|
||||
}
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
profilePictureView.recycle() // Clear the cached image before updating
|
||||
profilePictureView.update()
|
||||
binding.profilePictureView.recycle() // Clear the cached image before updating
|
||||
binding.profilePictureView.update()
|
||||
}
|
||||
displayNameToBeUploaded = null
|
||||
profilePictureToBeUploaded = null
|
||||
loader.isVisible = false
|
||||
binding.loader.isVisible = false
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
@ -211,7 +223,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
* @return true if the update was successful.
|
||||
*/
|
||||
private fun saveDisplayName(): Boolean {
|
||||
val displayName = displayNameEditText.text.toString().trim()
|
||||
val displayName = binding.displayNameEditText.text.toString().trim()
|
||||
if (displayName.isEmpty()) {
|
||||
Toast.makeText(this, R.string.activity_settings_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
|
@ -13,12 +13,12 @@ import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.android.synthetic.main.dialog_share_logs.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogShareLogsBinding
|
||||
import org.session.libsignal.utilities.ExternalStorageUtil
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
@ -26,7 +26,7 @@ import org.thoughtcrime.securesms.util.StreamUtil
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.Objects
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ShareLogsDialog : BaseDialog() {
|
||||
@ -34,16 +34,15 @@ class ShareLogsDialog : BaseDialog() {
|
||||
private var shareJob: Job? = null
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val contentView =
|
||||
LayoutInflater.from(requireContext()).inflate(R.layout.dialog_share_logs, null)
|
||||
contentView.cancelButton.setOnClickListener {
|
||||
val binding = DialogShareLogsBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
contentView.shareButton.setOnClickListener {
|
||||
binding.shareButton.setOnClickListener {
|
||||
// start the export and share
|
||||
shareLogs()
|
||||
}
|
||||
builder.setView(contentView)
|
||||
builder.setView(binding.root)
|
||||
builder.setCancelable(false)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,229 @@
|
||||
package org.thoughtcrime.securesms.repository
|
||||
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
interface ConversationRepository {
|
||||
fun isOxenHostedOpenGroup(threadId: Long): Boolean
|
||||
fun getRecipientForThreadId(threadId: Long): Recipient
|
||||
fun saveDraft(threadId: Long, text: String)
|
||||
fun getDraft(threadId: Long): String?
|
||||
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
||||
fun unblock(recipient: Recipient)
|
||||
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
||||
|
||||
suspend fun deleteForEveryone(
|
||||
threadId: Long,
|
||||
recipient: Recipient,
|
||||
message: MessageRecord
|
||||
): ResultOf<Unit>
|
||||
|
||||
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
|
||||
|
||||
suspend fun deleteMessageWithoutUnsendRequest(
|
||||
threadId: Long,
|
||||
messages: Set<MessageRecord>
|
||||
): ResultOf<Unit>
|
||||
|
||||
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||
|
||||
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||
}
|
||||
|
||||
class DefaultConversationRepository @Inject constructor(
|
||||
private val textSecurePreferences: TextSecurePreferences,
|
||||
private val messageDataProvider: MessageDataProvider,
|
||||
private val threadDb: ThreadDatabase,
|
||||
private val draftDb: DraftDatabase,
|
||||
private val lokiThreadDb: LokiThreadDatabase,
|
||||
private val smsDb: SmsDatabase,
|
||||
private val mmsDb: MmsDatabase,
|
||||
private val recipientDb: RecipientDatabase,
|
||||
private val lokiMessageDb: LokiMessageDatabase
|
||||
) : ConversationRepository {
|
||||
|
||||
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||
return openGroup?.room == "session" || openGroup?.room == "oxen"
|
||||
|| openGroup?.room == "lokinet" || openGroup?.room == "crypto"
|
||||
}
|
||||
|
||||
override fun getRecipientForThreadId(threadId: Long): Recipient {
|
||||
return threadDb.getRecipientForThreadId(threadId)!!
|
||||
}
|
||||
|
||||
override fun saveDraft(threadId: Long, text: String) {
|
||||
if (text.isEmpty()) return
|
||||
val drafts = DraftDatabase.Drafts()
|
||||
drafts.add(DraftDatabase.Draft(DraftDatabase.Draft.TEXT, text))
|
||||
draftDb.insertDrafts(threadId, drafts)
|
||||
}
|
||||
|
||||
override fun getDraft(threadId: Long): String? {
|
||||
val drafts = draftDb.getDrafts(threadId)
|
||||
draftDb.clearDrafts(threadId)
|
||||
return drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value
|
||||
}
|
||||
|
||||
override fun inviteContacts(threadId: Long, contacts: List<Recipient>) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
|
||||
for (contact in contacts) {
|
||||
val message = VisibleMessage()
|
||||
message.sentTimestamp = System.currentTimeMillis()
|
||||
val openGroupInvitation = OpenGroupInvitation()
|
||||
openGroupInvitation.name = openGroup.name
|
||||
openGroupInvitation.url = openGroup.joinURL
|
||||
message.openGroupInvitation = openGroupInvitation
|
||||
val outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation(
|
||||
openGroupInvitation,
|
||||
contact,
|
||||
message.sentTimestamp
|
||||
)
|
||||
smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!)
|
||||
MessageSender.send(message, contact.address)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unblock(recipient: Recipient) {
|
||||
recipientDb.setBlocked(recipient, false)
|
||||
}
|
||||
|
||||
override fun deleteLocally(recipient: Recipient, message: MessageRecord) {
|
||||
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||
textSecurePreferences.getLocalNumber()?.let {
|
||||
MessageSender.send(unsendRequest, Address.fromSerialized(it))
|
||||
}
|
||||
}
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
}
|
||||
|
||||
override suspend fun deleteForEveryone(
|
||||
threadId: Long,
|
||||
recipient: Recipient,
|
||||
message: MessageRecord
|
||||
): ResultOf<Unit> = suspendCoroutine { continuation ->
|
||||
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||
MessageSender.send(unsendRequest, recipient.address)
|
||||
}
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||
if (openGroup != null) {
|
||||
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
|
||||
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||
.success {
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
}.fail { error ->
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
messageDataProvider.getServerHashForMessage(message.id)?.let { serverHash ->
|
||||
var publicKey = recipient.address.serialize()
|
||||
if (recipient.isClosedGroupRecipient) {
|
||||
publicKey = GroupUtil.doubleDecodeGroupID(publicKey).toHexString()
|
||||
}
|
||||
SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
|
||||
.success {
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
}.fail { error ->
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? {
|
||||
if (recipient.isOpenGroupRecipient) return null
|
||||
messageDataProvider.getServerHashForMessage(message.id) ?: return null
|
||||
val unsendRequest = UnsendRequest()
|
||||
if (message.isOutgoing) {
|
||||
unsendRequest.author = textSecurePreferences.getLocalNumber()
|
||||
} else {
|
||||
unsendRequest.author = message.individualRecipient.address.contactIdentifier()
|
||||
}
|
||||
unsendRequest.timestamp = message.timestamp
|
||||
|
||||
return unsendRequest
|
||||
}
|
||||
|
||||
override suspend fun deleteMessageWithoutUnsendRequest(
|
||||
threadId: Long,
|
||||
messages: Set<MessageRecord>
|
||||
): ResultOf<Unit> = suspendCoroutine { continuation ->
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||
if (openGroup != null) {
|
||||
val messageServerIDs = mutableMapOf<Long, MessageRecord>()
|
||||
for (message in messages) {
|
||||
val messageServerID =
|
||||
lokiMessageDb.getServerID(message.id, !message.isMms) ?: continue
|
||||
messageServerIDs[messageServerID] = message
|
||||
}
|
||||
for ((messageServerID, message) in messageServerIDs) {
|
||||
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||
.success {
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
}.fail { error ->
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (message in messages) {
|
||||
if (message.isMms) {
|
||||
mmsDb.deleteMessage(message.id)
|
||||
} else {
|
||||
smsDb.deleteMessage(message.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
}
|
||||
|
||||
override suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit> =
|
||||
suspendCoroutine { continuation ->
|
||||
val sessionID = recipient.address.toString()
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||
OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server)
|
||||
.success {
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
}.fail { error ->
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> =
|
||||
suspendCoroutine { continuation ->
|
||||
val sessionID = recipient.address.toString()
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||
OpenGroupAPIV2.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
|
||||
.success {
|
||||
continuation.resume(ResultOf.Success(Unit))
|
||||
}.fail { error ->
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.repository
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
sealed class ResultOf<out T> {
|
||||
|
||||
data class Success<out R>(val value: R) : ResultOf<R>()
|
||||
|
||||
data class Failure(val throwable: Throwable) : ResultOf<Nothing>()
|
||||
|
||||
inline fun onFailure(block: (throwable: Throwable) -> Unit) = this.also {
|
||||
if (this is Failure) {
|
||||
block(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun onSuccess(block: (value: T) -> Unit) = this.also {
|
||||
if (this is Success) {
|
||||
block(value)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <R> flatMap(mapper: (T) -> R): ResultOf<R> = when (this) {
|
||||
is Success -> wrap { mapper(value) }
|
||||
is Failure -> Failure(throwable)
|
||||
}
|
||||
|
||||
fun getOrThrow(): T = when (this) {
|
||||
is Success -> value
|
||||
is Failure -> throw throwable
|
||||
}
|
||||
|
||||
companion object {
|
||||
inline fun <T> wrap(block: () -> T): ResultOf<T> =
|
||||
try {
|
||||
Success(block())
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Failure(e)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,39 +2,40 @@ package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.fragment_scan_qr_code.*
|
||||
import network.loki.messenger.R
|
||||
import androidx.fragment.app.Fragment
|
||||
import network.loki.messenger.databinding.FragmentScanQrCodeBinding
|
||||
import org.thoughtcrime.securesms.qr.ScanListener
|
||||
import org.thoughtcrime.securesms.qr.ScanningThread
|
||||
|
||||
class ScanQRCodeFragment : Fragment() {
|
||||
private lateinit var binding: FragmentScanQrCodeBinding
|
||||
private val scanningThread = ScanningThread()
|
||||
var scanListener: ScanListener? = null
|
||||
set(value) { field = value; scanningThread.setScanListener(scanListener) }
|
||||
var message: CharSequence = ""
|
||||
|
||||
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? {
|
||||
return layoutInflater.inflate(R.layout.fragment_scan_qr_code, viewGroup, false)
|
||||
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View {
|
||||
binding = FragmentScanQrCodeBinding.inflate(layoutInflater, viewGroup, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, bundle: Bundle?) {
|
||||
super.onViewCreated(view, bundle)
|
||||
when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> overlayView.orientation = LinearLayout.HORIZONTAL
|
||||
else -> overlayView.orientation = LinearLayout.VERTICAL
|
||||
Configuration.ORIENTATION_LANDSCAPE -> binding.overlayView.orientation = LinearLayout.HORIZONTAL
|
||||
else -> binding.overlayView.orientation = LinearLayout.VERTICAL
|
||||
}
|
||||
messageTextView.text = message
|
||||
binding.messageTextView.text = message
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
cameraView.onResume()
|
||||
cameraView.setPreviewCallback(scanningThread)
|
||||
binding.cameraView.onResume()
|
||||
binding.cameraView.setPreviewCallback(scanningThread)
|
||||
try {
|
||||
scanningThread.start()
|
||||
} catch (exception: Exception) {
|
||||
@ -45,18 +46,18 @@ class ScanQRCodeFragment : Fragment() {
|
||||
|
||||
override fun onConfigurationChanged(newConfiguration: Configuration) {
|
||||
super.onConfigurationChanged(newConfiguration)
|
||||
this.cameraView.onPause()
|
||||
binding.cameraView.onPause()
|
||||
when (newConfiguration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> overlayView.orientation = LinearLayout.HORIZONTAL
|
||||
else -> overlayView.orientation = LinearLayout.VERTICAL
|
||||
Configuration.ORIENTATION_LANDSCAPE -> binding.overlayView.orientation = LinearLayout.HORIZONTAL
|
||||
else -> binding.overlayView.orientation = LinearLayout.VERTICAL
|
||||
}
|
||||
cameraView.onResume()
|
||||
cameraView.setPreviewCallback(scanningThread)
|
||||
binding.cameraView.onResume()
|
||||
binding.cameraView.setPreviewCallback(scanningThread)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
this.cameraView.onPause()
|
||||
this.binding.cameraView.onPause()
|
||||
this.scanningThread.stopScanning()
|
||||
}
|
||||
}
|
@ -1,23 +1,24 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.fragment_scan_qr_code_placeholder.*
|
||||
import network.loki.messenger.R
|
||||
import androidx.fragment.app.Fragment
|
||||
import network.loki.messenger.databinding.FragmentScanQrCodePlaceholderBinding
|
||||
|
||||
class ScanQRCodePlaceholderFragment: Fragment() {
|
||||
private lateinit var binding: FragmentScanQrCodePlaceholderBinding
|
||||
var delegate: ScanQRCodePlaceholderFragmentDelegate? = null
|
||||
|
||||
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? {
|
||||
return layoutInflater.inflate(R.layout.fragment_scan_qr_code_placeholder, viewGroup, false)
|
||||
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View {
|
||||
binding = FragmentScanQrCodePlaceholderBinding.inflate(layoutInflater, viewGroup, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
grantCameraAccessButton.setOnClickListener { delegate?.requestCameraAccess() }
|
||||
binding.grantCameraAccessButton.setOnClickListener { delegate?.requestCameraAccess() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/contentView"
|
||||
@ -74,7 +73,7 @@
|
||||
android:id="@+id/seedReminderStub"
|
||||
android:layout="@layout/seed_reminder_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@ -91,7 +90,9 @@
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="172dp"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/view_conversation"/>
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="6"
|
||||
tools:listitem="@layout/view_conversation" />
|
||||
|
||||
<View
|
||||
android:id="@+id/gradientView"
|
||||
@ -103,21 +104,22 @@
|
||||
android:id="@+id/emptyStateContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="32dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_centerInParent="true">
|
||||
android:paddingBottom="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="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:text="@string/activity_home_empty_state_message" />
|
||||
android:textSize="@dimen/medium_font_size" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/createNewPrivateChatButton"
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
|
@ -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>
|
@ -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() }
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,20 @@
|
||||
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 com.annimon.stream.Stream;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.session.libsession.messaging.utilities.Data;
|
||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
|
||||
@ -17,14 +25,6 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
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 {
|
||||
|
||||
private static final JsonDataSerializer serializer = new JsonDataSerializer();
|
||||
|
@ -1,7 +1,8 @@
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.provider.ContactsContract;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
@ -10,45 +11,14 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.session.libsession.utilities.Address;
|
||||
|
||||
import static android.provider.ContactsContract.Intents.Insert.EMAIL;
|
||||
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;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.RecipientExporter;
|
||||
|
||||
//FIXME AC: This test group is outdated.
|
||||
@Ignore("This test group uses outdated instrumentation and needs a migration to modern tools.")
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
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
|
||||
public void asAddContactIntent_with_neither_email_nor_phone() {
|
||||
RecipientExporter exporter = RecipientExporter.export(givenRecipient("Bob", mock(Address.class)));
|
||||
@ -64,19 +34,4 @@ public final class RecipientExporterTest extends TestCase {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util.dynamiclanguage;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.session.libsession.utilities.dynamiclanguage.LanguageString;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
@ -7,6 +7,8 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||
|
||||
import network.loki.messenger.BuildConfig;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
@ -4,9 +4,9 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
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 "com.google.gms:google-services:4.3.4"
|
||||
classpath "com.google.gms:google-services:4.3.10"
|
||||
classpath files('libs/gradle-witness.jar')
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,15 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
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
|
||||
curve25519Version=0.5.0
|
||||
protobufVersion=2.5.0
|
||||
okhttpVersion=3.12.1
|
||||
jacksonDatabindVersion=2.9.8
|
||||
jacksonDatabindVersion=2.9.8
|
||||
mockitoKotlinVersion=4.0.0
|
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,6 @@
|
||||
#Thu Dec 30 07:09:53 SAST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
@ -23,12 +23,13 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
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.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.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.annimon:stream:1.1.8'
|
||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||
@ -39,7 +40,7 @@ dependencies {
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$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"
|
||||
testImplementation "junit:junit:3.8.2"
|
||||
testImplementation "org.assertj:assertj-core:1.7.1"
|
||||
|
@ -1,11 +1,18 @@
|
||||
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.Factory.UNLIMITED
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.util.*
|
||||
import java.util.Timer
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@ -25,7 +32,10 @@ class JobQueue : JobDelegate {
|
||||
|
||||
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) {
|
||||
if (!isActive) break
|
||||
job.delegate = this@JobQueue
|
||||
@ -74,7 +84,7 @@ class JobQueue : JobDelegate {
|
||||
|
||||
fun add(job: 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) {
|
||||
@ -97,7 +107,7 @@ class JobQueue : JobDelegate {
|
||||
Log.e("Loki","tried to re-queue pending/in-progress job")
|
||||
return
|
||||
}
|
||||
queue.offer(job)
|
||||
queue.trySend(job)
|
||||
Log.d("Loki", "resumed pending send message $id")
|
||||
}
|
||||
|
||||
@ -114,7 +124,7 @@ class JobQueue : JobDelegate {
|
||||
}
|
||||
pendingJobs.sortedBy { it.id }.forEach { job ->
|
||||
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}).")
|
||||
timer.schedule(delay = retryInterval) {
|
||||
Log.i("Loki", "Retrying ${job::class.simpleName}.")
|
||||
queue.offer(job)
|
||||
queue.trySend(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user