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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

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

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: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"

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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