mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-09 08:21:48 +00:00
Bug fixes and provide conversation tooltips (#851)
* refactor: removing unused strings and changing session header dimensions * refactor: remove bodyTextView from LinkPreviewView.kt and changing header image colours * fix: path layout is aligned, global search input should always prompt soft input on open * fix: unread count and scroll to bottom button visibility properly taking into account adapter item count and RecyclerView.NO_POSITION fixes #848 * fix: crash on error toast for failing to share logs * feat: conversation tooltips in NewConversationButtonSetView.kt * fix: UI issue for conversation action bar cutting off lower than baseline characters fixes #839 * refactor (wip): replacing bindings with nullable types to try prevent mystery bug * refactor: use the nullable bindings for ConversationActivityV2.kt and remove inputBarHeightChanged * fix: remove recipient listener on destroy * build: add latest strings and increase build
This commit is contained in:
@@ -137,7 +137,6 @@ import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sqrt
|
||||
@@ -151,8 +150,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener,
|
||||
SearchBottomBar.EventListener, VoiceMessageViewDelegate {
|
||||
|
||||
private lateinit var binding: ActivityConversationV2Binding
|
||||
private lateinit var actionBarBinding: ActivityConversationV2ActionBarBinding
|
||||
private var binding: ActivityConversationV2Binding? = null
|
||||
private var actionBarBinding: ActivityConversationV2ActionBarBinding? = null
|
||||
|
||||
@Inject lateinit var textSecurePreferences: TextSecurePreferences
|
||||
@Inject lateinit var threadDb: ThreadDatabase
|
||||
@@ -202,12 +201,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
private val isScrolledToBottom: Boolean
|
||||
get() {
|
||||
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
val position = layoutManager?.findFirstCompletelyVisibleItemPosition() ?: 0
|
||||
return position == 0
|
||||
}
|
||||
|
||||
private val layoutManager: LinearLayoutManager
|
||||
get() { return binding.conversationRecyclerView.layoutManager as LinearLayoutManager }
|
||||
private val layoutManager: LinearLayoutManager?
|
||||
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
|
||||
|
||||
private val seed by lazy {
|
||||
var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED)
|
||||
@@ -277,7 +276,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityConversationV2Binding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setContentView(binding!!.root)
|
||||
// messageIdToScroll
|
||||
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
|
||||
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
|
||||
@@ -292,12 +291,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
setUpLinkPreviewObserver()
|
||||
restoreDraftIfNeeded()
|
||||
setUpUiStateObserver()
|
||||
binding.scrollToBottomButton.setOnClickListener {
|
||||
val layoutManager = binding.conversationRecyclerView.layoutManager ?: return@setOnClickListener
|
||||
binding!!.scrollToBottomButton.setOnClickListener {
|
||||
val layoutManager = binding?.conversationRecyclerView?.layoutManager ?: return@setOnClickListener
|
||||
if (layoutManager.isSmoothScrolling) {
|
||||
binding.conversationRecyclerView.scrollToPosition(0)
|
||||
binding?.conversationRecyclerView?.scrollToPosition(0)
|
||||
} else {
|
||||
binding.conversationRecyclerView.smoothScrollToPosition(0)
|
||||
binding?.conversationRecyclerView?.smoothScrollToPosition(0)
|
||||
}
|
||||
}
|
||||
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
|
||||
@@ -307,7 +306,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
updateSubtitle()
|
||||
getLatestOpenGroupInfoIfNeeded()
|
||||
setUpBlockedBanner()
|
||||
binding.searchBottomBar.setEventListener(this)
|
||||
binding!!.searchBottomBar.setEventListener(this)
|
||||
setUpSearchResultObserver()
|
||||
scrollToFirstUnreadMessageIfNeeded()
|
||||
showOrHideInputIfNeeded()
|
||||
@@ -347,10 +346,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
baseDialog.show(supportFragmentManager, tag)
|
||||
}
|
||||
|
||||
// called from onCreate
|
||||
private fun setUpRecyclerView() {
|
||||
binding.conversationRecyclerView.adapter = adapter
|
||||
binding!!.conversationRecyclerView.adapter = adapter
|
||||
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
|
||||
binding.conversationRecyclerView.layoutManager = layoutManager
|
||||
binding!!.conversationRecyclerView.layoutManager = layoutManager
|
||||
// Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will)
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
@@ -373,7 +373,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
adapter.changeCursor(null)
|
||||
}
|
||||
})
|
||||
binding.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
handleRecyclerViewScrolled()
|
||||
@@ -381,50 +381,53 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
})
|
||||
}
|
||||
|
||||
// called from onCreate
|
||||
private fun setUpToolBar() {
|
||||
val actionBar = supportActionBar!!
|
||||
actionBarBinding = ActivityConversationV2ActionBarBinding.inflate(layoutInflater)
|
||||
actionBar.title = ""
|
||||
actionBar.customView = actionBarBinding.root
|
||||
actionBar.customView = actionBarBinding!!.root
|
||||
actionBar.setDisplayShowCustomEnabled(true)
|
||||
actionBarBinding.conversationTitleView.text = viewModel.recipient.toShortString()
|
||||
actionBarBinding!!.conversationTitleView.text = viewModel.recipient.toShortString()
|
||||
@DimenRes val sizeID: Int = if (viewModel.recipient.isClosedGroupRecipient) {
|
||||
R.dimen.medium_profile_picture_size
|
||||
} else {
|
||||
R.dimen.small_profile_picture_size
|
||||
}
|
||||
val size = resources.getDimension(sizeID).roundToInt()
|
||||
actionBarBinding.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
|
||||
actionBarBinding.profilePictureView.glide = glide
|
||||
actionBarBinding!!.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
|
||||
actionBarBinding!!.profilePictureView.glide = glide
|
||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
|
||||
actionBarBinding.profilePictureView.update(viewModel.recipient)
|
||||
actionBarBinding!!.profilePictureView.update(viewModel.recipient)
|
||||
}
|
||||
|
||||
// called from onCreate
|
||||
private fun setUpInputBar() {
|
||||
binding.inputBar.delegate = this
|
||||
binding.inputBarRecordingView.delegate = this
|
||||
binding!!.inputBar.delegate = this
|
||||
binding!!.inputBarRecordingView.delegate = this
|
||||
// GIF button
|
||||
binding.gifButtonContainer.addView(gifButton)
|
||||
binding!!.gifButtonContainer.addView(gifButton)
|
||||
gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
gifButton.onUp = { showGIFPicker() }
|
||||
gifButton.snIsEnabled = false
|
||||
// Document button
|
||||
binding.documentButtonContainer.addView(documentButton)
|
||||
binding!!.documentButtonContainer.addView(documentButton)
|
||||
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
documentButton.onUp = { showDocumentPicker() }
|
||||
documentButton.snIsEnabled = false
|
||||
// Library button
|
||||
binding.libraryButtonContainer.addView(libraryButton)
|
||||
binding!!.libraryButtonContainer.addView(libraryButton)
|
||||
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
libraryButton.onUp = { pickFromLibrary() }
|
||||
libraryButton.snIsEnabled = false
|
||||
// Camera button
|
||||
binding.cameraButtonContainer.addView(cameraButton)
|
||||
binding!!.cameraButtonContainer.addView(cameraButton)
|
||||
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
||||
cameraButton.onUp = { showCamera() }
|
||||
cameraButton.snIsEnabled = false
|
||||
}
|
||||
|
||||
// called from onCreate
|
||||
private fun restoreDraftIfNeeded() {
|
||||
val mediaURI = intent.data
|
||||
val mediaType = AttachmentManager.MediaType.from(intent.type)
|
||||
@@ -448,33 +451,34 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
} else if (intent.hasExtra(Intent.EXTRA_TEXT)) {
|
||||
val dataTextExtra = intent.getCharSequenceExtra(Intent.EXTRA_TEXT) ?: ""
|
||||
binding.inputBar.text = dataTextExtra.toString()
|
||||
binding!!.inputBar.text = dataTextExtra.toString()
|
||||
} else {
|
||||
viewModel.getDraft()?.let { text ->
|
||||
binding.inputBar.text = text
|
||||
binding!!.inputBar.text = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addOpenGroupGuidelinesIfNeeded(isOxenHostedOpenGroup: Boolean) {
|
||||
if (!isOxenHostedOpenGroup) { return }
|
||||
binding.openGroupGuidelinesView.visibility = View.VISIBLE
|
||||
val recyclerViewLayoutParams = binding.conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams
|
||||
binding?.openGroupGuidelinesView?.visibility = View.VISIBLE
|
||||
val recyclerViewLayoutParams = binding?.conversationRecyclerView?.layoutParams as RelativeLayout.LayoutParams? ?: return
|
||||
recyclerViewLayoutParams.topMargin = toPx(57, resources) // The height of the open group guidelines view is hardcoded to this
|
||||
binding.conversationRecyclerView.layoutParams = recyclerViewLayoutParams
|
||||
binding?.conversationRecyclerView?.layoutParams = recyclerViewLayoutParams
|
||||
}
|
||||
|
||||
// called from onCreate
|
||||
private fun setUpTypingObserver() {
|
||||
ApplicationContext.getInstance(this).typingStatusRepository.getTypists(viewModel.threadId).observe(this) { state ->
|
||||
val recipients = if (state != null) state.typists else listOf()
|
||||
// FIXME: Also checking isScrolledToBottom is a quick fix for an issue where the
|
||||
// typing indicator overlays the recycler view when scrolled up
|
||||
binding.typingIndicatorViewContainer.isVisible = recipients.isNotEmpty() && isScrolledToBottom
|
||||
binding.typingIndicatorViewContainer.setTypists(recipients)
|
||||
inputBarHeightChanged(binding.inputBar.height)
|
||||
val viewContainer = binding?.typingIndicatorViewContainer ?: return@observe
|
||||
viewContainer.isVisible = recipients.isNotEmpty() && isScrolledToBottom
|
||||
viewContainer.setTypists(recipients)
|
||||
}
|
||||
if (textSecurePreferences.isTypingIndicatorsEnabled()) {
|
||||
binding.inputBar.addTextChangedListener(object : SimpleTextWatcher() {
|
||||
binding!!.inputBar.addTextChangedListener(object : SimpleTextWatcher() {
|
||||
|
||||
override fun onTextChanged(text: String?) {
|
||||
ApplicationContext.getInstance(this@ConversationActivityV2).typingStatusSender.onTypingStarted(viewModel.threadId)
|
||||
@@ -487,19 +491,24 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
viewModel.recipient.addListener(this)
|
||||
}
|
||||
|
||||
private fun tearDownRecipientObserver() {
|
||||
viewModel.recipient.removeListener(this)
|
||||
}
|
||||
|
||||
private fun getLatestOpenGroupInfoIfNeeded() {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) ?: return
|
||||
OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() }
|
||||
}
|
||||
|
||||
// called from onCreate
|
||||
private fun setUpBlockedBanner() {
|
||||
if (viewModel.recipient.isGroupRecipient) { return }
|
||||
val sessionID = viewModel.recipient.address.toString()
|
||||
val contact = sessionContactDb.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
binding.blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
|
||||
binding.blockedBanner.isVisible = viewModel.recipient.isBlocked
|
||||
binding.blockedBanner.setOnClickListener { viewModel.unblock() }
|
||||
binding?.blockedBannerTextView?.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
|
||||
binding?.blockedBanner?.isVisible = viewModel.recipient.isBlocked
|
||||
binding?.blockedBanner?.setOnClickListener { viewModel.unblock() }
|
||||
}
|
||||
|
||||
private fun setUpLinkPreviewObserver() {
|
||||
@@ -510,13 +519,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
if (previewState == null) return@observe
|
||||
when {
|
||||
previewState.isLoading -> {
|
||||
binding.inputBar.draftLinkPreview()
|
||||
binding?.inputBar?.draftLinkPreview()
|
||||
}
|
||||
previewState.linkPreview.isPresent -> {
|
||||
binding.inputBar.updateLinkPreviewDraft(glide, previewState.linkPreview.get())
|
||||
binding?.inputBar?.updateLinkPreviewDraft(glide, previewState.linkPreview.get())
|
||||
}
|
||||
else -> {
|
||||
binding.inputBar.cancelLinkPreviewDraft()
|
||||
binding?.inputBar?.cancelLinkPreviewDraft()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -538,7 +547,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val lastSeenTimestamp = threadDb.getLastSeenAndHasSent(viewModel.threadId).first()
|
||||
val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return
|
||||
if (lastSeenItemPosition <= 3) { return }
|
||||
binding.conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
|
||||
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -548,8 +557,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
viewModel.saveDraft(binding.inputBar.text.trim())
|
||||
viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "")
|
||||
tearDownRecipientObserver()
|
||||
super.onDestroy()
|
||||
binding = null
|
||||
actionBarBinding = null
|
||||
}
|
||||
// endregion
|
||||
|
||||
@@ -557,11 +569,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
override fun onModified(recipient: Recipient) {
|
||||
runOnUiThread {
|
||||
if (viewModel.recipient.isContactRecipient) {
|
||||
binding.blockedBanner.isVisible = viewModel.recipient.isBlocked
|
||||
binding?.blockedBanner?.isVisible = viewModel.recipient.isBlocked
|
||||
}
|
||||
updateSubtitle()
|
||||
showOrHideInputIfNeeded()
|
||||
actionBarBinding.profilePictureView.update(recipient)
|
||||
actionBarBinding?.profilePictureView?.update(recipient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,18 +581,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
if (viewModel.recipient.isClosedGroupRecipient) {
|
||||
val group = groupDb.getGroup(viewModel.recipient.address.toGroupString()).orNull()
|
||||
val isActive = (group?.isActive == true)
|
||||
binding.inputBar.showInput = isActive
|
||||
binding?.inputBar?.showInput = isActive
|
||||
} else {
|
||||
binding.inputBar.showInput = true
|
||||
binding?.inputBar?.showInput = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun inputBarHeightChanged(newValue: Int) {
|
||||
}
|
||||
|
||||
override fun inputBarEditTextContentChanged(newContent: CharSequence) {
|
||||
val inputBarText = binding?.inputBar?.text ?: return // TODO check if we should be referencing newContent here instead
|
||||
if (textSecurePreferences.isLinkPreviewsEnabled()) {
|
||||
linkPreviewViewModel.onTextChanged(this, binding.inputBar.text, 0, 0)
|
||||
linkPreviewViewModel.onTextChanged(this, inputBarText, 0, 0)
|
||||
}
|
||||
showOrHideMentionCandidatesIfNeeded(newContent)
|
||||
if (LinkPreviewUtil.findWhitelistedUrls(newContent.toString()).isNotEmpty()
|
||||
@@ -588,7 +598,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
LinkPreviewDialog {
|
||||
setUpLinkPreviewObserver()
|
||||
linkPreviewViewModel.onEnabled()
|
||||
linkPreviewViewModel.onTextChanged(this, binding.inputBar.text, 0, 0)
|
||||
linkPreviewViewModel.onTextChanged(this, inputBarText, 0, 0)
|
||||
}.show(supportFragmentManager, "Link Preview Dialog")
|
||||
textSecurePreferences.setHasSeenLinkPreviewSuggestionDialog()
|
||||
}
|
||||
@@ -630,12 +640,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
private fun showOrUpdateMentionCandidatesIfNeeded(query: String = "") {
|
||||
val additionalContentContainer = binding?.additionalContentContainer ?: return
|
||||
if (!isShowingMentionCandidatesView) {
|
||||
binding.additionalContentContainer.removeAllViews()
|
||||
additionalContentContainer.removeAllViews()
|
||||
val view = MentionCandidatesView(this)
|
||||
view.glide = glide
|
||||
view.onCandidateSelected = { handleMentionSelected(it) }
|
||||
binding.additionalContentContainer.addView(view)
|
||||
additionalContentContainer.addView(view)
|
||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
|
||||
this.mentionCandidatesView = view
|
||||
view.show(candidates, viewModel.threadId)
|
||||
@@ -653,7 +664,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
animation.duration = 250L
|
||||
animation.addUpdateListener { animator ->
|
||||
mentionCandidatesView.alpha = animator.animatedValue as Float
|
||||
if (animator.animatedFraction == 1.0f) { binding.additionalContentContainer.removeAllViews() }
|
||||
if (animator.animatedFraction == 1.0f) { binding?.additionalContentContainer?.removeAllViews() }
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
@@ -662,7 +673,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
override fun toggleAttachmentOptions() {
|
||||
val targetAlpha = if (isShowingAttachmentOptions) 0.0f else 1.0f
|
||||
val allButtonContainers = listOf( binding.cameraButtonContainer, binding.libraryButtonContainer, binding.documentButtonContainer, binding.gifButtonContainer)
|
||||
val allButtonContainers = listOfNotNull(
|
||||
binding?.cameraButtonContainer,
|
||||
binding?.libraryButtonContainer,
|
||||
binding?.documentButtonContainer,
|
||||
binding?.gifButtonContainer
|
||||
)
|
||||
val isReversed = isShowingAttachmentOptions // Run the animation in reverse
|
||||
val count = allButtonContainers.size
|
||||
allButtonContainers.indices.forEach { index ->
|
||||
@@ -681,39 +697,41 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun showVoiceMessageUI() {
|
||||
binding.inputBarRecordingView.show()
|
||||
binding.inputBar.alpha = 0.0f
|
||||
binding?.inputBarRecordingView?.show()
|
||||
binding?.inputBar?.alpha = 0.0f
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||
animation.duration = 250L
|
||||
animation.addUpdateListener { animator ->
|
||||
binding.inputBar.alpha = animator.animatedValue as Float
|
||||
binding?.inputBar?.alpha = animator.animatedValue as Float
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
private fun expandVoiceMessageLockView() {
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), binding.inputBarRecordingView.lockView.scaleX, 1.10f)
|
||||
val lockView = binding?.inputBarRecordingView?.lockView ?: return
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.10f)
|
||||
animation.duration = 250L
|
||||
animation.addUpdateListener { animator ->
|
||||
binding.inputBarRecordingView.lockView.scaleX = animator.animatedValue as Float
|
||||
binding.inputBarRecordingView.lockView.scaleY = animator.animatedValue as Float
|
||||
lockView.scaleX = animator.animatedValue as Float
|
||||
lockView.scaleY = animator.animatedValue as Float
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
private fun collapseVoiceMessageLockView() {
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), binding.inputBarRecordingView.lockView.scaleX, 1.0f)
|
||||
val lockView = binding?.inputBarRecordingView?.lockView ?: return
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.0f)
|
||||
animation.duration = 250L
|
||||
animation.addUpdateListener { animator ->
|
||||
binding.inputBarRecordingView.lockView.scaleX = animator.animatedValue as Float
|
||||
binding.inputBarRecordingView.lockView.scaleY = animator.animatedValue as Float
|
||||
lockView.scaleX = animator.animatedValue as Float
|
||||
lockView.scaleY = animator.animatedValue as Float
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
private fun hideVoiceMessageUI() {
|
||||
val chevronImageView = binding.inputBarRecordingView.chevronImageView
|
||||
val slideToCancelTextView = binding.inputBarRecordingView.slideToCancelTextView
|
||||
val chevronImageView = binding?.inputBarRecordingView?.chevronImageView ?: return
|
||||
val slideToCancelTextView = binding?.inputBarRecordingView?.slideToCancelTextView ?: return
|
||||
listOf( chevronImageView, slideToCancelTextView ).forEach { view ->
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), view.translationX, 0.0f)
|
||||
animation.duration = 250L
|
||||
@@ -722,15 +740,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
binding.inputBarRecordingView.hide()
|
||||
binding?.inputBarRecordingView?.hide()
|
||||
}
|
||||
|
||||
override fun handleVoiceMessageUIHidden() {
|
||||
binding.inputBar.alpha = 1.0f
|
||||
val inputBar = binding?.inputBar ?: return
|
||||
inputBar.alpha = 1.0f
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
|
||||
animation.duration = 250L
|
||||
animation.addUpdateListener { animator ->
|
||||
binding.inputBar.alpha = animator.animatedValue as Float
|
||||
inputBar.alpha = animator.animatedValue as Float
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
@@ -738,18 +757,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
private fun handleRecyclerViewScrolled() {
|
||||
// FIXME: Checking isScrolledToBottom is a quick fix for an issue where the
|
||||
// typing indicator overlays the recycler view when scrolled up
|
||||
val binding = binding ?: return
|
||||
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
|
||||
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
|
||||
val isTypingIndicatorVisibleAfter = binding.typingIndicatorViewContainer.isVisible
|
||||
if (isTypingIndicatorVisibleAfter != wasTypingIndicatorVisibleBefore) {
|
||||
inputBarHeightChanged(binding.inputBar.height)
|
||||
}
|
||||
binding.scrollToBottomButton.isVisible = !isScrolledToBottom
|
||||
unreadCount = min(unreadCount, layoutManager.findFirstVisibleItemPosition())
|
||||
binding.typingIndicatorViewContainer.isVisible
|
||||
binding.scrollToBottomButton.isVisible = !isScrolledToBottom && adapter.itemCount > 0
|
||||
val firstVisiblePosition = layoutManager?.findFirstVisibleItemPosition() ?: -1
|
||||
unreadCount = min(unreadCount, firstVisiblePosition).coerceAtLeast(0)
|
||||
updateUnreadCountIndicator()
|
||||
}
|
||||
|
||||
private fun updateUnreadCountIndicator() {
|
||||
val binding = binding ?: return
|
||||
val formattedUnreadCount = if (unreadCount < 10000) unreadCount.toString() else "9999+"
|
||||
binding.unreadCountTextView.text = formattedUnreadCount
|
||||
val textSize = if (unreadCount < 10000) 12.0f else 9.0f
|
||||
@@ -759,6 +778,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
private fun updateSubtitle() {
|
||||
val actionBarBinding = actionBarBinding ?: return
|
||||
actionBarBinding.muteIconImageView.isVisible = viewModel.recipient.isMuted
|
||||
actionBarBinding.conversationSubtitleView.isVisible = true
|
||||
if (viewModel.recipient.isMuted) {
|
||||
@@ -816,7 +836,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
// `position` is the adapter position; not the visual position
|
||||
private fun handleSwipeToReply(message: MessageRecord, position: Int) {
|
||||
binding.inputBar.draftQuote(viewModel.recipient, message, glide)
|
||||
binding?.inputBar?.draftQuote(viewModel.recipient, message, glide)
|
||||
}
|
||||
|
||||
// `position` is the adapter position; not the visual position
|
||||
@@ -840,8 +860,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
override fun onMicrophoneButtonMove(event: MotionEvent) {
|
||||
val rawX = event.rawX
|
||||
val chevronImageView = binding.inputBarRecordingView.chevronImageView
|
||||
val slideToCancelTextView = binding.inputBarRecordingView.slideToCancelTextView
|
||||
val chevronImageView = binding?.inputBarRecordingView?.chevronImageView ?: return
|
||||
val slideToCancelTextView = binding?.inputBarRecordingView?.slideToCancelTextView ?: return
|
||||
if (rawX < screenWidth / 2) {
|
||||
val translationX = rawX - screenWidth / 2
|
||||
val sign = -1.0f
|
||||
@@ -876,9 +896,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val x = event.rawX.roundToInt()
|
||||
val y = event.rawY.roundToInt()
|
||||
if (isValidLockViewLocation(x, y)) {
|
||||
binding.inputBarRecordingView.lock()
|
||||
binding?.inputBarRecordingView?.lock()
|
||||
} else {
|
||||
val recordButtonOverlay = binding.inputBarRecordingView.recordButtonOverlay
|
||||
val recordButtonOverlay = binding?.inputBarRecordingView?.recordButtonOverlay ?: return
|
||||
val location = IntArray(2) { 0 }
|
||||
recordButtonOverlay.getLocationOnScreen(location)
|
||||
val hitRect = Rect(location[0], location[1], location[0] + recordButtonOverlay.width, location[1] + recordButtonOverlay.height)
|
||||
@@ -893,6 +913,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
private fun isValidLockViewLocation(x: Int, y: Int): Boolean {
|
||||
// We can be anywhere above the lock view and a bit to the side of it (at most `lockViewHitMargin`
|
||||
// to the side)
|
||||
val binding = binding ?: return false
|
||||
val lockViewLocation = IntArray(2) { 0 }
|
||||
binding.inputBarRecordingView.lockView.getLocationOnScreen(lockViewLocation)
|
||||
val hitRect = Rect(lockViewLocation[0] - lockViewHitMargin, 0,
|
||||
@@ -901,6 +922,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
private fun handleMentionSelected(mention: Mention) {
|
||||
val binding = binding ?: return
|
||||
if (currentMentionStartIndex == -1) { return }
|
||||
mentions.add(mention)
|
||||
val previousText = binding.inputBar.text
|
||||
@@ -914,13 +936,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
override fun scrollToMessageIfPossible(timestamp: Long) {
|
||||
val lastSeenItemPosition = adapter.getItemPositionForTimestamp(timestamp) ?: return
|
||||
binding.conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
|
||||
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
|
||||
}
|
||||
|
||||
override fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) {
|
||||
if (indexInAdapter < 0 || indexInAdapter >= adapter.itemCount) { return }
|
||||
val viewHolder = binding.conversationRecyclerView.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder
|
||||
viewHolder?.view?.playVoiceMessage()
|
||||
val viewHolder = binding?.conversationRecyclerView?.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder ?: return
|
||||
viewHolder.view.playVoiceMessage()
|
||||
}
|
||||
|
||||
override fun sendMessage() {
|
||||
@@ -928,6 +950,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
BlockedDialog(viewModel.recipient).show(supportFragmentManager, "Blocked Dialog")
|
||||
return
|
||||
}
|
||||
val binding = binding ?: return
|
||||
if (binding.inputBar.linkPreview != null || binding.inputBar.quote != null) {
|
||||
sendAttachments(listOf(), getMessageBody(), binding.inputBar.quote, binding.inputBar.linkPreview)
|
||||
} else {
|
||||
@@ -954,9 +977,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
message.text = text
|
||||
val outgoingTextMessage = OutgoingTextMessage.from(message, viewModel.recipient)
|
||||
// Clear the input bar
|
||||
binding.inputBar.text = ""
|
||||
binding.inputBar.cancelQuoteDraft()
|
||||
binding.inputBar.cancelLinkPreviewDraft()
|
||||
binding?.inputBar?.text = ""
|
||||
binding?.inputBar?.cancelQuoteDraft()
|
||||
binding?.inputBar?.cancelLinkPreviewDraft()
|
||||
// Clear mentions
|
||||
previousText = ""
|
||||
currentMentionStartIndex = -1
|
||||
@@ -981,9 +1004,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
val outgoingTextMessage = OutgoingMediaMessage.from(message, viewModel.recipient, attachments, quote, linkPreview)
|
||||
// Clear the input bar
|
||||
binding.inputBar.text = ""
|
||||
binding.inputBar.cancelQuoteDraft()
|
||||
binding.inputBar.cancelLinkPreviewDraft()
|
||||
binding?.inputBar?.text = ""
|
||||
binding?.inputBar?.cancelQuoteDraft()
|
||||
binding?.inputBar?.cancelLinkPreviewDraft()
|
||||
// Clear mentions
|
||||
previousText = ""
|
||||
currentMentionStartIndex = -1
|
||||
@@ -1025,7 +1048,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
private fun pickFromLibrary() {
|
||||
AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, viewModel.recipient, binding.inputBar.text.trim())
|
||||
binding?.inputBar?.text?.trim()?.let { text ->
|
||||
AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, viewModel.recipient, text)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCamera() {
|
||||
@@ -1356,7 +1381,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun reply(messages: Set<MessageRecord>) {
|
||||
binding.inputBar.draftQuote(viewModel.recipient, messages.first(), glide)
|
||||
binding?.inputBar?.draftQuote(viewModel.recipient, messages.first(), glide)
|
||||
endActionMode()
|
||||
}
|
||||
|
||||
@@ -1376,7 +1401,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
// region General
|
||||
private fun getMessageBody(): String {
|
||||
var result = binding.inputBar.text.trim()
|
||||
var result = binding?.inputBar?.text?.trim() ?: return ""
|
||||
for (mention in mentions) {
|
||||
try {
|
||||
val startIndex = result.indexOf("@" + mention.displayName)
|
||||
@@ -1396,31 +1421,32 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
if (result == null) return@Observer
|
||||
if (result.getResults().isNotEmpty()) {
|
||||
result.getResults()[result.position]?.let {
|
||||
jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs, Runnable { searchViewModel.onMissingResult() })
|
||||
jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs,
|
||||
{ searchViewModel.onMissingResult() })
|
||||
}
|
||||
}
|
||||
binding.searchBottomBar.setData(result.position, result.getResults().size)
|
||||
binding?.searchBottomBar?.setData(result.position, result.getResults().size)
|
||||
})
|
||||
}
|
||||
|
||||
fun onSearchOpened() {
|
||||
searchViewModel.onSearchOpened()
|
||||
binding.searchBottomBar.visibility = View.VISIBLE
|
||||
binding.searchBottomBar.setData(0, 0)
|
||||
binding.inputBar.visibility = View.GONE
|
||||
binding?.searchBottomBar?.visibility = View.VISIBLE
|
||||
binding?.searchBottomBar?.setData(0, 0)
|
||||
binding?.inputBar?.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun onSearchClosed() {
|
||||
searchViewModel.onSearchClosed()
|
||||
binding.searchBottomBar.visibility = View.GONE
|
||||
binding.inputBar.visibility = View.VISIBLE
|
||||
binding?.searchBottomBar?.visibility = View.GONE
|
||||
binding?.inputBar?.visibility = View.VISIBLE
|
||||
adapter.onSearchQueryUpdated(null)
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
fun onSearchQueryUpdated(query: String) {
|
||||
searchViewModel.onQueryUpdated(query, viewModel.threadId)
|
||||
binding.searchBottomBar.showLoading()
|
||||
binding?.searchBottomBar?.showLoading()
|
||||
adapter.onSearchQueryUpdated(query)
|
||||
}
|
||||
|
||||
@@ -1440,7 +1466,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
private fun moveToMessagePosition(position: Int, onMessageNotFound: Runnable?) {
|
||||
if (position >= 0) {
|
||||
binding.conversationRecyclerView.scrollToPosition(position)
|
||||
binding?.conversationRecyclerView?.scrollToPosition(position)
|
||||
} else {
|
||||
onMessageNotFound?.run()
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.toDp
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate {
|
||||
private lateinit var binding: ViewInputBarBinding
|
||||
@@ -176,7 +174,6 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
|
||||
interface InputBarDelegate {
|
||||
|
||||
fun inputBarHeightChanged(newValue: Int)
|
||||
fun inputBarEditTextContentChanged(newContent: CharSequence)
|
||||
fun toggleAttachmentOptions()
|
||||
fun showVoiceMessageUI()
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
@@ -16,7 +15,6 @@ 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
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
@@ -26,7 +24,6 @@ class LinkPreviewView : LinearLayout {
|
||||
private lateinit var binding: ViewLinkPreviewBinding
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var url: String? = null
|
||||
lateinit var bodyTextView: TextView
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
@@ -88,11 +85,6 @@ class LinkPreviewView : LinearLayout {
|
||||
openURL()
|
||||
return
|
||||
}
|
||||
// intersectedModalSpans should only be a list of one item
|
||||
val hitSpans = bodyTextView.getIntersectedModalSpans(hitRect)
|
||||
hitSpans.iterator().forEach { span ->
|
||||
span.onClick(bodyTextView)
|
||||
}
|
||||
}
|
||||
|
||||
fun openURL() {
|
||||
|
||||
@@ -43,7 +43,7 @@ import org.thoughtcrime.securesms.util.SearchUtil
|
||||
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 {
|
||||
@@ -93,7 +93,6 @@ class VisibleMessageContentView : LinearLayout {
|
||||
binding.quoteView.isVisible = message is MmsMessageRecord && message.quote != null
|
||||
|
||||
binding.linkPreviewView.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty()
|
||||
binding.linkPreviewView.bodyTextView = binding.bodyTextView
|
||||
|
||||
val linkPreviewLayout = binding.linkPreviewView.layoutParams
|
||||
linkPreviewLayout.width = if (mediaThumbnailMessage) 0 else ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
|
||||
@@ -270,7 +270,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
|
||||
private fun setupHeaderImage() {
|
||||
val isDayUiMode = UiModeUtilities.isDayUiMode(this)
|
||||
val headerTint = if (isDayUiMode) R.color.black else R.color.accent
|
||||
val headerTint = if (isDayUiMode) R.color.black else R.color.white
|
||||
binding.sessionHeaderImage.setColorFilter(getColor(headerTint))
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,29 @@ import android.content.res.ColorStateList
|
||||
import android.graphics.PointF
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.util.*
|
||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.util.NewConversationButtonImageView
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.util.animateSizeChange
|
||||
import org.thoughtcrime.securesms.util.contains
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.distanceTo
|
||||
import org.thoughtcrime.securesms.util.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.isAbove
|
||||
import org.thoughtcrime.securesms.util.isLeftOf
|
||||
import org.thoughtcrime.securesms.util.isRightOf
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
|
||||
class NewConversationButtonSetView : RelativeLayout {
|
||||
private var expandedButton: Button? = null
|
||||
@@ -26,9 +40,27 @@ class NewConversationButtonSetView : RelativeLayout {
|
||||
|
||||
// region Convenience
|
||||
private val sessionButtonExpandedPosition: PointF get() { return PointF(width.toFloat() / 2 - sessionButton.expandedSize / 2 - sessionButton.shadowMargin, 0.0f) }
|
||||
private val sessionTooltipExpandedPosition: PointF get() {
|
||||
val x = sessionButtonExpandedPosition.x + sessionButton.width / 2 - sessionTooltip.width / 2
|
||||
val y = sessionButtonExpandedPosition.y + sessionButton.height - sessionTooltip.height / 2
|
||||
return PointF(x, y)
|
||||
}
|
||||
private val closedGroupButtonExpandedPosition: PointF get() { return PointF(width.toFloat() - closedGroupButton.expandedSize - 2 * closedGroupButton.shadowMargin, height.toFloat() - bottomMargin - closedGroupButton.expandedSize - 2 * closedGroupButton.shadowMargin) }
|
||||
private val closedGroupTooltipExpandedPosition: PointF get() {
|
||||
val x = closedGroupButtonExpandedPosition.x + closedGroupButton.width / 2 - closedGroupTooltip.width / 2
|
||||
val y = closedGroupButtonExpandedPosition.y + closedGroupButton.height - closedGroupTooltip.height / 2
|
||||
return PointF(x, y)
|
||||
}
|
||||
private val openGroupButtonExpandedPosition: PointF get() { return PointF(0.0f, height.toFloat() - bottomMargin - openGroupButton.expandedSize - 2 * openGroupButton.shadowMargin) }
|
||||
private val openGroupTooltipExpandedPosition: PointF get() {
|
||||
val x = openGroupButtonExpandedPosition.x + openGroupButton.width / 2 - openGroupTooltip.width / 2
|
||||
val y = openGroupButtonExpandedPosition.y + openGroupButton.height - openGroupTooltip.height / 2
|
||||
return PointF(x, y)
|
||||
}
|
||||
private val buttonRestPosition: PointF get() { return PointF(width.toFloat() / 2 - mainButton.expandedSize / 2 - mainButton.shadowMargin, height.toFloat() - bottomMargin - mainButton.expandedSize - 2 * mainButton.shadowMargin) }
|
||||
private fun tooltipRestPosition(viewWidth: Int): PointF {
|
||||
return PointF(width.toFloat() / 2 - viewWidth / 2, height.toFloat() - bottomMargin)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Settings
|
||||
@@ -41,8 +73,29 @@ class NewConversationButtonSetView : RelativeLayout {
|
||||
// region Components
|
||||
private val mainButton by lazy { Button(context, true, R.drawable.ic_plus) }
|
||||
private val sessionButton by lazy { Button(context, false, R.drawable.ic_message) }
|
||||
private val sessionTooltip by lazy {
|
||||
TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 10f)
|
||||
setText(R.string.NewConversationButton_SessionTooltip)
|
||||
isAllCaps = true
|
||||
}
|
||||
}
|
||||
private val closedGroupButton by lazy { Button(context, false, R.drawable.ic_group) }
|
||||
private val closedGroupTooltip by lazy {
|
||||
TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 10f)
|
||||
setText(R.string.NewConversationButton_ClosedGroupTooltip)
|
||||
isAllCaps = true
|
||||
}
|
||||
}
|
||||
private val openGroupButton by lazy { Button(context, false, R.drawable.ic_globe) }
|
||||
private val openGroupTooltip by lazy {
|
||||
TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 10f)
|
||||
setText(R.string.NewConversationButton_OpenGroupTooltip)
|
||||
isAllCaps = true
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Button
|
||||
@@ -163,21 +216,27 @@ class NewConversationButtonSetView : RelativeLayout {
|
||||
isHapticFeedbackEnabled = true
|
||||
// Set up session button
|
||||
addView(sessionButton)
|
||||
addView(sessionTooltip)
|
||||
sessionButton.alpha = 0.0f
|
||||
sessionTooltip.alpha = 0.0f
|
||||
val sessionButtonLayoutParams = sessionButton.layoutParams as LayoutParams
|
||||
sessionButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
||||
sessionButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
||||
sessionButtonLayoutParams.bottomMargin = bottomMargin.toInt()
|
||||
// Set up closed group button
|
||||
addView(closedGroupButton)
|
||||
addView(closedGroupTooltip)
|
||||
closedGroupButton.alpha = 0.0f
|
||||
closedGroupTooltip.alpha = 0.0f
|
||||
val closedGroupButtonLayoutParams = closedGroupButton.layoutParams as LayoutParams
|
||||
closedGroupButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
||||
closedGroupButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
||||
closedGroupButtonLayoutParams.bottomMargin = bottomMargin.toInt()
|
||||
// Set up open group button
|
||||
addView(openGroupButton)
|
||||
addView(openGroupTooltip)
|
||||
openGroupButton.alpha = 0.0f
|
||||
openGroupTooltip.alpha = 0.0f
|
||||
val openGroupButtonLayoutParams = openGroupButton.layoutParams as LayoutParams
|
||||
openGroupButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
||||
openGroupButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
||||
@@ -260,24 +319,57 @@ class NewConversationButtonSetView : RelativeLayout {
|
||||
|
||||
private fun expand() {
|
||||
val buttonsExcludingMainButton = listOf( sessionButton, closedGroupButton, openGroupButton )
|
||||
val allTooltips = listOf(sessionTooltip, closedGroupTooltip, openGroupTooltip)
|
||||
|
||||
sessionButton.animatePositionChange(buttonRestPosition, sessionButtonExpandedPosition)
|
||||
sessionTooltip.animatePositionChange(tooltipRestPosition(sessionTooltip.width), sessionTooltipExpandedPosition)
|
||||
closedGroupButton.animatePositionChange(buttonRestPosition, closedGroupButtonExpandedPosition)
|
||||
closedGroupTooltip.animatePositionChange(tooltipRestPosition(closedGroupTooltip.width), closedGroupTooltipExpandedPosition)
|
||||
openGroupButton.animatePositionChange(buttonRestPosition, openGroupButtonExpandedPosition)
|
||||
openGroupTooltip.animatePositionChange(tooltipRestPosition(openGroupTooltip.width), openGroupTooltipExpandedPosition)
|
||||
buttonsExcludingMainButton.forEach { it.animateAlphaChange(0.0f, 1.0f) }
|
||||
allTooltips.forEach { it.animateAlphaChange(0.0f, 1.0f) }
|
||||
postDelayed({ isExpanded = true }, Button.animationDuration)
|
||||
}
|
||||
|
||||
private fun collapse() {
|
||||
val allButtons = listOf( mainButton, sessionButton, closedGroupButton, openGroupButton )
|
||||
val allTooltips = listOf(sessionTooltip, closedGroupTooltip, openGroupTooltip)
|
||||
|
||||
allButtons.forEach {
|
||||
val currentPosition = PointF(it.x, it.y)
|
||||
it.animatePositionChange(currentPosition, buttonRestPosition)
|
||||
val endAlpha = if (it == mainButton) 1.0f else 0.0f
|
||||
it.animateAlphaChange(it.alpha, endAlpha)
|
||||
}
|
||||
allTooltips.forEach {
|
||||
it.animateAlphaChange(1.0f, 0.0f)
|
||||
it.animatePositionChange(PointF(it.x, it.y), tooltipRestPosition(it.width))
|
||||
}
|
||||
postDelayed({ isExpanded = false }, Button.animationDuration)
|
||||
}
|
||||
// endregion
|
||||
|
||||
fun View.animatePositionChange(startPosition: PointF, endPosition: PointF) {
|
||||
val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition)
|
||||
animation.duration = Button.animationDuration
|
||||
animation.addUpdateListener { animator ->
|
||||
val point = animator.animatedValue as PointF
|
||||
x = point.x
|
||||
y = point.y
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
fun View.animateAlphaChange(startAlpha: Float, endAlpha: Float) {
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), startAlpha, endAlpha)
|
||||
animation.duration = Button.animationDuration
|
||||
animation.addUpdateListener { animator ->
|
||||
alpha = animator.animatedValue as Float
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// region Delegate
|
||||
|
||||
@@ -45,7 +45,11 @@ class GlobalSearchInputLayout @JvmOverloads constructor(
|
||||
override fun onFocusChange(v: View?, hasFocus: Boolean) {
|
||||
if (v === binding.searchInput) {
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(windowToken, 0)
|
||||
if (!hasFocus) {
|
||||
imm.hideSoftInputFromWindow(windowToken, 0)
|
||||
} else {
|
||||
imm.showSoftInput(v, 0)
|
||||
}
|
||||
listener?.onInputFocusChanged(hasFocus)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogShareLogsBinding
|
||||
@@ -93,7 +95,9 @@ class ShareLogsDialog : BaseDialog() {
|
||||
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
|
||||
withContext(Main) {
|
||||
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user