Add media keyboard support in CFv2.

This commit is contained in:
Cody Henthorne 2023-06-23 13:07:58 -04:00 committed by Nicholas
parent b042945fef
commit 65255121de
8 changed files with 171 additions and 21 deletions

View file

@ -34,10 +34,13 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
hideInput(resetKeyboardGuideline = false)
}
fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, toggled: (Boolean) -> Unit = { }) {
fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, showSoftKeyOnHide: Boolean = false) {
if (fragmentCreator.id == inputId) {
hideInput(resetKeyboardGuideline = true)
toggled(false)
if (showSoftKeyOnHide) {
showSoftkey(imeTarget)
} else {
hideInput(resetKeyboardGuideline = true)
}
} else {
hideInput(resetKeyboardGuideline = false)
showInput(fragmentCreator, imeTarget)
@ -55,6 +58,7 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
fragmentManager
.beginTransaction()
.replace(R.id.input_container, input!!)
.runOnCommit { (input as? InputFragment)?.show() }
.commit()
overrideKeyboardGuidelineWithPreviousHeight()
@ -66,6 +70,7 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
private fun hideInput(resetKeyboardGuideline: Boolean) {
val inputHidden = input != null
input?.let {
(input as? InputFragment)?.hide()
fragmentManager
.beginTransaction()
.remove(it)
@ -94,4 +99,9 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
fun onInputShown()
fun onInputHidden()
}
interface InputFragment {
fun show()
fun hide()
}
}

View file

@ -105,7 +105,6 @@ public class MediaKeyboard extends FrameLayout implements InputView {
if (!isInitialised) initView();
setVisibility(VISIBLE);
if (keyboardListener != null) keyboardListener.onShown();
keyboardPagerFragment.show();
}
@ -113,7 +112,6 @@ public class MediaKeyboard extends FrameLayout implements InputView {
public void hide(boolean immediate) {
setVisibility(GONE);
onCloseEmojiSearchInternal(false);
if (keyboardListener != null) keyboardListener.onHidden();
Log.i(TAG, "hide()");
keyboardPagerFragment.hide();
}

View file

@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.giph.ui.GiphyActivity
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
@ -32,6 +33,7 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba
private val contactShareLauncher = fragment.registerForActivityResult(ContactShareEditor) { contacts -> callbacks.onSendContacts(contacts) }
private val mediaSelectionLauncher = fragment.registerForActivityResult(MediaSelection) { result -> callbacks.onMediaSend(result) }
private val gifSearchLauncher = fragment.registerForActivityResult(GifSearch) { result -> callbacks.onMediaSend(result) }
fun launchContactShareEditor(uri: Uri, chatColors: ChatColors) {
contactShareLauncher.launch(uri to chatColors)
@ -41,14 +43,18 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba
mediaSelectionLauncher.launch(MediaSelectionInput(mediaList, recipientId, text))
}
private object MediaSelection : ActivityResultContract<MediaSelectionInput, MediaSendActivityResult>() {
fun launchGifSearch(recipientId: RecipientId, text: CharSequence?) {
gifSearchLauncher.launch(GifSearchInput(recipientId, text))
}
private object MediaSelection : ActivityResultContract<MediaSelectionInput, MediaSendActivityResult?>() {
override fun createIntent(context: Context, input: MediaSelectionInput): Intent {
val (media, recipientId, text) = input
return MediaSelectionActivity.editor(context, MessageSendType.SignalMessageSendType, media, recipientId, text)
}
override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult {
return MediaSendActivityResult.fromData(intent!!)
override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? {
return intent?.let { MediaSendActivityResult.fromData(intent) }
}
}
@ -63,10 +69,27 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba
}
}
private object GifSearch : ActivityResultContract<GifSearchInput, MediaSendActivityResult?>() {
override fun createIntent(context: Context, input: GifSearchInput): Intent {
return Intent(context, GiphyActivity::class.java).apply {
putExtra(GiphyActivity.EXTRA_IS_MMS, false)
putExtra(GiphyActivity.EXTRA_RECIPIENT_ID, input.recipientId)
putExtra(GiphyActivity.EXTRA_TRANSPORT, MessageSendType.SignalMessageSendType)
putExtra(GiphyActivity.EXTRA_TEXT, input.text)
}
}
override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? {
return intent?.let { MediaSendActivityResult.fromData(intent) }
}
}
private data class MediaSelectionInput(val media: List<Media>, val recipientId: RecipientId, val text: CharSequence?)
private data class GifSearchInput(val recipientId: RecipientId, val text: CharSequence?)
interface Callbacks {
fun onSendContacts(contacts: List<Contact>)
fun onMediaSend(result: MediaSendActivityResult)
fun onMediaSend(result: MediaSendActivityResult?)
}
}

View file

@ -52,6 +52,7 @@ import androidx.core.view.doOnPreDraw
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentResultListener
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@ -106,6 +107,9 @@ import org.thoughtcrime.securesms.components.ProgressCardDialogFragmentArgs
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
import org.thoughtcrime.securesms.components.SendButton
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
@ -190,6 +194,12 @@ import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestio
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
import org.thoughtcrime.securesms.invites.InviteActions
import org.thoughtcrime.securesms.keyboard.KeyboardPage
import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
import org.thoughtcrime.securesms.keyboard.gif.GifKeyboardPageFragment
import org.thoughtcrime.securesms.keyboard.sticker.StickerKeyboardPageFragment
import org.thoughtcrime.securesms.keyboard.sticker.StickerSearchDialogFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModelV2
@ -219,6 +229,7 @@ import org.thoughtcrime.securesms.notifications.v2.ConversationId
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment
@ -233,7 +244,9 @@ import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity
import org.thoughtcrime.securesms.revealable.ViewOnceUtil
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.stickers.StickerEventListener
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.stickers.StickerManagementActivity
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity
import org.thoughtcrime.securesms.stories.StoryViewerArgs
@ -271,6 +284,7 @@ import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil
import java.util.Locale
import java.util.Optional
import java.util.concurrent.ExecutionException
import kotlin.time.Duration.Companion.milliseconds
/**
* A single unified fragment for Conversations.
@ -278,7 +292,13 @@ import java.util.concurrent.ExecutionException
class ConversationFragment :
LoggingFragment(R.layout.v2_conversation_fragment),
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
ReactionsBottomSheetDialogFragment.Callback {
ReactionsBottomSheetDialogFragment.Callback,
EmojiKeyboardPageFragment.Callback,
EmojiEventListener,
GifKeyboardPageFragment.Host,
StickerEventListener,
StickerKeyboardPageFragment.Callback,
MediaKeyboard.MediaKeyboardListener {
companion object {
private val TAG = Log.tag(ConversationFragment::class.java)
@ -340,6 +360,8 @@ class ConversationFragment :
ConversationSearchViewModel(getString(R.string.note_to_self))
}
private val keyboardPagerViewModel: KeyboardPagerViewModel by activityViewModels()
private val stickerViewModel: StickerSuggestionsViewModel by viewModel {
StickerSuggestionsViewModel()
}
@ -347,6 +369,7 @@ class ConversationFragment :
private val conversationTooltips = ConversationTooltips(this)
private val colorizer = Colorizer()
private val textDraftSaveDebouncer = Debouncer(500)
private val recentEmojis: RecentEmojiPageModel by lazy { RecentEmojiPageModel(ApplicationDependencies.getApplication(), TextSecurePreferences.RECENT_STORAGE_KEY) }
private lateinit var layoutManager: LinearLayoutManager
private lateinit var markReadHelper: MarkReadHelper
@ -441,7 +464,7 @@ class ConversationFragment :
container.fragmentManager = childFragmentManager
ToolbarDependentMarginListener(binding.toolbar)
initializeMediaKeyboardToggle()
initializeMediaKeyboard()
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
@ -506,6 +529,70 @@ class ConversationFragment :
clearFocusedItem()
}
override fun openEmojiSearch() {
// TODO [cfv2] emoji search
}
override fun onEmojiSelected(emoji: String?) {
if (emoji != null) {
inputPanel.onEmojiSelected(emoji)
recentEmojis.onCodePointSelected(emoji)
}
}
override fun onKeyEvent(keyEvent: KeyEvent?) {
if (keyEvent != null) {
inputPanel.onKeyEvent(keyEvent)
}
}
override fun openStickerSearch() {
StickerSearchDialogFragment.show(childFragmentManager)
}
override fun onStickerSelected(sticker: StickerRecord) {
sendSticker(
stickerRecord = sticker,
clearCompose = false
)
}
override fun onStickerManagementClicked() {
startActivity(StickerManagementActivity.getIntent(requireContext()))
container.hideInput()
}
override fun isMms(): Boolean {
return false
}
override fun openGifSearch() {
val recipientId = viewModel.recipientSnapshot?.id ?: return
conversationActivityResultContracts.launchGifSearch(recipientId, composeText.textTrimmed)
}
override fun onGifSelectSuccess(blobUri: Uri, width: Int, height: Int) {
setMedia(
uri = blobUri,
mediaType = SlideFactory.MediaType.from(BlobProvider.getMimeType(blobUri))!!,
width = width,
height = height,
videoGif = true
)
}
override fun onShown() {
inputPanel.mediaKeyboardListener.onShown()
}
override fun onHidden() {
inputPanel.mediaKeyboardListener.onHidden()
}
override fun onKeyboardChanged(page: KeyboardPage) {
inputPanel.mediaKeyboardListener.onKeyboardChanged(page)
}
private fun observeConversationThread() {
var firstRender = true
disposables += viewModel
@ -1069,20 +1156,21 @@ class ConversationFragment :
.addTo(disposables)
}
private fun initializeMediaKeyboardToggle() {
private fun initializeMediaKeyboard() {
val isSystemEmojiPreferred = SignalStore.settings().isPreferSystemEmoji
val keyboardMode: TextSecurePreferences.MediaKeyboardMode = TextSecurePreferences.getMediaKeyboardMode(requireContext())
val stickerIntro: Boolean = !TextSecurePreferences.hasSeenStickerIntroTooltip(requireContext())
inputPanel.showMediaKeyboardToggle(true)
val toggleMode = when (keyboardMode) {
val keyboardPage = when (keyboardMode) {
TextSecurePreferences.MediaKeyboardMode.EMOJI -> if (isSystemEmojiPreferred) KeyboardPage.STICKER else KeyboardPage.EMOJI
TextSecurePreferences.MediaKeyboardMode.STICKER -> KeyboardPage.STICKER
TextSecurePreferences.MediaKeyboardMode.GIF -> KeyboardPage.GIF
}
inputPanel.setMediaKeyboardToggleMode(toggleMode)
inputPanel.setMediaKeyboardToggleMode(keyboardPage)
keyboardPagerViewModel.switchToPage(keyboardPage)
if (stickerIntro) {
TextSecurePreferences.setMediaKeyboardMode(requireContext(), TextSecurePreferences.MediaKeyboardMode.STICKER)
@ -1165,6 +1253,8 @@ class ConversationFragment :
)
sendMessageWithoutComposeInput(slide, clearCompose = clearCompose)
viewModel.updateStickerLastUsedTime(stickerRecord, System.currentTimeMillis().milliseconds)
}
private fun sendMessageWithoutComposeInput(
@ -1199,7 +1289,7 @@ class ConversationFragment :
preUploadResults: List<MessageSender.PreUploadResult> = emptyList(),
afterSendComplete: () -> Unit = {}
) {
val metricId = viewModel.recipientSnapshot?.let { if (it.isGroup == true) SignalLocalMetrics.GroupMessageSend.start() else SignalLocalMetrics.IndividualMessageSend.start() }
val metricId = viewModel.recipientSnapshot?.let { if (it.isGroup) SignalLocalMetrics.GroupMessageSend.start() else SignalLocalMetrics.IndividualMessageSend.start() }
val send: Completable = viewModel.sendMessage(
metricId = metricId,
@ -2511,7 +2601,11 @@ class ConversationFragment :
)
}
override fun onMediaSend(result: MediaSendActivityResult) {
override fun onMediaSend(result: MediaSendActivityResult?) {
if (result == null) {
return
}
val recipientSnapshot = viewModel.recipientSnapshot
if (result.recipientId != recipientSnapshot?.id) {
Log.w(TAG, "Result's recipientId did not match ours! Result: " + result.recipientId + ", Ours: " + recipientSnapshot?.id)
@ -2955,7 +3049,7 @@ class ConversationFragment :
}
override fun onEmojiToggle() {
// TODO [cfv2] Not yet implemented
container.toggleInput(MediaKeyboardFragmentCreator, composeText, showSoftKeyOnHide = true)
}
override fun onLinkPreviewCanceled() {
@ -3032,6 +3126,11 @@ class ConversationFragment :
}
}
private object MediaKeyboardFragmentCreator : InputAwareConstraintLayout.FragmentCreator {
override val id: Int = 2
override fun create(): Fragment = KeyboardPagerFragment()
}
private inner class KeyboardEvents : OnBackPressedCallback(false), InputAwareConstraintLayout.Listener {
override fun handleOnBackPressed() {
container.hideInput()

View file

@ -68,6 +68,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.Quote
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.database.model.StickerRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob
@ -102,6 +103,7 @@ import org.thoughtcrime.securesms.util.requireTextSlide
import java.io.IOException
import java.util.Optional
import kotlin.math.max
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
class ConversationRepository(
@ -225,7 +227,8 @@ class ConversationRepository(
messageToEdit = messageToEdit?.id ?: 0,
mentions = mentions,
sharedContacts = contacts,
linkPreviews = linkPreviews
linkPreviews = linkPreviews,
attachments = slideDeck?.asAttachments() ?: emptyList()
)
if (preUploadResults.isEmpty()) {
@ -551,6 +554,12 @@ class ConversationRepository(
}
}
fun updateStickerLastUsedTime(stickerRecord: StickerRecord, timestamp: Duration) {
SignalExecutors.BOUNDED_IO.execute {
SignalDatabase.stickers.updateStickerLastUsedTime(stickerRecord.rowId, timestamp.inWholeMilliseconds)
}
}
/**
* Glide target for a contact photo which expects an error drawable, and publishes
* the result to the given emitter.

View file

@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.Quote
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.database.model.StickerRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
@ -62,6 +63,7 @@ import org.thoughtcrime.securesms.util.hasGiftBadge
import org.thoughtcrime.securesms.util.rx.RxStore
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import java.util.Optional
import kotlin.time.Duration
/**
* ConversationViewModel, which operates solely off of a thread id that never changes.
@ -362,4 +364,8 @@ class ConversationViewModel(
fun deleteSlideData(slides: List<Slide>) {
repository.deleteSlideData(slides)
}
fun updateStickerLastUsedTime(stickerRecord: StickerRecord, timestamp: Duration) {
repository.updateStickerLastUsedTime(stickerRecord, timestamp)
}
}

View file

@ -12,6 +12,7 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.concurrent.addTo
@ -62,6 +63,7 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_
conversationViewModel = ViewModelProvider(requireParentFragment()).get(ConversationViewModel::class.java)
conversationViewModel
.recipient
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
attachmentKeyboardView.setWallpaperEnabled(it.hasWallpaper())
}

View file

@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.InputAwareConstraintLayout
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
import org.thoughtcrime.securesms.keyboard.gif.GifKeyboardPageFragment
@ -20,7 +21,7 @@ import org.thoughtcrime.securesms.util.fragments.findListener
import org.thoughtcrime.securesms.util.visible
import kotlin.reflect.KClass
class KeyboardPagerFragment : Fragment() {
class KeyboardPagerFragment : Fragment(), InputAwareConstraintLayout.InputFragment {
private lateinit var emojiButton: View
private lateinit var stickerButton: View
@ -113,7 +114,8 @@ class KeyboardPagerFragment : Fragment() {
transaction.commitAllowingStateLoss()
}
fun show() {
override fun show() {
findListener<MediaKeyboard.MediaKeyboardListener>()?.onShown()
if (isAdded && view != null) {
onHiddenChanged(false)
@ -121,7 +123,8 @@ class KeyboardPagerFragment : Fragment() {
}
}
fun hide() {
override fun hide() {
findListener<MediaKeyboard.MediaKeyboardListener>()?.onHidden()
if (isAdded && view != null) {
onHiddenChanged(true)