mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Add basic attachment keyboard support to CFv2.
This commit is contained in:
committed by
Greyson Parrelli
parent
0c57113d8e
commit
4b09f4a654
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.EditText
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
/**
|
||||
* A flavor of [InsetAwareConstraintLayout] that allows "replacing" the keyboard with our
|
||||
* own input fragment.
|
||||
*/
|
||||
class InputAwareConstraintLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : InsetAwareConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var inputId: Int? = null
|
||||
private var input: Fragment? = null
|
||||
|
||||
lateinit var fragmentManager: FragmentManager
|
||||
var listener: Listener? = null
|
||||
|
||||
fun showSoftkey(editText: EditText) {
|
||||
ViewUtil.focusAndShowKeyboard(editText)
|
||||
hideInput(resetKeyboardGuideline = false)
|
||||
}
|
||||
|
||||
fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, toggled: (Boolean) -> Unit = { }) {
|
||||
if (fragmentCreator.id == inputId) {
|
||||
hideInput(resetKeyboardGuideline = true)
|
||||
toggled(false)
|
||||
} else {
|
||||
hideInput(resetKeyboardGuideline = false)
|
||||
showInput(fragmentCreator, imeTarget)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideInput() {
|
||||
hideInput(resetKeyboardGuideline = true)
|
||||
}
|
||||
|
||||
private fun showInput(fragmentCreator: FragmentCreator, imeTarget: EditText) {
|
||||
inputId = fragmentCreator.id
|
||||
input = fragmentCreator.create()
|
||||
|
||||
fragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.input_container, input!!)
|
||||
.commit()
|
||||
|
||||
overrideKeyboardGuidelineWithPreviousHeight()
|
||||
ViewUtil.hideKeyboard(context, imeTarget)
|
||||
|
||||
listener?.onInputShown()
|
||||
}
|
||||
|
||||
private fun hideInput(resetKeyboardGuideline: Boolean) {
|
||||
val inputHidden = input != null
|
||||
input?.let {
|
||||
fragmentManager
|
||||
.beginTransaction()
|
||||
.remove(it)
|
||||
.commit()
|
||||
}
|
||||
input = null
|
||||
inputId = null
|
||||
|
||||
if (resetKeyboardGuideline) {
|
||||
resetKeyboardGuideline()
|
||||
} else {
|
||||
clearKeyboardGuidelineOverride()
|
||||
}
|
||||
|
||||
if (inputHidden) {
|
||||
listener?.onInputHidden()
|
||||
}
|
||||
}
|
||||
|
||||
interface FragmentCreator {
|
||||
val id: Int
|
||||
fun create(): Fragment
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onInputShown()
|
||||
fun onInputHidden()
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.Guideline;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class InsetAwareConstraintLayout extends ConstraintLayout {
|
||||
|
||||
private WindowInsetsTypeProvider windowInsetsTypeProvider = WindowInsetsTypeProvider.ALL;
|
||||
private Insets insets;
|
||||
|
||||
public InsetAwareConstraintLayout(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public void setWindowInsetsTypeProvider(@NonNull WindowInsetsTypeProvider windowInsetsTypeProvider) {
|
||||
this.windowInsetsTypeProvider = windowInsetsTypeProvider;
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
|
||||
WindowInsetsCompat windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets);
|
||||
Insets newInsets = windowInsetsCompat.getInsets(windowInsetsTypeProvider.getInsetsType());
|
||||
|
||||
applyInsets(newInsets);
|
||||
return super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
public void applyInsets(@NonNull Insets insets) {
|
||||
Guideline statusBarGuideline = findViewById(R.id.status_bar_guideline);
|
||||
Guideline navigationBarGuideline = findViewById(R.id.navigation_bar_guideline);
|
||||
Guideline parentStartGuideline = findViewById(R.id.parent_start_guideline);
|
||||
Guideline parentEndGuideline = findViewById(R.id.parent_end_guideline);
|
||||
|
||||
if (statusBarGuideline != null) {
|
||||
statusBarGuideline.setGuidelineBegin(insets.top);
|
||||
}
|
||||
|
||||
if (navigationBarGuideline != null) {
|
||||
navigationBarGuideline.setGuidelineEnd(insets.bottom);
|
||||
}
|
||||
|
||||
if (parentStartGuideline != null) {
|
||||
if (ViewUtil.isLtr(this)) {
|
||||
parentStartGuideline.setGuidelineBegin(insets.left);
|
||||
} else {
|
||||
parentStartGuideline.setGuidelineBegin(insets.right);
|
||||
}
|
||||
}
|
||||
|
||||
if (parentEndGuideline != null) {
|
||||
if (ViewUtil.isLtr(this)) {
|
||||
parentEndGuideline.setGuidelineEnd(insets.right);
|
||||
} else {
|
||||
parentEndGuideline.setGuidelineEnd(insets.left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showSoftkey(@NonNull EditText editText) {
|
||||
ViewUtil.focusAndShowKeyboard(editText);
|
||||
}
|
||||
|
||||
public interface WindowInsetsTypeProvider {
|
||||
|
||||
WindowInsetsTypeProvider ALL = () ->
|
||||
WindowInsetsCompat.Type.ime() |
|
||||
WindowInsetsCompat.Type.systemBars() |
|
||||
WindowInsetsCompat.Type.displayCutout();
|
||||
|
||||
@WindowInsetsCompat.Type.InsetsType
|
||||
int getInsetsType();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Surface
|
||||
import android.view.WindowInsets
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.Guideline
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
/**
|
||||
* A specialized [ConstraintLayout] that sets guidelines based on the window insets provided
|
||||
* by the system.
|
||||
*
|
||||
* In portrait mode these are how the guidelines will be configured:
|
||||
*
|
||||
* - [R.id.status_bar_guideline] is set to the bottom of the status bar
|
||||
* - [R.id.navigation_bar_guideline] is set to the top of the navigation bar
|
||||
* - [R.id.parent_start_guideline] is set to the start of the parent
|
||||
* - [R.id.parent_end_guideline] is set to the end of the parent
|
||||
* - [R.id.keyboard_guideline] will be set to the top of the keyboard and will
|
||||
* change as the keyboard is shown or hidden
|
||||
*
|
||||
* In landscape, the spirit of the guidelines are maintained but their names may not
|
||||
* correlated exactly to the inset they are providing.
|
||||
*
|
||||
* These guidelines will only be updated if present in your layout, you can use
|
||||
* `<include layout="@layout/system_ui_guidelines" />` to quickly include them.
|
||||
*/
|
||||
open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
companion object {
|
||||
private val keyboardType = WindowInsetsCompat.Type.ime()
|
||||
private val windowTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
|
||||
}
|
||||
|
||||
private val statusBarGuideline: Guideline? by lazy { findViewById(R.id.status_bar_guideline) }
|
||||
private val navigationBarGuideline: Guideline? by lazy { findViewById(R.id.navigation_bar_guideline) }
|
||||
private val parentStartGuideline: Guideline? by lazy { findViewById(R.id.parent_start_guideline) }
|
||||
private val parentEndGuideline: Guideline? by lazy { findViewById(R.id.parent_end_guideline) }
|
||||
private val keyboardGuideline: Guideline? by lazy { findViewById(R.id.keyboard_guideline) }
|
||||
|
||||
private val displayMetrics = DisplayMetrics()
|
||||
private var overridingKeyboard: Boolean = false
|
||||
|
||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||
val windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets)
|
||||
|
||||
val windowInsets = windowInsetsCompat.getInsets(windowTypes)
|
||||
val keyboardInset = windowInsetsCompat.getInsets(keyboardType)
|
||||
|
||||
applyInsets(windowInsets, keyboardInset)
|
||||
|
||||
return super.onApplyWindowInsets(insets)
|
||||
}
|
||||
|
||||
private fun applyInsets(windowInsets: Insets, keyboardInset: Insets) {
|
||||
val isLtr = ViewUtil.isLtr(this)
|
||||
|
||||
statusBarGuideline?.setGuidelineBegin(windowInsets.top)
|
||||
navigationBarGuideline?.setGuidelineEnd(windowInsets.bottom)
|
||||
parentStartGuideline?.setGuidelineBegin(if (isLtr) windowInsets.left else windowInsets.right)
|
||||
parentEndGuideline?.setGuidelineEnd(if (isLtr) windowInsets.right else windowInsets.left)
|
||||
|
||||
if (keyboardInset.bottom > 0) {
|
||||
setKeyboardHeight(keyboardInset.bottom)
|
||||
keyboardGuideline?.setGuidelineEnd(keyboardInset.bottom)
|
||||
} else if (!overridingKeyboard) {
|
||||
keyboardGuideline?.setGuidelineEnd(windowInsets.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun overrideKeyboardGuidelineWithPreviousHeight() {
|
||||
overridingKeyboard = true
|
||||
keyboardGuideline?.setGuidelineEnd(getKeyboardHeight())
|
||||
}
|
||||
|
||||
protected fun clearKeyboardGuidelineOverride() {
|
||||
overridingKeyboard = false
|
||||
}
|
||||
|
||||
protected fun resetKeyboardGuideline() {
|
||||
clearKeyboardGuidelineOverride()
|
||||
keyboardGuideline?.setGuidelineEnd(navigationBarGuideline.guidelineEnd)
|
||||
}
|
||||
|
||||
private fun getKeyboardHeight(): Int {
|
||||
val height = if (isLandscape()) {
|
||||
SignalStore.misc().keyboardLandscapeHeight
|
||||
} else {
|
||||
SignalStore.misc().keyboardPortraitHeight
|
||||
}
|
||||
|
||||
return if (height <= 0) {
|
||||
resources.getDimensionPixelSize(R.dimen.default_custom_keyboard_size)
|
||||
} else {
|
||||
height
|
||||
}
|
||||
}
|
||||
|
||||
private fun setKeyboardHeight(height: Int) {
|
||||
if (isLandscape()) {
|
||||
SignalStore.misc().keyboardLandscapeHeight = height
|
||||
} else {
|
||||
SignalStore.misc().keyboardPortraitHeight = height
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLandscape(): Boolean {
|
||||
val rotation = getDeviceRotation()
|
||||
return rotation == Surface.ROTATION_90
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getDeviceRotation(): Int {
|
||||
if (isInEditMode) {
|
||||
return Surface.ROTATION_0
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
context.display?.getRealMetrics(displayMetrics)
|
||||
} else {
|
||||
ServiceUtil.getWindowManager(context).defaultDisplay.getRealMetrics(displayMetrics)
|
||||
}
|
||||
|
||||
return if (displayMetrics.widthPixels > displayMetrics.heightPixels) Surface.ROTATION_90 else Surface.ROTATION_0
|
||||
}
|
||||
|
||||
private val Guideline?.guidelineEnd: Int
|
||||
get() = if (this == null) 0 else (layoutParams as LayoutParams).guideEnd
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.ActivityCompat
|
||||
@@ -31,6 +32,8 @@ import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -60,8 +63,8 @@ import org.thoughtcrime.securesms.badges.gifts.viewgift.sent.ViewSentGiftBottomS
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle
|
||||
import org.thoughtcrime.securesms.components.ComposeText
|
||||
import org.thoughtcrime.securesms.components.HidingLinearLayout
|
||||
import org.thoughtcrime.securesms.components.InputAwareConstraintLayout
|
||||
import org.thoughtcrime.securesms.components.InputPanel
|
||||
import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout
|
||||
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
|
||||
import org.thoughtcrime.securesms.components.SendButton
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
@@ -73,6 +76,7 @@ import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil
|
||||
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity
|
||||
import org.thoughtcrime.securesms.conversation.AttachmentKeyboardButton
|
||||
import org.thoughtcrime.securesms.conversation.BadDecryptLearnMoreDialog
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
@@ -93,6 +97,7 @@ import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog
|
||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel
|
||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel
|
||||
import org.thoughtcrime.securesms.conversation.v2.keyboard.AttachmentKeyboardFragment
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
@@ -119,6 +124,8 @@ import org.thoughtcrime.securesms.longmessage.LongMessageFragment
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory.create
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
@@ -215,8 +222,8 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
}
|
||||
|
||||
private val container: InsetAwareConstraintLayout
|
||||
get() = requireView() as InsetAwareConstraintLayout
|
||||
private val container: InputAwareConstraintLayout
|
||||
get() = requireView() as InputAwareConstraintLayout
|
||||
|
||||
private val inputPanel: InputPanel
|
||||
get() = binding.conversationInputPanel.root
|
||||
@@ -256,6 +263,8 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
presentActionBarMenu()
|
||||
|
||||
observeConversationThread()
|
||||
|
||||
container.fragmentManager = childFragmentManager
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -358,6 +367,12 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
post { sendButton.triggerSelectedChangedEvent() }
|
||||
}
|
||||
|
||||
val attachListener = { _: View ->
|
||||
container.toggleInput(AttachmentKeyboardFragmentCreator, composeText)
|
||||
}
|
||||
binding.conversationInputPanel.attachButton.setOnClickListener(attachListener)
|
||||
binding.conversationInputPanel.inlineAttachmentButton.setOnClickListener(attachListener)
|
||||
|
||||
presentGroupCallJoinButton()
|
||||
|
||||
binding.scrollToBottom.setOnClickListener {
|
||||
@@ -369,6 +384,17 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
|
||||
adapter.registerAdapterDataObserver(DataObserver(scrollToPositionDelegate))
|
||||
|
||||
val keyboardEvents = KeyboardEvents()
|
||||
container.listener = keyboardEvents
|
||||
requireActivity()
|
||||
.onBackPressedDispatcher
|
||||
.addCallback(
|
||||
viewLifecycleOwner,
|
||||
keyboardEvents
|
||||
)
|
||||
|
||||
childFragmentManager.setFragmentResultListener(AttachmentKeyboardFragment.RESULT_KEY, viewLifecycleOwner, AttachmentKeyboardFragmentListener())
|
||||
}
|
||||
|
||||
private fun calculateCharactersRemaining() {
|
||||
@@ -630,9 +656,11 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendMessage(metricId: String? = null, scheduledDate: Long = -1) {
|
||||
val slideDeck: SlideDeck? = if (attachmentManager.isAttachmentPresent) attachmentManager.buildSlideDeck() else null
|
||||
|
||||
private fun sendMessage(
|
||||
metricId: String? = null,
|
||||
scheduledDate: Long = -1,
|
||||
slideDeck: SlideDeck? = if (attachmentManager.isAttachmentPresent) attachmentManager.buildSlideDeck() else null
|
||||
) {
|
||||
val send: Completable = viewModel.sendMessage(
|
||||
metricId = metricId,
|
||||
body = composeText.editableText.toString(),
|
||||
@@ -1286,6 +1314,8 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
|
||||
//endregion Compose + Send Callbacks
|
||||
|
||||
//region Attachment + Media Keyboard
|
||||
|
||||
private inner class AttachmentManagerListener : AttachmentManager.AttachmentListener {
|
||||
override fun onAttachmentChanged() {
|
||||
// TODO [cody] implement
|
||||
@@ -1295,4 +1325,47 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
// TODO [cody] implement
|
||||
}
|
||||
}
|
||||
|
||||
private object AttachmentKeyboardFragmentCreator : InputAwareConstraintLayout.FragmentCreator {
|
||||
override val id: Int = 1
|
||||
override fun create(): Fragment = AttachmentKeyboardFragment()
|
||||
}
|
||||
|
||||
private inner class AttachmentKeyboardFragmentListener : FragmentResultListener {
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onFragmentResult(requestKey: String, result: Bundle) {
|
||||
val button: AttachmentKeyboardButton? = result.getSerializable(AttachmentKeyboardFragment.BUTTON_RESULT) as? AttachmentKeyboardButton
|
||||
val media: Media? = result.getParcelable(AttachmentKeyboardFragment.MEDIA_RESULT)
|
||||
|
||||
if (button != null) {
|
||||
when (button) {
|
||||
AttachmentKeyboardButton.GALLERY -> AttachmentManager.selectGallery(this@ConversationFragment, 1, viewModel.recipientSnapshot!!, composeText.textTrimmed, sendButton.selectedSendType, inputPanel.quote.isPresent)
|
||||
AttachmentKeyboardButton.FILE -> AttachmentManager.selectDocument(this@ConversationFragment, 1)
|
||||
AttachmentKeyboardButton.CONTACT -> AttachmentManager.selectContactInfo(this@ConversationFragment, 1)
|
||||
AttachmentKeyboardButton.LOCATION -> AttachmentManager.selectLocation(this@ConversationFragment, 1, viewModel.recipientSnapshot!!.chatColors.asSingleColor())
|
||||
AttachmentKeyboardButton.PAYMENT -> AttachmentManager.selectPayment(this@ConversationFragment, viewModel.recipientSnapshot!!)
|
||||
}
|
||||
} else if (media != null) {
|
||||
startActivityForResult(MediaSelectionActivity.editor(requireActivity(), sendButton.selectedSendType, listOf(media), viewModel.recipientSnapshot!!.id, composeText.textTrimmed), 12)
|
||||
}
|
||||
|
||||
container.hideInput()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class KeyboardEvents : OnBackPressedCallback(false), InputAwareConstraintLayout.Listener {
|
||||
override fun handleOnBackPressed() {
|
||||
container.hideInput()
|
||||
}
|
||||
|
||||
override fun onInputShown() {
|
||||
isEnabled = true
|
||||
}
|
||||
|
||||
override fun onInputHidden() {
|
||||
isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.keyboard
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.addTo
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.AttachmentKeyboard
|
||||
import org.thoughtcrime.securesms.conversation.AttachmentKeyboardButton
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.ViewModelFactory
|
||||
|
||||
/**
|
||||
* Fragment wrapped version of [AttachmentKeyboard] to help encapsulate logic the view
|
||||
* needs from external sources.
|
||||
*/
|
||||
class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_fragment), AttachmentKeyboard.Callback {
|
||||
|
||||
companion object {
|
||||
const val RESULT_KEY = "AttachmentKeyboardFragmentResult"
|
||||
const val MEDIA_RESULT = "Media"
|
||||
const val BUTTON_RESULT = "Button"
|
||||
}
|
||||
|
||||
private val viewModel: AttachmentKeyboardViewModel by viewModels(
|
||||
factoryProducer = ViewModelFactory.factoryProducer { AttachmentKeyboardViewModel() }
|
||||
)
|
||||
|
||||
private lateinit var conversationViewModel: ConversationViewModel
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
@Suppress("ReplaceGetOrSet")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
val attachmentKeyboardView = view.findViewById<AttachmentKeyboard>(R.id.attachment_keyboard)
|
||||
|
||||
attachmentKeyboardView.setCallback(this)
|
||||
|
||||
viewModel.getRecentMedia()
|
||||
.subscribeBy {
|
||||
attachmentKeyboardView.onMediaChanged(it)
|
||||
}
|
||||
.addTo(lifecycleDisposable)
|
||||
|
||||
conversationViewModel = ViewModelProvider(requireParentFragment()).get(ConversationViewModel::class.java)
|
||||
conversationViewModel
|
||||
.recipient
|
||||
.subscribeBy {
|
||||
attachmentKeyboardView.setWallpaperEnabled(it.hasWallpaper())
|
||||
}
|
||||
.addTo(lifecycleDisposable)
|
||||
}
|
||||
|
||||
override fun onAttachmentMediaClicked(media: Media) {
|
||||
setFragmentResult(RESULT_KEY, bundleOf(MEDIA_RESULT to media))
|
||||
}
|
||||
|
||||
override fun onAttachmentSelectorClicked(button: AttachmentKeyboardButton) {
|
||||
setFragmentResult(RESULT_KEY, bundleOf(BUTTON_RESULT to button))
|
||||
}
|
||||
|
||||
override fun onAttachmentPermissionsRequested() {
|
||||
Permissions.with(requireParentFragment())
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.onAllGranted { viewModel.refreshRecentMedia() }
|
||||
.withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.keyboard
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaRepository
|
||||
|
||||
class AttachmentKeyboardViewModel(
|
||||
private val mediaRepository: MediaRepository = MediaRepository()
|
||||
) : ViewModel() {
|
||||
|
||||
private val refreshRecentMedia = BehaviorSubject.createDefault(Unit)
|
||||
|
||||
fun getRecentMedia(): Observable<MutableList<Media>> {
|
||||
return refreshRecentMedia
|
||||
.flatMapSingle {
|
||||
mediaRepository
|
||||
.recentMedia
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun refreshRecentMedia() {
|
||||
refreshRecentMedia.onNext(Unit)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChangeNumberConstraintObserver;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -33,6 +36,8 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||
private static final String LINKED_DEVICES_REMINDER = "misc.linked_devices_reminder";
|
||||
private static final String HAS_LINKED_DEVICES = "misc.linked_devices_present";
|
||||
private static final String USERNAME_QR_CODE_COLOR = "mis.username_qr_color_scheme";
|
||||
private static final String KEYBOARD_LANDSCAPE_HEIGHT = "misc.keyboard.landscape_height";
|
||||
private static final String KEYBOARD_PORTRAIT_HEIGHT = "misc.keyboard.protrait_height";
|
||||
|
||||
MiscellaneousValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
@@ -277,4 +282,39 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||
putString(USERNAME_QR_CODE_COLOR, color.serialize());
|
||||
}
|
||||
|
||||
public int getKeyboardLandscapeHeight() {
|
||||
int height = getInteger(KEYBOARD_LANDSCAPE_HEIGHT, 0);
|
||||
if (height == 0) {
|
||||
//noinspection deprecation
|
||||
height = PreferenceManager.getDefaultSharedPreferences(ApplicationDependencies.getApplication())
|
||||
.getInt("keyboard_height_landscape", 0);
|
||||
|
||||
if (height > 0) {
|
||||
setKeyboardLandscapeHeight(height);
|
||||
}
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setKeyboardLandscapeHeight(int height) {
|
||||
putLong(KEYBOARD_LANDSCAPE_HEIGHT, height);
|
||||
}
|
||||
|
||||
public int getKeyboardPortraitHeight() {
|
||||
int height = (int) getInteger(KEYBOARD_PORTRAIT_HEIGHT, 0);
|
||||
if (height == 0) {
|
||||
//noinspection deprecation
|
||||
height = PreferenceManager.getDefaultSharedPreferences(ApplicationDependencies.getApplication())
|
||||
.getInt("keyboard_height_portrait", 0);
|
||||
|
||||
if (height > 0) {
|
||||
setKeyboardPortraitHeight(height);
|
||||
}
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setKeyboardPortraitHeight(int height) {
|
||||
putInteger(KEYBOARD_PORTRAIT_HEIGHT, height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.annimon.stream.Stream;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.signal.core.util.SqlUtil;
|
||||
@@ -40,6 +41,9 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Handles the retrieval of media present on the user's device.
|
||||
*/
|
||||
@@ -61,6 +65,25 @@ public class MediaRepository {
|
||||
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getFolders(context)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of recent media items (images and videos).
|
||||
*/
|
||||
public Single<List<Media>> getRecentMedia() {
|
||||
return Single.<List<Media>>fromCallable(() -> {
|
||||
if (!StorageUtil.canReadFromMediaStore()) {
|
||||
Log.w(TAG, "No storage permissions!", new Throwable());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return getMediaInBucket(ApplicationDependencies.getApplication(), Media.ALL_MEDIA_BUCKET_ID);
|
||||
})
|
||||
.onErrorReturn(t -> {
|
||||
Log.w(TAG, "Unable to get recent media", t);
|
||||
return Collections.emptyList();
|
||||
})
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of media items (images and videos) that are present int he specified bucket.
|
||||
*/
|
||||
|
||||
20
app/src/main/res/layout/attachment_keyboard_fragment.xml
Normal file
20
app/src/main/res/layout/attachment_keyboard_fragment.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2023 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.AttachmentKeyboard
|
||||
android:id="@+id/attachment_keyboard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -30,7 +30,7 @@
|
||||
android:id="@+id/layout_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/navigation_bar_guideline"
|
||||
app:layout_constraintBottom_toBottomOf="@id/keyboard_guideline"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
android:orientation="horizontal"
|
||||
tools:layout_constraintGuide_end="48dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/keyboard_guideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
tools:layout_constraintGuide_end="48dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/parent_start_guideline"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.InsetAwareConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.thoughtcrime.securesms.components.InputAwareConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.mutiselect.MultiselectRecyclerView
|
||||
android:id="@+id/conversation_item_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dip"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
@@ -44,6 +44,8 @@
|
||||
android:scrollbars="vertical"
|
||||
android:splitMotionEvents="false"
|
||||
app:layout_constraintBottom_toTopOf="@id/conversation_bottom_panel_barrier"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:itemCount="20"
|
||||
tools:listitem="@layout/conversation_item_sent_text_only" />
|
||||
@@ -106,7 +108,7 @@
|
||||
android:visibility="invisible"
|
||||
app:cstv_scroll_button_src="@drawable/ic_at_20"
|
||||
app:layout_constraintBottom_toTopOf="@id/scroll_to_bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_goneMarginBottom="20dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -119,7 +121,7 @@
|
||||
android:visibility="invisible"
|
||||
app:cstv_scroll_button_src="@drawable/ic_chevron_down_20"
|
||||
app:layout_constraintBottom_toTopOf="@id/conversation_bottom_panel_barrier"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
@@ -135,18 +137,18 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/attachment_editor"
|
||||
android:layout="@layout/conversation_activity_attachment_editor_stub"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/conversation_input_panel"/>
|
||||
app:layout_constraintBottom_toTopOf="@+id/conversation_input_panel"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline" />
|
||||
|
||||
<include
|
||||
android:id="@+id/conversation_input_panel"
|
||||
layout="@layout/conversation_input_panel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toTopOf="@+id/conversation_input_space_left"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/conversation_input_space_left"
|
||||
@@ -156,10 +158,19 @@
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/navigation_bar_guideline"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/keyboard_guideline"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
tools:text="160/160 (1)"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.InsetAwareConstraintLayout>
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/input_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/navigation_bar_guideline"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
app:layout_constraintTop_toTopOf="@id/keyboard_guideline" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.InputAwareConstraintLayout>
|
||||
|
||||
@@ -44,3 +44,5 @@ class LifecycleDisposable : DefaultLifecycleObserver {
|
||||
add(disposable)
|
||||
}
|
||||
}
|
||||
|
||||
fun Disposable.addTo(lifecycleDisposable: LifecycleDisposable): Disposable = apply { lifecycleDisposable.add(this) }
|
||||
|
||||
Reference in New Issue
Block a user