From 7b13550086d55d67d5ca1c7151ddbd863a9ce2d0 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 29 Nov 2022 13:12:56 -0400 Subject: [PATCH] Add entry points for adding to a group story. --- .../ConversationSettingsFragment.kt | 14 ++ .../ConversationSettingsViewModel.kt | 3 +- .../preferences/ButtonStripPreference.kt | 6 + .../mediasend/v2/MediaSelectionActivity.kt | 23 ++- .../mediasend/v2/MediaSelectionDestination.kt | 12 ++ .../mediasend/v2/MediaSelectionRepository.kt | 2 + .../mediasend/v2/MediaSelectionViewModel.kt | 10 +- .../v2/review/MediaReviewFragment.kt | 9 +- .../v2/text/TextStoryPostCreationFragment.kt | 7 + .../RecipientBottomSheetDialogFragment.java | 16 +- .../stories/viewer/AddToGroupStoryDelegate.kt | 161 ++++++++++++++++++ .../viewer/page/StoryViewerPageFragment.kt | 14 +- app/src/main/res/drawable/add_to_story_16.xml | 20 +++ app/src/main/res/drawable/add_to_story_24.xml | 20 +++ .../conversation_settings_button_strip.xml | 23 +++ .../layout/stories_viewer_fragment_page.xml | 36 ++++ .../res/values-night/material3_colors.xml | 1 + app/src/main/res/values/material3_colors.xml | 1 + app/src/main/res/values/strings.xml | 11 ++ 19 files changed, 372 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/viewer/AddToGroupStoryDelegate.kt create mode 100644 app/src/main/res/drawable/add_to_story_16.xml create mode 100644 app/src/main/res/drawable/add_to_story_24.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index ac6ed5abb..6a7a83900 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheet import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.StoryViewerArgs import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs +import org.thoughtcrime.securesms.stories.viewer.AddToGroupStoryDelegate import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.ContextUtil @@ -137,6 +138,7 @@ class ConversationSettingsFragment : DSLSettingsFragment( private lateinit var toolbarBadge: BadgeImageView private lateinit var toolbarTitle: TextView private lateinit var toolbarBackground: View + private lateinit var addToGroupStoryDelegate: AddToGroupStoryDelegate private val navController get() = Navigation.findNavController(requireView()) @@ -221,6 +223,7 @@ class ConversationSettingsFragment : DSLSettingsFragment( } } + addToGroupStoryDelegate = AddToGroupStoryDelegate(this) viewModel.state.observe(viewLifecycleOwner) { state -> if (state.recipient != Recipient.UNKNOWN) { @@ -368,6 +371,17 @@ class ConversationSettingsFragment : DSLSettingsFragment( customPref( ButtonStripPreference.Model( state = state.buttonStripState, + onAddToStoryClick = { + if (state.recipient.isPushV2Group && state.requireGroupSettingsState().isAnnouncementGroup && !state.requireGroupSettingsState().isSelfAdmin) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.ConversationSettingsFragment__cant_add_to_group_story) + .setMessage(R.string.ConversationSettingsFragment__only_admins_of_this_group_can_add_to_its_story) + .setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() } + .show() + } else { + addToGroupStoryDelegate.addToStory(state.recipient.id) + } + }, onVideoClick = { if (state.recipient.isPushV2Group && state.requireGroupSettingsState().isAnnouncementGroup && !state.requireGroupSettingsState().isSelfAdmin) { MaterialAlertDialogBuilder(requireContext()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt index bd47e28f0..70ef4263d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt @@ -272,7 +272,8 @@ sealed class ConversationSettingsViewModel( isAudioSecure = recipient.isPushV2Group, isMuted = recipient.isMuted, isMuteAvailable = true, - isSearchAvailable = true + isSearchAvailable = true, + isAddToStoryAvailable = recipient.isPushV2Group && !recipient.isBlocked && isActive ), canModifyBlockedState = RecipientUtil.isBlockable(recipient), specificSettingsState = state.requireGroupSettingsState().copy( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/ButtonStripPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/ButtonStripPreference.kt index e94faddc6..e4d7d298e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/ButtonStripPreference.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/ButtonStripPreference.kt @@ -24,6 +24,7 @@ object ButtonStripPreference { class Model( val state: State, val background: DSLSettingsIcon? = null, + val onAddToStoryClick: () -> Unit = {}, val onMessageClick: () -> Unit = {}, val onVideoClick: () -> Unit = {}, val onAudioClick: () -> Unit = {}, @@ -41,6 +42,8 @@ object ButtonStripPreference { class ViewHolder(itemView: View) : MappingViewHolder(itemView) { + private val addToStory: View = itemView.findViewById(R.id.add_to_story) + private val addToStoryContainer: View = itemView.findViewById(R.id.button_strip_add_to_story_container) private val message: View = itemView.findViewById(R.id.message) private val messageContainer: View = itemView.findViewById(R.id.button_strip_message_container) private val videoCall: View = itemView.findViewById(R.id.start_video) @@ -60,6 +63,7 @@ object ButtonStripPreference { audioContainer.visible = model.state.isAudioAvailable muteContainer.visible = model.state.isMuteAvailable searchContainer.visible = model.state.isSearchAvailable + addToStoryContainer.visible = model.state.isAddToStoryAvailable if (model.state.isAudioSecure) { audioLabel.setText(R.string.ConversationSettingsFragment__audio) @@ -88,6 +92,7 @@ object ButtonStripPreference { audioCall.setOnClickListener { model.onAudioClick() } mute.setOnClickListener { model.onMuteClick() } search.setOnClickListener { model.onSearchClick() } + addToStory.setOnClickListener { model.onAddToStoryClick() } } } @@ -99,5 +104,6 @@ object ButtonStripPreference { val isSearchAvailable: Boolean = false, val isAudioSecure: Boolean = false, val isMuted: Boolean = false, + val isAddToStoryAvailable: Boolean = false ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt index 3eb3bd15b..5fb86b138 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt @@ -93,8 +93,9 @@ class MediaSelectionActivity : val initialMedia: List = intent.getParcelableArrayListExtra(MEDIA) ?: listOf() val message: CharSequence? = if (shareToTextStory) null else draftText val isReply: Boolean = intent.getBooleanExtra(IS_REPLY, false) + val isAddToGroupStoryFlow: Boolean = intent.getBooleanExtra(IS_ADD_TO_GROUP_STORY_FLOW, false) - val factory = MediaSelectionViewModel.Factory(destination, sendType, initialMedia, message, isReply, isStory, MediaSelectionRepository(this)) + val factory = MediaSelectionViewModel.Factory(destination, sendType, initialMedia, message, isReply, isStory, isAddToGroupStoryFlow, MediaSelectionRepository(this)) viewModel = ViewModelProvider(this, factory)[MediaSelectionViewModel::class.java] val textStoryToggle: ConstraintLayout = findViewById(R.id.switch_widget) @@ -221,7 +222,7 @@ class MediaSelectionActivity : return Stories.isFeatureEnabled() && isCameraFirst() && !viewModel.hasSelectedMedia() && - destination == MediaSelectionDestination.ChooseAfterMediaSelection + (destination == MediaSelectionDestination.ChooseAfterMediaSelection || destination is MediaSelectionDestination.SingleStory) } override fun onSaveInstanceState(outState: Bundle) { @@ -348,6 +349,7 @@ class MediaSelectionActivity : private const val IS_REPLY = "is_reply" private const val IS_STORY = "is_story" private const val AS_TEXT_STORY = "as_text_story" + private const val IS_ADD_TO_GROUP_STORY_FLOW = "is_add_to_group_story_flow" @JvmStatic fun camera(context: Context): Intent { @@ -363,6 +365,19 @@ class MediaSelectionActivity : ) } + fun addToGroupStory( + context: Context, + recipientId: RecipientId + ): Intent { + return buildIntent( + context = context, + startAction = R.id.action_directly_to_mediaCaptureFragment, + isStory = true, + isAddToGroupStoryFlow = true, + destination = MediaSelectionDestination.SingleStory(recipientId) + ) + } + @JvmStatic fun camera( context: Context, @@ -457,7 +472,8 @@ class MediaSelectionActivity : message: CharSequence? = null, isReply: Boolean = false, isStory: Boolean = false, - asTextStory: Boolean = false + asTextStory: Boolean = false, + isAddToGroupStoryFlow: Boolean = false ): Intent { return Intent(context, MediaSelectionActivity::class.java).apply { putExtra(START_ACTION, startAction) @@ -468,6 +484,7 @@ class MediaSelectionActivity : putExtra(IS_REPLY, isReply) putExtra(IS_STORY, isStory) putExtra(AS_TEXT_STORY, asTextStory) + putExtra(IS_ADD_TO_GROUP_STORY_FLOW, isAddToGroupStoryFlow) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionDestination.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionDestination.kt index c1c5da987..b9e730ec9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionDestination.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionDestination.kt @@ -38,6 +38,16 @@ sealed class MediaSelectionDestination { } } + class SingleStory(private val id: RecipientId) : MediaSelectionDestination() { + override fun getRecipientSearchKey(): ContactSearchKey.RecipientSearchKey = ContactSearchKey.RecipientSearchKey.Story(id) + + override fun toBundle(): Bundle { + return Bundle().apply { + putParcelable(STORY, id) + } + } + } + class MultipleRecipients(val recipientSearchKeys: List) : MediaSelectionDestination() { companion object { @@ -72,6 +82,7 @@ sealed class MediaSelectionDestination { private const val WALLPAPER = "wallpaper" private const val AVATAR = "avatar" private const val RECIPIENT = "recipient" + private const val STORY = "story" private const val RECIPIENT_LIST = "recipient_list" fun fromBundle(bundle: Bundle): MediaSelectionDestination { @@ -79,6 +90,7 @@ sealed class MediaSelectionDestination { bundle.containsKey(WALLPAPER) -> Wallpaper bundle.containsKey(AVATAR) -> Avatar bundle.containsKey(RECIPIENT) -> SingleRecipient(requireNotNull(bundle.getParcelable(RECIPIENT))) + bundle.containsKey(STORY) -> SingleStory(requireNotNull(bundle.getParcelable(STORY))) bundle.containsKey(RECIPIENT_LIST) -> MultipleRecipients.fromParcel(requireNotNull(bundle.getParcelableArrayList(RECIPIENT_LIST))) else -> ChooseAfterMediaSelection } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt index f7bf4376c..97f579f45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt @@ -105,6 +105,8 @@ class MediaSelectionRepository(context: Context) { val singleRecipient: Recipient? = singleContact?.let { Recipient.resolved(it.recipientId) } val storyType: StoryType = if (singleRecipient?.isDistributionList == true) { SignalDatabase.distributionLists.getStoryType(singleRecipient.requireDistributionListId()) + } else if (singleRecipient?.isGroup == true && singleContact.isStory) { + StoryType.STORY_WITH_REPLIES } else { StoryType.NONE } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt index d5473603a..cf9f40607 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt @@ -45,6 +45,7 @@ class MediaSelectionViewModel( initialMessage: CharSequence?, val isReply: Boolean, isStory: Boolean, + val isAddToGroupStoryFlow: Boolean, private val repository: MediaSelectionRepository, private val identityChangesSince: Long = System.currentTimeMillis() ) : ViewModel() { @@ -360,10 +361,10 @@ class MediaSelectionViewModel( return } - val filteredPreUploadMedia = if (Stories.isFeatureEnabled()) { - media.filter { Stories.MediaTransform.canPreUploadMedia(it) } - } else { + val filteredPreUploadMedia = if (destination is MediaSelectionDestination.SingleRecipient || !Stories.isFeatureEnabled()) { media + } else { + media.filter { Stories.MediaTransform.canPreUploadMedia(it) } } repository.uploadRepository.startUpload(filteredPreUploadMedia, store.state.recipient) @@ -482,10 +483,11 @@ class MediaSelectionViewModel( private val initialMessage: CharSequence?, private val isReply: Boolean, private val isStory: Boolean, + private val isAddToGroupStoryFlow: Boolean, private val repository: MediaSelectionRepository ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return requireNotNull(modelClass.cast(MediaSelectionViewModel(destination, sendType, initialMedia, initialMessage, isReply, isStory, repository))) + return requireNotNull(modelClass.cast(MediaSelectionViewModel(destination, sendType, initialMedia, initialMessage, isReply, isStory, isAddToGroupStoryFlow, repository))) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt index 331c8ccfe..f6ec87a4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt @@ -23,6 +23,7 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import app.cash.exhaustive.Exhaustive +import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import org.signal.core.util.concurrent.SimpleTask import org.thoughtcrime.securesms.R @@ -200,6 +201,12 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) { } else { multiselectLauncher.launch(args) } + } else if (sharedViewModel.isAddToGroupStoryFlow) { + MaterialAlertDialogBuilder(requireContext()) + .setMessage(getString(R.string.MediaReviewFragment__add_to_the_group_story, sharedViewModel.state.value!!.recipient!!.getDisplayName(requireContext()))) + .setPositiveButton(R.string.MediaReviewFragment__add_to_story) { _, _ -> performSend() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() } else { performSend() } @@ -317,7 +324,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) { .setInterpolator(MediaAnimations.interpolator) .alpha(1f) - sharedViewModel + disposables += sharedViewModel .send(selection.filterIsInstance(ContactSearchKey.RecipientSearchKey::class.java)) .subscribe( { result -> callback.onSentWithResult(result) }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt index 8a51b7f23..6e4dac8de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt @@ -12,6 +12,7 @@ import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey @@ -157,6 +158,12 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati ) ) } + } else if (sharedViewModel.isAddToGroupStoryFlow) { + MaterialAlertDialogBuilder(requireContext()) + .setMessage(getString(R.string.MediaReviewFragment__add_to_the_group_story, sharedViewModel.state.value!!.recipient!!.getDisplayName(requireContext()))) + .setPositiveButton(R.string.MediaReviewFragment__add_to_story) { _, _ -> performSend(contacts) } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() } else { performSend(contacts) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java index 90dc8b476..00e8958a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java @@ -231,18 +231,20 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF !recipient.isReleaseNotes(); ButtonStripPreference.State buttonStripState = new ButtonStripPreference.State( - /* isMessageAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && !recipient.isReleaseNotes(), - /* isVideoAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && recipient.isRegistered(), - /* isAudioAvailable = */ isAudioAvailable, - /* isMuteAvailable = */ false, - /* isSearchAvailable = */ false, - /* isAudioSecure = */ recipient.isRegistered(), - /* isMuted = */ false + /* isMessageAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && !recipient.isReleaseNotes(), + /* isVideoAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && recipient.isRegistered(), + /* isAudioAvailable = */ isAudioAvailable, + /* isMuteAvailable = */ false, + /* isSearchAvailable = */ false, + /* isAudioSecure = */ recipient.isRegistered(), + /* isMuted = */ false, + /* isAddToStoryAvailable = */ false ); ButtonStripPreference.Model buttonStripModel = new ButtonStripPreference.Model( buttonStripState, DSLSettingsIcon.from(ContextUtil.requireDrawable(requireContext(), R.drawable.selectable_recipient_bottom_sheet_icon_button)), + () -> Unit.INSTANCE, () -> { dismiss(); viewModel.onMessageClicked(requireActivity()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/AddToGroupStoryDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/AddToGroupStoryDelegate.kt new file mode 100644 index 000000000..86b6a1d2c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/AddToGroupStoryDelegate.kt @@ -0,0 +1,161 @@ +package org.thoughtcrime.securesms.stories.viewer + +import android.content.Intent +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.CheckResult +import androidx.annotation.WorkerThread +import androidx.fragment.app.Fragment +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.kotlin.subscribeBy +import io.reactivex.rxjava3.subjects.CompletableSubject +import org.signal.core.util.concurrent.SignalExecutors +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.ThreadTable +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult +import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity +import org.thoughtcrime.securesms.mms.OutgoingMediaMessage +import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage +import org.thoughtcrime.securesms.mms.SlideDeck +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.sharing.MultiShareArgs +import org.thoughtcrime.securesms.sharing.MultiShareSender +import org.thoughtcrime.securesms.sms.MessageSender +import org.thoughtcrime.securesms.util.LifecycleDisposable + +/** + * Delegate for dealing with sending stories directly to a group. + */ +class AddToGroupStoryDelegate( + private val fragment: Fragment +) { + + companion object { + private val TAG = Log.tag(AddToGroupStoryDelegate::class.java) + } + + private val lifecycleDisposable = LifecycleDisposable().apply { + bindTo(fragment.viewLifecycleOwner) + } + + private val addToStoryLauncher: ActivityResultLauncher = fragment.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val data = result.data + if (data == null) { + Log.d(TAG, "No result data.") + } else { + Log.d(TAG, "Processing result...") + val mediaSelectionResult: MediaSendActivityResult = MediaSendActivityResult.fromData(data) + handleResult(mediaSelectionResult) + } + } + + fun addToStory(recipientId: RecipientId) { + val addToStoryIntent = MediaSelectionActivity.addToGroupStory( + fragment.requireContext(), + recipientId + ) + + addToStoryLauncher.launch(addToStoryIntent) + } + + private fun handleResult(result: MediaSendActivityResult) { + lifecycleDisposable += ResultHandler.handleResult(result) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy { + Toast.makeText(fragment.requireContext(), R.string.TextStoryPostCreationFragment__sent_story, Toast.LENGTH_SHORT).show() + } + } + + /** + * Dispatches the send result on a background thread, isolated from the fragment. + */ + private object ResultHandler { + + /** + * Handles the result, completing after sending the message. + */ + @CheckResult + fun handleResult(result: MediaSendActivityResult): Completable { + Log.d(TAG, "Dispatching result handler.") + val subject = CompletableSubject.create() + SignalExecutors.BOUNDED_IO.execute { + if (result.isPushPreUpload) { + sendPreUploadedMedia(result) + } else { + sendNonPreUploadedMedia(result) + } + + subject.onComplete() + } + + return subject + } + + @WorkerThread + private fun sendPreUploadedMedia(result: MediaSendActivityResult) { + Log.d(TAG, "Sending preupload media.") + + val recipient = Recipient.resolved(result.recipientId) + val secureMessage = OutgoingSecureMediaMessage( + OutgoingMediaMessage( + Recipient.resolved(result.recipientId), + SlideDeck(), + "", + System.currentTimeMillis(), + -1, + 0, + false, + ThreadTable.DistributionTypes.DEFAULT, + result.storyType, + null, + false, + null, + emptyList(), + emptyList(), + result.mentions.toList(), + null + ) + ) + + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + if (result.body.isNotEmpty()) { + result.preUploadResults.forEach { + SignalDatabase.attachments.updateAttachmentCaption(it.attachmentId, result.body) + } + } + + MessageSender.sendPushWithPreUploadedMedia( + ApplicationDependencies.getApplication(), + secureMessage, + result.preUploadResults, + threadId + ) { + Log.d(TAG, "Sent.") + } + } + + @WorkerThread + private fun sendNonPreUploadedMedia(result: MediaSendActivityResult) { + Log.d(TAG, "Sending non-preupload media.") + + val multiShareArgs = MultiShareArgs.Builder(setOf(ContactSearchKey.RecipientSearchKey.Story(result.recipientId))) + .withMedia(result.nonUploadedMedia.toList()) + .withDraftText(result.body) + .withMentions(result.mentions.toList()) + .build() + + val results = MultiShareSender.sendSync(multiShareArgs) + + Log.d(TAG, "Sent. Failures? ${results.containsFailures()}") + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index ca73bb7b6..1ce3abe7f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.stories.StorySlateView import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs +import org.thoughtcrime.securesms.stories.viewer.AddToGroupStoryDelegate import org.thoughtcrime.securesms.stories.viewer.StoryViewerViewModel import org.thoughtcrime.securesms.stories.viewer.StoryVolumeViewModel import org.thoughtcrime.securesms.stories.viewer.info.StoryInfoBottomSheetDialogFragment @@ -109,6 +110,7 @@ class StoryViewerPageFragment : private lateinit var sendingBar: View private lateinit var storyNormalBottomGradient: View private lateinit var storyCaptionBottomGradient: View + private lateinit var addToGroupStoryButton: MaterialButton private lateinit var callback: Callback @@ -176,6 +178,7 @@ class StoryViewerPageFragment : val storyGradientTop: View = view.findViewById(R.id.story_gradient_top) val storyGradientBottom: View = view.findViewById(R.id.story_bottom_gradient_container) val storyVolumeOverlayView: StoryVolumeOverlayView = view.findViewById(R.id.story_volume_overlay) + val addToGroupStoryButtonWrapper: View = view.findViewById(R.id.add_wrapper) storyNormalBottomGradient = view.findViewById(R.id.story_gradient_bottom) storyCaptionBottomGradient = view.findViewById(R.id.story_caption_gradient) @@ -187,6 +190,7 @@ class StoryViewerPageFragment : viewsAndReplies = view.findViewById(R.id.views_and_replies_bar) sendingBarTextView = view.findViewById(R.id.sending_text_view) sendingBar = view.findViewById(R.id.sending_bar) + addToGroupStoryButton = view.findViewById(R.id.add) storySlate.callback = this @@ -202,7 +206,8 @@ class StoryViewerPageFragment : progressBar, storyGradientTop, storyGradientBottom, - storyCaptionContainer + storyCaptionContainer, + addToGroupStoryButtonWrapper ) senderAvatar.setFallbackPhotoProvider(FallbackPhotoProvider()) @@ -212,6 +217,11 @@ class StoryViewerPageFragment : requireActivity().onBackPressed() } + val addToGroupStoryDelegate = AddToGroupStoryDelegate(this) + addToGroupStoryButton.setOnClickListener { + addToGroupStoryDelegate.addToStory(storyViewerPageArgs.recipientId) + } + val singleTapHandler = SingleTapHandler( cardWrapper, viewModel::goToNextPost, @@ -384,6 +394,8 @@ class StoryViewerPageFragment : if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) { val post = state.posts[state.selectedPostIndex] + addToGroupStoryButton.visible = post.group != null + presentBottomBar(post, state.replyState, state.isReceiptsEnabled) presentSenderAvatar(senderAvatar, post) presentGroupAvatar(groupAvatar, post) diff --git a/app/src/main/res/drawable/add_to_story_16.xml b/app/src/main/res/drawable/add_to_story_16.xml new file mode 100644 index 000000000..b5e7b1417 --- /dev/null +++ b/app/src/main/res/drawable/add_to_story_16.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/add_to_story_24.xml b/app/src/main/res/drawable/add_to_story_24.xml new file mode 100644 index 000000000..c7c3ce936 --- /dev/null +++ b/app/src/main/res/drawable/add_to_story_24.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/layout/conversation_settings_button_strip.xml b/app/src/main/res/layout/conversation_settings_button_strip.xml index 0968f4a6b..5458c0485 100644 --- a/app/src/main/res/layout/conversation_settings_button_strip.xml +++ b/app/src/main/res/layout/conversation_settings_button_strip.xml @@ -17,6 +17,29 @@ android:paddingTop="24dp" android:paddingBottom="16dp"> + + + + + + + + + + + + + #1F414659 #991B1C1F #61303133 + #A3303133 #1FE2E1E5 #99BEBFC5 #99E2E1E5 diff --git a/app/src/main/res/values/material3_colors.xml b/app/src/main/res/values/material3_colors.xml index 52945af6b..2e456a8f7 100644 --- a/app/src/main/res/values/material3_colors.xml +++ b/app/src/main/res/values/material3_colors.xml @@ -51,6 +51,7 @@ #1FDCE5F9 #99FBFCFF #61E7EBF3 + #A3E7EBF3 #1F1B1B1D #99545863 #991B1D1D diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d3644a475..7cb6824fd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4216,11 +4216,16 @@ Unknown ringtone + + Can\'t add to group story + + Only admins of this group can add to its story Contacts app not found Send message Start video call Start audio call + Story Message Video Audio @@ -4385,6 +4390,10 @@ Limit reached + + Add to the group story \"%s\" + + Add to story Add a message Add a reply Send to @@ -4893,6 +4902,8 @@ %1$d reply %1$d replies + + Add Views off