Add entry points for adding to a group story.

This commit is contained in:
Alex Hart
2022-11-29 13:12:56 -04:00
committed by Cody Henthorne
parent 7949996c5c
commit 7b13550086
19 changed files with 372 additions and 17 deletions

View File

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

View File

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

View File

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

View File

@@ -93,8 +93,9 @@ class MediaSelectionActivity :
val initialMedia: List<Media> = 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)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,29 @@
android:paddingTop="24dp"
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
android:id="@+id/button_strip_message_container"
android:layout_width="0dp"

View File

@@ -201,6 +201,42 @@
app:srcCompat="@drawable/ic_x_24"
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
android:id="@+id/sender_avatar"
android:layout_width="32dp"

View File

@@ -51,6 +51,7 @@
<color name="signal_colorSecondaryContainer_12">#1F414659</color>
<color name="signal_colorSurface_60">#991B1C1F</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_colorOnSurfaceVariant_60">#99BEBFC5</color>
<color name="signal_colorOnBackground_60">#99E2E1E5</color>

View File

@@ -51,6 +51,7 @@
<color name="signal_colorSecondaryContainer_12">#1FDCE5F9</color>
<color name="signal_colorSurface_60">#99FBFCFF</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_colorOnSurfaceVariant_60">#99545863</color>
<color name="signal_colorOnBackground_60">#991B1D1D</color>

View File

@@ -4216,11 +4216,16 @@
<string name="NotificationsSettingsFragment__unknown_ringtone">Unknown ringtone</string>
<!-- 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 -->
<string name="ConversationSettingsFragment__contacts_app_not_found">Contacts app not found</string>
<string name="ConversationSettingsFragment__send_message">Send message</string>
<string name="ConversationSettingsFragment__start_video_call">Start video 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__video">Video</string>
<string name="ConversationSettingsFragment__audio">Audio</string>
@@ -4385,6 +4390,10 @@
<string name="MultiselectForwardFragment__limit_reached">Limit reached</string>
<!-- 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_reply">Add a reply</string>
<string name="MediaReviewFragment__send_to">Send to</string>
@@ -4893,6 +4902,8 @@
<item quantity="one">%1$d reply</item>
<item quantity="other">%1$d replies</item>
</plurals>
<!-- Label on group stories to add a story -->
<string name="StoryViewerPageFragment__add">Add</string>
<!-- Used when view receipts are disabled -->
<string name="StoryViewerPageFragment__views_off">Views off</string>
<!-- Used to join views and replies when both exist on a story item -->