Plumb schedule message and edit message send flows for CFv2.

This commit is contained in:
Cody Henthorne 2023-06-29 14:41:00 -04:00 committed by Greyson Parrelli
parent 9b1917cbdc
commit a17800283a
5 changed files with 206 additions and 32 deletions

View file

@ -142,7 +142,7 @@ class DraftRepository(
val messageEdit: ConversationMessage? = drafts.firstOrNull { it.type == DraftTable.Draft.MESSAGE_EDIT }?.let { loadDraftMessageEditInternal(it.value) }
if (messageEdit != null) {
return ShareOrDraftData.SetEditMessage(messageEdit) to drafts
return ShareOrDraftData.SetEditMessage(messageEdit, draftText) to drafts
}
if (draftText != null) {
@ -287,7 +287,7 @@ class DraftRepository(
data class SetText(val text: CharSequence) : ShareOrDraftData
data class SetLocation(val location: SignalPlace, val draftText: CharSequence?) : ShareOrDraftData
data class SetQuote(val quote: ConversationMessage, val draftText: CharSequence?) : ShareOrDraftData
data class SetEditMessage(val messageEdit: ConversationMessage) : ShareOrDraftData
data class SetEditMessage(val messageEdit: ConversationMessage, val draftText: CharSequence?) : ShareOrDraftData
}
data class KeyboardImageDetails(val width: Int, val height: Int, val hasTransparency: Boolean)

View file

@ -130,6 +130,7 @@ 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.ConversationBottomSheetCallback
import org.thoughtcrime.securesms.conversation.ConversationHeaderView
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.ConversationIntents.ConversationScreenType
@ -147,6 +148,13 @@ import org.thoughtcrime.securesms.conversation.MarkReadHelper
import org.thoughtcrime.securesms.conversation.MenuState
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.conversation.MessageStyler.getStyling
import org.thoughtcrime.securesms.conversation.ReenableScheduledMessagesDialogFragment
import org.thoughtcrime.securesms.conversation.ScheduleMessageContextMenu
import org.thoughtcrime.securesms.conversation.ScheduleMessageDialogCallback
import org.thoughtcrime.securesms.conversation.ScheduleMessageTimePickerBottomSheet
import org.thoughtcrime.securesms.conversation.ScheduleMessageTimePickerBottomSheet.Companion.showSchedule
import org.thoughtcrime.securesms.conversation.ScheduledMessagesBottomSheet
import org.thoughtcrime.securesms.conversation.ScheduledMessagesRepository
import org.thoughtcrime.securesms.conversation.SelectedConversationModel
import org.thoughtcrime.securesms.conversation.ShowAdminsBottomSheetDialog
import org.thoughtcrime.securesms.conversation.colors.ChatColors
@ -272,6 +280,8 @@ import org.thoughtcrime.securesms.util.DrawableUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.MessageConstraintsUtil.getEditMessageThresholdHours
import org.thoughtcrime.securesms.util.MessageConstraintsUtil.isValidEditMessageSend
import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.SaveAttachmentUtil
import org.thoughtcrime.securesms.util.SignalLocalMetrics
@ -311,7 +321,10 @@ class ConversationFragment :
StickerEventListener,
StickerKeyboardPageFragment.Callback,
MediaKeyboard.MediaKeyboardListener,
EmojiSearchFragment.Callback {
EmojiSearchFragment.Callback,
ScheduleMessageTimePickerBottomSheet.ScheduleCallback,
ScheduleMessageDialogCallback,
ConversationBottomSheetCallback {
companion object {
private val TAG = Log.tag(ConversationFragment::class.java)
@ -340,7 +353,8 @@ class ConversationFragment :
requestedStartingPosition = args.startingPosition,
repository = ConversationRepository(context = requireContext(), isInBubble = args.conversationScreenType == ConversationScreenType.BUBBLE),
recipientRepository = conversationRecipientRepository,
messageRequestRepository = messageRequestRepository
messageRequestRepository = messageRequestRepository,
scheduledMessagesRepository = ScheduledMessagesRepository()
)
}
@ -419,6 +433,7 @@ class ConversationFragment :
private var searchMenuItem: MenuItem? = null
private var isSearchRequested: Boolean = false
private var previousPages: Set<KeyboardPage>? = null
private var reShowScheduleMessagesBar: Boolean = false
private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy {
override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) {
@ -452,9 +467,13 @@ class ConversationFragment :
private val searchNav: ConversationSearchBottomBar
get() = binding.conversationSearchBottomBar.root
private val scheduledMessagesStub: Stub<View> by lazy { Stub(binding.scheduledMessagesStub) }
private lateinit var reactionDelegate: ConversationReactionDelegate
private lateinit var voiceMessageRecordingDelegate: VoiceMessageRecordingDelegate
//region Android Lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SignalLocalMetrics.ConversationOpen.start()
@ -548,6 +567,23 @@ class ConversationFragment :
}
}
//endregion
//region Fragment callbacks and listeners
override fun getConversationAdapterListener(): ConversationAdapter.ItemClickListener {
return adapter.clickListener
}
override fun jumpToMessage(messageRecord: MessageRecord) {
viewModel
.moveToMessage(messageRecord)
.subscribeBy {
moveToPosition(it)
}
.addTo(disposables)
}
override fun onReactWithAnyEmojiDialogDismissed() {
reactionDelegate.hide()
}
@ -639,6 +675,16 @@ class ConversationFragment :
inputPanel.mediaKeyboardListener.onKeyboardChanged(page)
}
override fun onScheduleSend(scheduledTime: Long) {
sendMessage(scheduledDate = scheduledTime)
}
override fun onSchedulePermissionsGranted(metricId: String?, scheduledDate: Long) {
sendMessage(scheduledDate = scheduledDate)
}
//endregion
private fun observeConversationThread() {
var firstRender = true
disposables += viewModel
@ -722,10 +768,13 @@ class ConversationFragment :
sendButton.apply {
setOnClickListener(sendButtonListener)
setScheduledSendListener(sendButtonListener)
isEnabled = true
post { sendButton.triggerSelectedChangedEvent() }
}
sendEditButton.setOnClickListener { handleSendEditMessage() }
val attachListener = { _: View ->
container.toggleInput(AttachmentKeyboardFragmentCreator, composeText)
}
@ -795,29 +844,7 @@ class ConversationFragment :
).attachToRecyclerView(binding.conversationItemRecycler)
draftViewModel.loadShareOrDraftData()
.subscribeBy {
when (it) {
is ShareOrDraftData.SendKeyboardImage -> sendMessageWithoutComposeInput(slide = it.slide, clearCompose = false)
is ShareOrDraftData.SendSticker -> sendMessageWithoutComposeInput(slide = it.slide, clearCompose = true)
is ShareOrDraftData.SetEditMessage -> inputPanel.enterEditMessageMode(GlideApp.with(this), it.messageEdit, true)
is ShareOrDraftData.SetLocation -> attachmentManager.setLocation(it.location, MediaConstraints.getPushMediaConstraints())
is ShareOrDraftData.SetMedia -> {
composeText.setDraftText(it.text)
setMedia(it.media, it.mediaType)
}
is ShareOrDraftData.SetQuote -> {
composeText.setDraftText(it.draftText)
handleReplyToMessage(it.quote)
}
is ShareOrDraftData.SetText -> composeText.setDraftText(it.text)
is ShareOrDraftData.StartSendMedia -> {
val recipientId = viewModel.recipientSnapshot?.id ?: return@subscribeBy
conversationActivityResultContracts.launchMediaEditor(it.mediaList, recipientId, it.text)
}
}
}
.subscribeBy { data -> handleShareOrDraftData(data) }
.addTo(disposables)
initializeSearch()
@ -826,6 +853,11 @@ class ConversationFragment :
initializeInlineSearch()
inputPanel.setListener(InputPanelListener())
viewModel
.getScheduledMessagesCount()
.subscribeBy { count -> handleScheduledMessagesCountChange(count) }
.addTo(disposables)
}
private fun initializeInlineSearch() {
@ -1126,6 +1158,88 @@ class ConversationFragment :
}
}
private fun handleShareOrDraftData(data: ShareOrDraftData) {
when (data) {
is ShareOrDraftData.SendKeyboardImage -> sendMessageWithoutComposeInput(slide = data.slide, clearCompose = false)
is ShareOrDraftData.SendSticker -> sendMessageWithoutComposeInput(slide = data.slide, clearCompose = true)
is ShareOrDraftData.SetEditMessage -> {
composeText.setDraftText(data.draftText)
inputPanel.enterEditMessageMode(GlideApp.with(this), data.messageEdit, true)
}
is ShareOrDraftData.SetLocation -> attachmentManager.setLocation(data.location, MediaConstraints.getPushMediaConstraints())
is ShareOrDraftData.SetMedia -> {
composeText.setDraftText(data.text)
setMedia(data.media, data.mediaType)
}
is ShareOrDraftData.SetQuote -> {
composeText.setDraftText(data.draftText)
handleReplyToMessage(data.quote)
}
is ShareOrDraftData.SetText -> composeText.setDraftText(data.text)
is ShareOrDraftData.StartSendMedia -> {
val recipientId = viewModel.recipientSnapshot?.id ?: return
conversationActivityResultContracts.launchMediaEditor(data.mediaList, recipientId, data.text)
}
}
}
private fun handleScheduledMessagesCountChange(count: Int) {
if (count <= 0) {
scheduledMessagesStub.visibility = View.GONE
reShowScheduleMessagesBar = false
} else {
scheduledMessagesStub.get().apply {
visibility = View.VISIBLE
findViewById<View>(R.id.scheduled_messages_show_all)
.setOnClickListener {
val recipient = viewModel.recipientSnapshot ?: return@setOnClickListener
ScheduledMessagesBottomSheet.show(childFragmentManager, args.threadId, recipient.id)
}
findViewById<TextView>(R.id.scheduled_messages_text).text = resources.getQuantityString(R.plurals.conversation_scheduled_messages_bar__number_of_messages, count, count)
}
reShowScheduleMessagesBar = true
}
}
private fun handleSendEditMessage() {
if (!FeatureFlags.editMessageSending()) {
Log.w(TAG, "Edit message sending disabled, forcing exit of edit mode")
inputPanel.exitEditMessageMode()
return
}
if (!inputPanel.inEditMessageMode()) {
Log.w(TAG, "Not in edit message mode, unknown state, forcing re-exit")
inputPanel.exitEditMessageMode()
return
}
if (SignalStore.uiHints().hasNotSeenEditMessageBetaAlert()) {
Dialogs.showEditMessageBetaDialog(requireContext()) { handleSendEditMessage() }
return
}
val editMessage = inputPanel.editMessage
if (editMessage == null) {
Log.w(TAG, "No edit message found, forcing exit")
inputPanel.exitEditMessageMode()
return
}
if (!isValidEditMessageSend(editMessage, System.currentTimeMillis())) {
Log.i(TAG, "Edit message no longer valid")
val editDurationHours = getEditMessageThresholdHours()
Dialogs.showAlertDialog(requireContext(), null, resources.getQuantityString(R.plurals.ConversationActivity_edit_message_too_old, editDurationHours, editDurationHours))
return
}
sendMessage()
}
private fun getVoiceNoteMediaController() = requireListener<VoiceNoteMediaControllerOwner>().voiceNoteMediaController
private fun initializeConversationThreadUi() {
@ -1385,6 +1499,10 @@ class ConversationFragment :
preUploadResults: List<MessageSender.PreUploadResult> = emptyList(),
afterSendComplete: () -> Unit = {}
) {
if (scheduledDate != -1L && ReenableScheduledMessagesDialogFragment.showIfNeeded(requireContext(), childFragmentManager, null, scheduledDate)) {
return
}
val metricId = viewModel.recipientSnapshot?.let { if (it.isGroup) SignalLocalMetrics.GroupMessageSend.start() else SignalLocalMetrics.IndividualMessageSend.start() }
val send: Completable = viewModel.sendMessage(
@ -2664,7 +2782,14 @@ class ConversationFragment :
inner class ActionModeCallback : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.title = calculateSelectedItemCount()
// TODO [cfv2] scheduled message - listener.onMessageActionToolbarOpened();
searchMenuItem?.collapseActionView()
binding.toolbar.visible = false
if (scheduledMessagesStub.isVisible) {
reShowScheduleMessagesBar = true
scheduledMessagesStub.visibility = View.GONE
}
setCorrectActionModeMenuVisibility()
return true
}
@ -2676,7 +2801,13 @@ class ConversationFragment :
override fun onDestroyActionMode(mode: ActionMode) {
adapter.clearSelection()
setBottomActionBarVisibility(false)
// TODO [cfv2] scheduled message - listener.onMessageActionToolbarClosed();
binding.toolbar.visible = true
if (reShowScheduleMessagesBar) {
scheduledMessagesStub.visibility = View.VISIBLE
reShowScheduleMessagesBar = false
}
binding.conversationItemRecycler.invalidateItemDecorations()
actionMode = null
}
@ -2997,7 +3128,7 @@ class ConversationFragment :
//region Compose + Send Callbacks
private inner class SendButtonListener : View.OnClickListener, OnEditorActionListener {
private inner class SendButtonListener : View.OnClickListener, OnEditorActionListener, SendButton.ScheduledSendListener {
override fun onClick(v: View) {
sendMessage()
}
@ -3013,6 +3144,20 @@ class ConversationFragment :
}
return false
}
override fun onSendScheduled() {
ScheduleMessageContextMenu.show(sendButton, (requireView() as ViewGroup)) { time ->
if (time == -1L) {
showSchedule(childFragmentManager)
} else {
sendMessage(scheduledDate = time)
}
}
}
override fun canSchedule(): Boolean {
return !(inputPanel.isRecordingInLockedMode || draftViewModel.voiceNoteDraft != null)
}
}
private inner class ComposeTextEventsListener :

