Add entry points for adding to a group story.

main
Alex Hart 2022-11-29 13:12:56 -04:00 zatwierdzone przez Cody Henthorne
rodzic 7949996c5c
commit 7b13550086
19 zmienionych plików z 372 dodań i 17 usunięć

Wyświetl plik

@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheet
import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.stories.StoryViewerArgs import org.thoughtcrime.securesms.stories.StoryViewerArgs
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs 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.stories.viewer.StoryViewerActivity
import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ContextUtil import org.thoughtcrime.securesms.util.ContextUtil
@ -137,6 +138,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
private lateinit var toolbarBadge: BadgeImageView private lateinit var toolbarBadge: BadgeImageView
private lateinit var toolbarTitle: TextView private lateinit var toolbarTitle: TextView
private lateinit var toolbarBackground: View private lateinit var toolbarBackground: View
private lateinit var addToGroupStoryDelegate: AddToGroupStoryDelegate
private val navController get() = Navigation.findNavController(requireView()) private val navController get() = Navigation.findNavController(requireView())
@ -221,6 +223,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
} }
} }
addToGroupStoryDelegate = AddToGroupStoryDelegate(this)
viewModel.state.observe(viewLifecycleOwner) { state -> viewModel.state.observe(viewLifecycleOwner) { state ->
if (state.recipient != Recipient.UNKNOWN) { if (state.recipient != Recipient.UNKNOWN) {
@ -368,6 +371,17 @@ class ConversationSettingsFragment : DSLSettingsFragment(
customPref( customPref(
ButtonStripPreference.Model( ButtonStripPreference.Model(
state = state.buttonStripState, 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 = { onVideoClick = {
if (state.recipient.isPushV2Group && state.requireGroupSettingsState().isAnnouncementGroup && !state.requireGroupSettingsState().isSelfAdmin) { if (state.recipient.isPushV2Group && state.requireGroupSettingsState().isAnnouncementGroup && !state.requireGroupSettingsState().isSelfAdmin) {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())

Wyświetl plik

@ -272,7 +272,8 @@ sealed class ConversationSettingsViewModel(
isAudioSecure = recipient.isPushV2Group, isAudioSecure = recipient.isPushV2Group,
isMuted = recipient.isMuted, isMuted = recipient.isMuted,
isMuteAvailable = true, isMuteAvailable = true,
isSearchAvailable = true isSearchAvailable = true,
isAddToStoryAvailable = recipient.isPushV2Group && !recipient.isBlocked && isActive
), ),
canModifyBlockedState = RecipientUtil.isBlockable(recipient), canModifyBlockedState = RecipientUtil.isBlockable(recipient),
specificSettingsState = state.requireGroupSettingsState().copy( specificSettingsState = state.requireGroupSettingsState().copy(

Wyświetl plik

@ -24,6 +24,7 @@ object ButtonStripPreference {
class Model( class Model(
val state: State, val state: State,
val background: DSLSettingsIcon? = null, val background: DSLSettingsIcon? = null,
val onAddToStoryClick: () -> Unit = {},
val onMessageClick: () -> Unit = {}, val onMessageClick: () -> Unit = {},
val onVideoClick: () -> Unit = {}, val onVideoClick: () -> Unit = {},
val onAudioClick: () -> Unit = {}, val onAudioClick: () -> Unit = {},
@ -41,6 +42,8 @@ object ButtonStripPreference {
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) { class ViewHolder(itemView: View) : MappingViewHolder<Model>(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 message: View = itemView.findViewById(R.id.message)
private val messageContainer: View = itemView.findViewById(R.id.button_strip_message_container) private val messageContainer: View = itemView.findViewById(R.id.button_strip_message_container)
private val videoCall: View = itemView.findViewById(R.id.start_video) private val videoCall: View = itemView.findViewById(R.id.start_video)
@ -60,6 +63,7 @@ object ButtonStripPreference {
audioContainer.visible = model.state.isAudioAvailable audioContainer.visible = model.state.isAudioAvailable
muteContainer.visible = model.state.isMuteAvailable muteContainer.visible = model.state.isMuteAvailable
searchContainer.visible = model.state.isSearchAvailable searchContainer.visible = model.state.isSearchAvailable
addToStoryContainer.visible = model.state.isAddToStoryAvailable
if (model.state.isAudioSecure) { if (model.state.isAudioSecure) {
audioLabel.setText(R.string.ConversationSettingsFragment__audio) audioLabel.setText(R.string.ConversationSettingsFragment__audio)
@ -88,6 +92,7 @@ object ButtonStripPreference {
audioCall.setOnClickListener { model.onAudioClick() } audioCall.setOnClickListener { model.onAudioClick() }
mute.setOnClickListener { model.onMuteClick() } mute.setOnClickListener { model.onMuteClick() }
search.setOnClickListener { model.onSearchClick() } search.setOnClickListener { model.onSearchClick() }
addToStory.setOnClickListener { model.onAddToStoryClick() }
} }
} }
@ -99,5 +104,6 @@ object ButtonStripPreference {
val isSearchAvailable: Boolean = false, val isSearchAvailable: Boolean = false,
val isAudioSecure: Boolean = false, val isAudioSecure: Boolean = false,
val isMuted: Boolean = false, val isMuted: Boolean = false,
val isAddToStoryAvailable: Boolean = false
) )
} }

Wyświetl plik

@ -93,8 +93,9 @@ class MediaSelectionActivity :
val initialMedia: List<Media> = intent.getParcelableArrayListExtra(MEDIA) ?: listOf() val initialMedia: List<Media> = intent.getParcelableArrayListExtra(MEDIA) ?: listOf()
val message: CharSequence? = if (shareToTextStory) null else draftText val message: CharSequence? = if (shareToTextStory) null else draftText
val isReply: Boolean = intent.getBooleanExtra(IS_REPLY, false) 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] viewModel = ViewModelProvider(this, factory)[MediaSelectionViewModel::class.java]
val textStoryToggle: ConstraintLayout = findViewById(R.id.switch_widget) val textStoryToggle: ConstraintLayout = findViewById(R.id.switch_widget)
@ -221,7 +222,7 @@ class MediaSelectionActivity :
return Stories.isFeatureEnabled() && return Stories.isFeatureEnabled() &&
isCameraFirst() && isCameraFirst() &&
!viewModel.hasSelectedMedia() && !viewModel.hasSelectedMedia() &&
destination == MediaSelectionDestination.ChooseAfterMediaSelection (destination == MediaSelectionDestination.ChooseAfterMediaSelection || destination is MediaSelectionDestination.SingleStory)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -348,6 +349,7 @@ class MediaSelectionActivity :
private const val IS_REPLY = "is_reply" private const val IS_REPLY = "is_reply"
private const val IS_STORY = "is_story" private const val IS_STORY = "is_story"
private const val AS_TEXT_STORY = "as_text_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 @JvmStatic
fun camera(context: Context): Intent { 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 @JvmStatic
fun camera( fun camera(
context: Context, context: Context,
@ -457,7 +472,8 @@ class MediaSelectionActivity :
message: CharSequence? = null, message: CharSequence? = null,
isReply: Boolean = false, isReply: Boolean = false,
isStory: Boolean = false, isStory: Boolean = false,
asTextStory: Boolean = false asTextStory: Boolean = false,
isAddToGroupStoryFlow: Boolean = false
): Intent { ): Intent {
return Intent(context, MediaSelectionActivity::class.java).apply { return Intent(context, MediaSelectionActivity::class.java).apply {
putExtra(START_ACTION, startAction) putExtra(START_ACTION, startAction)
@ -468,6 +484,7 @@ class MediaSelectionActivity :
putExtra(IS_REPLY, isReply) putExtra(IS_REPLY, isReply)
putExtra(IS_STORY, isStory) putExtra(IS_STORY, isStory)
putExtra(AS_TEXT_STORY, asTextStory) putExtra(AS_TEXT_STORY, asTextStory)
putExtra(IS_ADD_TO_GROUP_STORY_FLOW, isAddToGroupStoryFlow)
} }
} }
} }

Wyświetl plik

@ -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<ContactSearchKey.RecipientSearchKey>) : MediaSelectionDestination() { class MultipleRecipients(val recipientSearchKeys: List<ContactSearchKey.RecipientSearchKey>) : MediaSelectionDestination() {
companion object { companion object {
@ -72,6 +82,7 @@ sealed class MediaSelectionDestination {
private const val WALLPAPER = "wallpaper" private const val WALLPAPER = "wallpaper"
private const val AVATAR = "avatar" private const val AVATAR = "avatar"
private const val RECIPIENT = "recipient" private const val RECIPIENT = "recipient"
private const val STORY = "story"
private const val RECIPIENT_LIST = "recipient_list" private const val RECIPIENT_LIST = "recipient_list"
fun fromBundle(bundle: Bundle): MediaSelectionDestination { fun fromBundle(bundle: Bundle): MediaSelectionDestination {
@ -79,6 +90,7 @@ sealed class MediaSelectionDestination {
bundle.containsKey(WALLPAPER) -> Wallpaper bundle.containsKey(WALLPAPER) -> Wallpaper
bundle.containsKey(AVATAR) -> Avatar bundle.containsKey(AVATAR) -> Avatar
bundle.containsKey(RECIPIENT) -> SingleRecipient(requireNotNull(bundle.getParcelable(RECIPIENT))) 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))) bundle.containsKey(RECIPIENT_LIST) -> MultipleRecipients.fromParcel(requireNotNull(bundle.getParcelableArrayList(RECIPIENT_LIST)))
else -> ChooseAfterMediaSelection else -> ChooseAfterMediaSelection
} }

Wyświetl plik

@ -105,6 +105,8 @@ class MediaSelectionRepository(context: Context) {
val singleRecipient: Recipient? = singleContact?.let { Recipient.resolved(it.recipientId) } val singleRecipient: Recipient? = singleContact?.let { Recipient.resolved(it.recipientId) }
val storyType: StoryType = if (singleRecipient?.isDistributionList == true) { val storyType: StoryType = if (singleRecipient?.isDistributionList == true) {
SignalDatabase.distributionLists.getStoryType(singleRecipient.requireDistributionListId()) SignalDatabase.distributionLists.getStoryType(singleRecipient.requireDistributionListId())
} else if (singleRecipient?.isGroup == true && singleContact.isStory) {
StoryType.STORY_WITH_REPLIES
} else { } else {
StoryType.NONE StoryType.NONE
} }

Wyświetl plik

@ -45,6 +45,7 @@ class MediaSelectionViewModel(
initialMessage: CharSequence?, initialMessage: CharSequence?,
val isReply: Boolean, val isReply: Boolean,
isStory: Boolean, isStory: Boolean,
val isAddToGroupStoryFlow: Boolean,
private val repository: MediaSelectionRepository, private val repository: MediaSelectionRepository,
private val identityChangesSince: Long = System.currentTimeMillis() private val identityChangesSince: Long = System.currentTimeMillis()
) : ViewModel() { ) : ViewModel() {
@ -360,10 +361,10 @@ class MediaSelectionViewModel(
return return
} }
val filteredPreUploadMedia = if (Stories.isFeatureEnabled()) { val filteredPreUploadMedia = if (destination is MediaSelectionDestination.SingleRecipient || !Stories.isFeatureEnabled()) {
media.filter { Stories.MediaTransform.canPreUploadMedia(it) }
} else {
media media
} else {
media.filter { Stories.MediaTransform.canPreUploadMedia(it) }
} }
repository.uploadRepository.startUpload(filteredPreUploadMedia, store.state.recipient) repository.uploadRepository.startUpload(filteredPreUploadMedia, store.state.recipient)
@ -482,10 +483,11 @@ class MediaSelectionViewModel(
private val initialMessage: CharSequence?, private val initialMessage: CharSequence?,
private val isReply: Boolean, private val isReply: Boolean,
private val isStory: Boolean, private val isStory: Boolean,
private val isAddToGroupStoryFlow: Boolean,
private val repository: MediaSelectionRepository private val repository: MediaSelectionRepository
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): 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)))
} }
} }
} }

Wyświetl plik

@ -23,6 +23,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import app.cash.exhaustive.Exhaustive import app.cash.exhaustive.Exhaustive
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.concurrent.SimpleTask import org.signal.core.util.concurrent.SimpleTask
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
@ -200,6 +201,12 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) {
} else { } else {
multiselectLauncher.launch(args) 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 { } else {
performSend() performSend()
} }
@ -317,7 +324,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) {
.setInterpolator(MediaAnimations.interpolator) .setInterpolator(MediaAnimations.interpolator)
.alpha(1f) .alpha(1f)
sharedViewModel disposables += sharedViewModel
.send(selection.filterIsInstance(ContactSearchKey.RecipientSearchKey::class.java)) .send(selection.filterIsInstance(ContactSearchKey.RecipientSearchKey::class.java))
.subscribe( .subscribe(
{ result -> callback.onSentWithResult(result) }, { result -> callback.onSentWithResult(result) },

Wyświetl plik

@ -12,6 +12,7 @@ import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey 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 { } else {
performSend(contacts) performSend(contacts)
} }

Wyświetl plik

@ -231,18 +231,20 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
!recipient.isReleaseNotes(); !recipient.isReleaseNotes();
ButtonStripPreference.State buttonStripState = new ButtonStripPreference.State( ButtonStripPreference.State buttonStripState = new ButtonStripPreference.State(
/* isMessageAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && !recipient.isReleaseNotes(), /* isMessageAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && !recipient.isReleaseNotes(),
/* isVideoAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && recipient.isRegistered(), /* isVideoAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && recipient.isRegistered(),
/* isAudioAvailable = */ isAudioAvailable, /* isAudioAvailable = */ isAudioAvailable,
/* isMuteAvailable = */ false, /* isMuteAvailable = */ false,
/* isSearchAvailable = */ false, /* isSearchAvailable = */ false,
/* isAudioSecure = */ recipient.isRegistered(), /* isAudioSecure = */ recipient.isRegistered(),
/* isMuted = */ false /* isMuted = */ false,
/* isAddToStoryAvailable = */ false
); );
ButtonStripPreference.Model buttonStripModel = new ButtonStripPreference.Model( ButtonStripPreference.Model buttonStripModel = new ButtonStripPreference.Model(
buttonStripState, buttonStripState,
DSLSettingsIcon.from(ContextUtil.requireDrawable(requireContext(), R.drawable.selectable_recipient_bottom_sheet_icon_button)), DSLSettingsIcon.from(ContextUtil.requireDrawable(requireContext(), R.drawable.selectable_recipient_bottom_sheet_icon_button)),
() -> Unit.INSTANCE,
() -> { () -> {
dismiss(); dismiss();
viewModel.onMessageClicked(requireActivity()); viewModel.onMessageClicked(requireActivity());

Wyświetl plik

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

Wyświetl plik

@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.stories.StorySlateView
import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs 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.StoryViewerViewModel
import org.thoughtcrime.securesms.stories.viewer.StoryVolumeViewModel import org.thoughtcrime.securesms.stories.viewer.StoryVolumeViewModel
import org.thoughtcrime.securesms.stories.viewer.info.StoryInfoBottomSheetDialogFragment import org.thoughtcrime.securesms.stories.viewer.info.StoryInfoBottomSheetDialogFragment
@ -109,6 +110,7 @@ class StoryViewerPageFragment :
private lateinit var sendingBar: View private lateinit var sendingBar: View
private lateinit var storyNormalBottomGradient: View private lateinit var storyNormalBottomGradient: View
private lateinit var storyCaptionBottomGradient: View private lateinit var storyCaptionBottomGradient: View
private lateinit var addToGroupStoryButton: MaterialButton
private lateinit var callback: Callback private lateinit var callback: Callback
@ -176,6 +178,7 @@ class StoryViewerPageFragment :
val storyGradientTop: View = view.findViewById(R.id.story_gradient_top) val storyGradientTop: View = view.findViewById(R.id.story_gradient_top)
val storyGradientBottom: View = view.findViewById(R.id.story_bottom_gradient_container) val storyGradientBottom: View = view.findViewById(R.id.story_bottom_gradient_container)
val storyVolumeOverlayView: StoryVolumeOverlayView = view.findViewById(R.id.story_volume_overlay) 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) storyNormalBottomGradient = view.findViewById(R.id.story_gradient_bottom)
storyCaptionBottomGradient = view.findViewById(R.id.story_caption_gradient) storyCaptionBottomGradient = view.findViewById(R.id.story_caption_gradient)
@ -187,6 +190,7 @@ class StoryViewerPageFragment :
viewsAndReplies = view.findViewById(R.id.views_and_replies_bar) viewsAndReplies = view.findViewById(R.id.views_and_replies_bar)
sendingBarTextView = view.findViewById(R.id.sending_text_view) sendingBarTextView = view.findViewById(R.id.sending_text_view)
sendingBar = view.findViewById(R.id.sending_bar) sendingBar = view.findViewById(R.id.sending_bar)
addToGroupStoryButton = view.findViewById(R.id.add)
storySlate.callback = this storySlate.callback = this
@ -202,7 +206,8 @@ class StoryViewerPageFragment :
progressBar, progressBar,
storyGradientTop, storyGradientTop,
storyGradientBottom, storyGradientBottom,
storyCaptionContainer storyCaptionContainer,
addToGroupStoryButtonWrapper
) )
senderAvatar.setFallbackPhotoProvider(FallbackPhotoProvider()) senderAvatar.setFallbackPhotoProvider(FallbackPhotoProvider())
@ -212,6 +217,11 @@ class StoryViewerPageFragment :
requireActivity().onBackPressed() requireActivity().onBackPressed()
} }
val addToGroupStoryDelegate = AddToGroupStoryDelegate(this)
addToGroupStoryButton.setOnClickListener {
addToGroupStoryDelegate.addToStory(storyViewerPageArgs.recipientId)
}
val singleTapHandler = SingleTapHandler( val singleTapHandler = SingleTapHandler(
cardWrapper, cardWrapper,
viewModel::goToNextPost, viewModel::goToNextPost,
@ -384,6 +394,8 @@ class StoryViewerPageFragment :
if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) { if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) {
val post = state.posts[state.selectedPostIndex] val post = state.posts[state.selectedPostIndex]
addToGroupStoryButton.visible = post.group != null
presentBottomBar(post, state.replyState, state.isReceiptsEnabled) presentBottomBar(post, state.replyState, state.isReceiptsEnabled)
presentSenderAvatar(senderAvatar, post) presentSenderAvatar(senderAvatar, post)
presentGroupAvatar(groupAvatar, post) presentGroupAvatar(groupAvatar, post)

Wyświetl plik

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<group>
<clip-path
android:pathData="M0,0h16v16h-16z"/>
<path
android:pathData="M13.074,6.414C13.374,6.414 13.618,6.171 13.618,5.871V4.084H15.404C15.704,4.084 15.948,3.841 15.948,3.54C15.948,3.24 15.704,2.997 15.404,2.997H13.618V1.21C13.618,0.91 13.374,0.667 13.074,0.667C12.774,0.667 12.53,0.91 12.53,1.21V2.997H10.744C10.443,2.997 10.2,3.24 10.2,3.54C10.2,3.841 10.443,4.084 10.744,4.084H12.53V5.871C12.53,6.171 12.774,6.414 13.074,6.414Z"
android:fillColor="#000000"/>
<path
android:pathData="M6.045,1.708H7.991V2.958H6.045C5.944,2.958 5.848,2.999 5.777,3.071L4.304,4.574H2.625C1.866,4.574 1.25,5.189 1.25,5.949V13C1.25,13.759 1.866,14.375 2.625,14.375H11.958C12.718,14.375 13.333,13.759 13.333,13V8.521H14.583V13C14.583,14.45 13.408,15.625 11.958,15.625H2.625C1.175,15.625 0,14.45 0,13V5.949C0,4.499 1.175,3.324 2.625,3.324H3.779L4.885,2.196C5.19,1.884 5.608,1.708 6.045,1.708Z"
android:fillColor="#000000"/>
<path
android:pathData="M7.292,5.042C5.106,5.042 3.333,6.814 3.333,9C3.333,11.186 5.106,12.958 7.292,12.958C9.478,12.958 11.25,11.186 11.25,9C11.25,6.814 9.478,5.042 7.292,5.042ZM4.583,9C4.583,7.504 5.796,6.292 7.292,6.292C8.788,6.292 10,7.504 10,9C10,10.496 8.788,11.708 7.292,11.708C5.796,11.708 4.583,10.496 4.583,9Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</group>
</vector>

Wyświetl plik

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
<path
android:pathData="M19.69,9.621C20.14,9.621 20.505,9.256 20.505,8.806V6.126H23.185C23.635,6.126 24,5.761 24,5.311C24,4.86 23.635,4.495 23.185,4.495H20.505V1.815C20.505,1.365 20.14,1 19.69,1C19.239,1 18.874,1.365 18.874,1.815V4.495H16.194C15.744,4.495 15.379,4.86 15.379,5.311C15.379,5.761 15.744,6.126 16.194,6.126H18.874V8.806C18.874,9.256 19.239,9.621 19.69,9.621Z"
android:fillColor="#000000"/>
<path
android:pathData="M13.049,2.631H9.059C8.85,2.632 8.644,2.679 8.455,2.769C8.267,2.859 8.1,2.99 7.969,3.153L6.524,4.961H5.126C4.575,4.96 4.029,5.068 3.52,5.278C3.01,5.488 2.547,5.797 2.158,6.187C1.768,6.577 1.459,7.039 1.249,7.549C1.038,8.058 0.931,8.604 0.932,9.155V17.544C0.931,18.095 1.038,18.641 1.249,19.15C1.459,19.66 1.768,20.122 2.158,20.512C2.547,20.902 3.01,21.211 3.52,21.421C4.029,21.631 4.575,21.739 5.126,21.738H17.243C17.794,21.739 18.34,21.631 18.849,21.421C19.359,21.211 19.822,20.902 20.211,20.512C20.601,20.122 20.91,19.66 21.12,19.15C21.33,18.641 21.438,18.095 21.437,17.544V14.281H19.806V17.544C19.806,18.223 19.536,18.875 19.055,19.356C18.574,19.837 17.923,20.107 17.243,20.107H5.126C4.446,20.107 3.794,19.837 3.314,19.356C2.833,18.875 2.563,18.223 2.563,17.544V9.155C2.563,8.476 2.833,7.824 3.314,7.343C3.794,6.862 4.446,6.592 5.126,6.592H7.307L7.801,5.977L9.171,4.262H13.049V2.631Z"
android:fillColor="#000000"/>
<path
android:pathData="M8.078,8.234C8.997,7.619 10.078,7.291 11.184,7.291C12.668,7.291 14.09,7.88 15.139,8.929C16.188,9.978 16.777,11.4 16.777,12.884C16.777,13.99 16.449,15.071 15.834,15.99C15.22,16.91 14.346,17.627 13.325,18.05C12.303,18.473 11.178,18.584 10.094,18.368C9.009,18.153 8.012,17.62 7.23,16.838C6.448,16.056 5.915,15.059 5.7,13.974C5.484,12.89 5.595,11.765 6.018,10.743C6.441,9.722 7.158,8.848 8.078,8.234ZM13.385,9.59C12.734,9.155 11.968,8.922 11.184,8.922C10.135,8.925 9.129,9.343 8.386,10.085C7.644,10.828 7.226,11.834 7.223,12.884C7.223,13.667 7.456,14.433 7.891,15.084C8.326,15.736 8.945,16.243 9.669,16.543C10.392,16.843 11.189,16.921 11.957,16.768C12.726,16.616 13.431,16.238 13.985,15.684C14.539,15.13 14.917,14.425 15.069,13.656C15.222,12.888 15.144,12.091 14.844,11.368C14.544,10.644 14.037,10.025 13.385,9.59Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</group>
</vector>

Wyświetl plik

@ -17,6 +17,29 @@
android:paddingTop="24dp" android:paddingTop="24dp"
android:paddingBottom="16dp"> android:paddingBottom="16dp">
<LinearLayout
android:id="@+id/button_strip_add_to_story_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:layout_weight="1"
android:gravity="center_horizontal"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/add_to_story"
style="@style/Signal.Widget.ImageView.ActionButton"
android:contentDescription="@string/ConversationSettingsFragment__story"
app:srcCompat="@drawable/add_to_story_24" />
<TextView
android:id="@+id/add_to_story_label"
style="@style/Signal.Widget.TextView.ActionButton"
android:text="@string/ConversationSettingsFragment__story" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/button_strip_message_container" android:id="@+id/button_strip_message_container"
android:layout_width="0dp" android:layout_width="0dp"

Wyświetl plik

@ -201,6 +201,42 @@
app:srcCompat="@drawable/ic_x_24" app:srcCompat="@drawable/ic_x_24"
app:tint="@color/core_white" /> app:tint="@color/core_white" />
<FrameLayout
android:id="@+id/add_wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/add"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:insetTop="8dp"
android:insetBottom="8dp"
android:minWidth="0dp"
android:minHeight="48dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/StoryViewerPageFragment__add"
android:textAllCaps="false"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_dark_colorOnSurface"
android:visibility="gone"
app:backgroundTint="@color/signal_colorSurfaceVariant_64"
app:backgroundTintMode="src_in"
app:cornerRadius="16dp"
app:icon="@drawable/add_to_story_16"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconSize="16dp"
app:iconTint="@color/signal_dark_colorOnSurface"
tools:visibility="visible" />
</FrameLayout>
<org.thoughtcrime.securesms.components.AvatarImageView <org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/sender_avatar" android:id="@+id/sender_avatar"
android:layout_width="32dp" android:layout_width="32dp"

Wyświetl plik

@ -51,6 +51,7 @@
<color name="signal_colorSecondaryContainer_12">#1F414659</color> <color name="signal_colorSecondaryContainer_12">#1F414659</color>
<color name="signal_colorSurface_60">#991B1C1F</color> <color name="signal_colorSurface_60">#991B1C1F</color>
<color name="signal_colorSurfaceVariant_38">#61303133</color> <color name="signal_colorSurfaceVariant_38">#61303133</color>
<color name="signal_colorSurfaceVariant_64">#A3303133</color>
<color name="signal_colorOnSurface_12">#1FE2E1E5</color> <color name="signal_colorOnSurface_12">#1FE2E1E5</color>
<color name="signal_colorOnSurfaceVariant_60">#99BEBFC5</color> <color name="signal_colorOnSurfaceVariant_60">#99BEBFC5</color>
<color name="signal_colorOnBackground_60">#99E2E1E5</color> <color name="signal_colorOnBackground_60">#99E2E1E5</color>

Wyświetl plik

@ -51,6 +51,7 @@
<color name="signal_colorSecondaryContainer_12">#1FDCE5F9</color> <color name="signal_colorSecondaryContainer_12">#1FDCE5F9</color>
<color name="signal_colorSurface_60">#99FBFCFF</color> <color name="signal_colorSurface_60">#99FBFCFF</color>
<color name="signal_colorSurfaceVariant_38">#61E7EBF3</color> <color name="signal_colorSurfaceVariant_38">#61E7EBF3</color>
<color name="signal_colorSurfaceVariant_64">#A3E7EBF3</color>
<color name="signal_colorOnSurface_12">#1F1B1B1D</color> <color name="signal_colorOnSurface_12">#1F1B1B1D</color>
<color name="signal_colorOnSurfaceVariant_60">#99545863</color> <color name="signal_colorOnSurfaceVariant_60">#99545863</color>
<color name="signal_colorOnBackground_60">#991B1D1D</color> <color name="signal_colorOnBackground_60">#991B1D1D</color>

Wyświetl plik

@ -4216,11 +4216,16 @@
<string name="NotificationsSettingsFragment__unknown_ringtone">Unknown ringtone</string> <string name="NotificationsSettingsFragment__unknown_ringtone">Unknown ringtone</string>
<!-- ConversationSettingsFragment --> <!-- ConversationSettingsFragment -->
<!-- Dialog title displayed when non-admin tries to add a story to an audience group -->
<string name="ConversationSettingsFragment__cant_add_to_group_story">Can\'t add to group story</string>
<!-- Dialog message displayed when non-admin tries to add a story to an audience group -->
<string name="ConversationSettingsFragment__only_admins_of_this_group_can_add_to_its_story">Only admins of this group can add to its story</string>
<!-- Error toasted when no activity can handle the add contact intent --> <!-- Error toasted when no activity can handle the add contact intent -->
<string name="ConversationSettingsFragment__contacts_app_not_found">Contacts app not found</string> <string name="ConversationSettingsFragment__contacts_app_not_found">Contacts app not found</string>
<string name="ConversationSettingsFragment__send_message">Send message</string> <string name="ConversationSettingsFragment__send_message">Send message</string>
<string name="ConversationSettingsFragment__start_video_call">Start video call</string> <string name="ConversationSettingsFragment__start_video_call">Start video call</string>
<string name="ConversationSettingsFragment__start_audio_call">Start audio call</string> <string name="ConversationSettingsFragment__start_audio_call">Start audio call</string>
<string name="ConversationSettingsFragment__story">Story</string>
<string name="ConversationSettingsFragment__message">Message</string> <string name="ConversationSettingsFragment__message">Message</string>
<string name="ConversationSettingsFragment__video">Video</string> <string name="ConversationSettingsFragment__video">Video</string>
<string name="ConversationSettingsFragment__audio">Audio</string> <string name="ConversationSettingsFragment__audio">Audio</string>
@ -4385,6 +4390,10 @@
<string name="MultiselectForwardFragment__limit_reached">Limit reached</string> <string name="MultiselectForwardFragment__limit_reached">Limit reached</string>
<!-- Media V2 --> <!-- Media V2 -->
<!-- Dialog message when sending a story via an add to group story button -->
<string name="MediaReviewFragment__add_to_the_group_story">Add to the group story \"%s\"</string>
<!-- Positive dialog action when sending a story via an add to group story button -->
<string name ="MediaReviewFragment__add_to_story">Add to story</string>
<string name="MediaReviewFragment__add_a_message">Add a message</string> <string name="MediaReviewFragment__add_a_message">Add a message</string>
<string name="MediaReviewFragment__add_a_reply">Add a reply</string> <string name="MediaReviewFragment__add_a_reply">Add a reply</string>
<string name="MediaReviewFragment__send_to">Send to</string> <string name="MediaReviewFragment__send_to">Send to</string>
@ -4893,6 +4902,8 @@
<item quantity="one">%1$d reply</item> <item quantity="one">%1$d reply</item>
<item quantity="other">%1$d replies</item> <item quantity="other">%1$d replies</item>
</plurals> </plurals>
<!-- Label on group stories to add a story -->
<string name="StoryViewerPageFragment__add">Add</string>
<!-- Used when view receipts are disabled --> <!-- Used when view receipts are disabled -->
<string name="StoryViewerPageFragment__views_off">Views off</string> <string name="StoryViewerPageFragment__views_off">Views off</string>
<!-- Used to join views and replies when both exist on a story item --> <!-- Used to join views and replies when both exist on a story item -->