Add basic attachment keyboard support to CFv2.

This commit is contained in:
Cody Henthorne
2023-05-17 10:28:50 -04:00
committed by Greyson Parrelli
parent 0c57113d8e
commit 4b09f4a654
13 changed files with 555 additions and 115 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
*/

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

View File

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

View File

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

View File

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

View File

@@ -44,3 +44,5 @@ class LifecycleDisposable : DefaultLifecycleObserver {
add(disposable)
}
}
fun Disposable.addTo(lifecycleDisposable: LifecycleDisposable): Disposable = apply { lifecycleDisposable.add(this) }