View file

@ -289,6 +289,12 @@ class ConversationRepository(
}.subscribeOn(Schedulers.io())
}
fun getMessagePosition(threadId: Long, messageRecord: MessageRecord): Single<Int> {
return Single.fromCallable {
SignalDatabase.messages.getMessagePositionInConversation(threadId, messageRecord.dateReceived)
}.subscribeOn(Schedulers.io())
}
fun getMessageCounts(threadId: Long): Flowable<MessageCounts> {
return RxDatabaseObserver.conversationList
.map { getUnreadCount(threadId) }

View file

@ -30,6 +30,7 @@ import org.signal.paging.ProxyPagingController
import org.thoughtcrime.securesms.components.reminder.Reminder
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.ScheduledMessagesRepository
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
@ -74,7 +75,8 @@ class ConversationViewModel(
requestedStartingPosition: Int,
private val repository: ConversationRepository,
recipientRepository: ConversationRecipientRepository,
messageRequestRepository: MessageRequestRepository
messageRequestRepository: MessageRequestRepository,
private val scheduledMessagesRepository: ScheduledMessagesRepository
) : ViewModel() {
private val disposables = CompositeDisposable()
@ -246,6 +248,11 @@ class ConversationViewModel(
return repository.getNextMentionPosition(threadId)
}
fun moveToMessage(messageRecord: MessageRecord): Single<Int> {
return repository.getMessagePosition(threadId, messageRecord)
.observeOn(AndroidSchedulers.mainThread())
}
fun setLastScrolled(lastScrolledTimestamp: Long) {
repository.setLastVisibleMessageTimestamp(
threadId,
@ -379,4 +386,10 @@ class ConversationViewModel(
fun updateStickerLastUsedTime(stickerRecord: StickerRecord, timestamp: Duration) {
repository.updateStickerLastUsedTime(stickerRecord, timestamp)
}
fun getScheduledMessagesCount(): Observable<Int> {
return scheduledMessagesRepository
.getScheduledMessageCount(threadId)
.observeOn(AndroidSchedulers.mainThread())
}
}

View file

@ -170,7 +170,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="conversation_input_panel,attachment_editor_stub,conversation_disabled_input " />
app:constraint_referenced_ids="conversation_input_panel,attachment_editor_stub,attachment_editor,conversation_disabled_input,scheduled_messages_stub,scheduled_messages" />
<ViewStub
android:id="@+id/attachment_editor_stub"
@ -182,6 +182,16 @@
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
app:layout_constraintStart_toStartOf="@id/parent_start_guideline" />
<ViewStub
android:id="@+id/scheduled_messages_stub"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inflatedId="@+id/scheduled_messages"
android:layout="@layout/conversation_activity_scheduled_messages_stub"
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"