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:
Harris
2022-02-28 17:23:58 +11:00
committed by GitHub
parent a002e3e1f7
commit 55aa266769
149 changed files with 8729 additions and 17915 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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