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
98 changed files with 3579 additions and 2365 deletions

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