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

* refactor: Migrate home screen to data binding

* Add view binding

* Migrate ConversationView to view binding

* Migrate ConversationActivityV2 to view binding

* View model refactor

* Move more functionality to the view model

* Add ui state events flow

* Update conversation item bindings

* Update profile picture view bindings

* Replace Kotlin synthetics with view bindings

* Fix qr code fragment binding and optimize imports

* View binding refactors

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

* Add conversation repository

* Migrate remaining TextSecurePreferences functions into the interface

* Add unit conversation unit tests

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,9 +4,7 @@ import android.content.Context
import android.database.Cursor
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import kotlinx.android.synthetic.main.view_visible_message.view.*
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,13 +4,14 @@ import android.content.Context
import android.content.res.Resources
import android.net.Uri
import android.text.InputType
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent
import android.widget.RelativeLayout
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_input_bar.view.*
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewInputBarBinding
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
@ -27,6 +28,7 @@ import kotlin.math.max
import kotlin.math.roundToInt
class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate {
private lateinit var binding: ViewInputBarBinding
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private val vMargin by lazy { toDp(4, resources) }
private val minHeight by lazy { toPx(56, resources) }
@ -39,8 +41,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
set(value) { field = value; showOrHideInputIfNeeded() }
var text: String
get() { return inputBarEditText.text?.toString() ?: "" }
set(value) { inputBarEditText.setText(value) }
get() { return binding.inputBarEditText.text?.toString() ?: "" }
set(value) { binding.inputBarEditText.setText(value) }
val attachmentButtonsContainerHeight: Int
get() = binding.attachmentsButtonContainer.height
private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) }
private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone) }
@ -52,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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,8 +11,8 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_link_preview.view.*
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewLinkPreviewBinding
import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.util.UiModeUtilities
class LinkPreviewView : LinearLayout {
private lateinit var binding: ViewLinkPreviewBinding
private val cornerMask by lazy { CornerMask(this) }
private var url: String? = null
lateinit var bodyTextView: TextView
@ -33,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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,13 +6,12 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.*
import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.util.UiModeUtilities
public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
private lateinit var binding: FragmentConversationBottomSheetBinding
//FIXME AC: Supplying a threadRecord directly into the field from an activity
// is not the best idea. It doesn't survive configuration change.
// We should be dealing with IDs and all sorts of serializable data instead
@ -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() {

View File

@ -11,8 +11,8 @@ import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.view_conversation.view.*
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewConversationBinding
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
import org.thoughtcrime.securesms.database.RecipientDatabase
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
class ConversationView : LinearLayout {
private lateinit var binding: ViewConversationBinding
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
var thread: ThreadRecord? = null
@ -31,7 +32,7 @@ class ConversationView : LinearLayout {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_conversation, this)
binding = ViewConversationBinding.inflate(LayoutInflater.from(context), this, true)
layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
}
// endregion
@ -39,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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,11 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.*
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
@ -15,9 +19,9 @@ import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.activity_settings.*
import network.loki.messenger.BuildConfig
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySettingsBinding
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.ui.alwaysUi
@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,20 @@
package org.thoughtcrime.securesms.jobs;
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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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