Add info sheet for stories.
This commit is contained in:
parent
caab91cdc3
commit
2e8ebe8b74
21 changed files with 660 additions and 131 deletions
|
@ -20,7 +20,8 @@ data class StoryViewerArgs(
|
|||
val recipientIds: List<RecipientId> = emptyList(),
|
||||
val isFromNotification: Boolean = false,
|
||||
val groupReplyStartPosition: Int = -1,
|
||||
val isUnviewedOnly: Boolean = false
|
||||
val isUnviewedOnly: Boolean = false,
|
||||
val isFromInfoContextMenuAction: Boolean = false
|
||||
) : Parcelable {
|
||||
|
||||
class Builder(private val recipientId: RecipientId, private val isInHiddenStoryMode: Boolean) {
|
||||
|
@ -33,6 +34,7 @@ data class StoryViewerArgs(
|
|||
private var isFromNotification: Boolean = false
|
||||
private var groupReplyStartPosition: Int = -1
|
||||
private var isUnviewedOnly: Boolean = false
|
||||
private var isFromInfoContextMenuAction: Boolean = false
|
||||
|
||||
fun withStoryId(storyId: Long): Builder {
|
||||
this.storyId = storyId
|
||||
|
@ -85,7 +87,8 @@ data class StoryViewerArgs(
|
|||
recipientIds = recipientIds,
|
||||
isFromNotification = isFromNotification,
|
||||
groupReplyStartPosition = groupReplyStartPosition,
|
||||
isUnviewedOnly = isUnviewedOnly
|
||||
isUnviewedOnly = isUnviewedOnly,
|
||||
isFromInfoContextMenuAction = isFromInfoContextMenuAction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
|||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.stories.landing.StoriesLandingItem
|
||||
import org.thoughtcrime.securesms.stories.my.MyStoriesItem
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryPost
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageState
|
||||
import org.thoughtcrime.securesms.util.DeleteDialog
|
||||
|
@ -80,6 +79,7 @@ object StoryContextMenu {
|
|||
fun show(
|
||||
context: Context,
|
||||
anchorView: View,
|
||||
previewView: View,
|
||||
model: StoriesLandingItem.Model,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
|
@ -99,6 +99,7 @@ object StoryContextMenu {
|
|||
override fun onDismissed() = onDismiss()
|
||||
override fun onDelete() = model.onDeleteStory(model)
|
||||
override fun onSave() = model.onSave(model)
|
||||
override fun onInfo() = model.onInfo(model, previewView)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -113,6 +114,7 @@ object StoryContextMenu {
|
|||
onGoToChat: (StoryPost) -> Unit,
|
||||
onSave: (StoryPost) -> Unit,
|
||||
onDelete: (StoryPost) -> Unit,
|
||||
onInfo: (StoryPost) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val selectedStory: StoryPost = storyViewerPageState.posts[storyViewerPageState.selectedPostIndex]
|
||||
|
@ -132,32 +134,7 @@ object StoryContextMenu {
|
|||
override fun onDismissed() = onDismiss()
|
||||
override fun onSave() = onSave(selectedStory)
|
||||
override fun onDelete() = onDelete(selectedStory)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun show(
|
||||
context: Context,
|
||||
anchorView: View,
|
||||
myStoriesItemModel: MyStoriesItem.Model,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
show(
|
||||
context = context,
|
||||
anchorView = anchorView,
|
||||
isFromSelf = true,
|
||||
isToGroup = false,
|
||||
isFromReleaseChannel = false,
|
||||
canHide = false,
|
||||
callbacks = object : Callbacks {
|
||||
override fun onHide() = throw NotImplementedError()
|
||||
override fun onUnhide() = throw NotImplementedError()
|
||||
override fun onForward() = myStoriesItemModel.onForwardClick(myStoriesItemModel)
|
||||
override fun onShare() = myStoriesItemModel.onShareClick(myStoriesItemModel)
|
||||
override fun onGoToChat() = throw NotImplementedError()
|
||||
override fun onDismissed() = onDismiss()
|
||||
override fun onSave() = myStoriesItemModel.onSaveClick(myStoriesItemModel)
|
||||
override fun onDelete() = myStoriesItemModel.onDeleteClick(myStoriesItemModel)
|
||||
override fun onInfo() = onInfo(selectedStory)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -219,6 +196,12 @@ object StoryContextMenu {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
add(
|
||||
ActionItem(R.drawable.ic_info_outline_message_details_24, context.getString(R.string.StoriesLandingItem__info)) {
|
||||
callbacks.onInfo()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SignalContextMenu.Builder(anchorView, rootView)
|
||||
|
@ -240,5 +223,6 @@ object StoryContextMenu {
|
|||
fun onDismissed()
|
||||
fun onSave()
|
||||
fun onDelete()
|
||||
fun onInfo()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -207,46 +207,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
|||
return StoriesLandingItem.Model(
|
||||
data = data,
|
||||
onRowClick = { model, preview ->
|
||||
if (model.data.storyRecipient.isMyStory) {
|
||||
startActivityIfAble(Intent(requireContext(), MyStoriesActivity::class.java))
|
||||
} else if (model.data.primaryStory.messageRecord.isOutgoing && model.data.primaryStory.messageRecord.isFailed) {
|
||||
if (model.data.primaryStory.messageRecord.isIdentityMismatchFailure) {
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), model.data.primaryStory.messageRecord)
|
||||
.show(childFragmentManager)
|
||||
} else {
|
||||
StoryDialogs.resendStory(requireContext()) {
|
||||
lifecycleDisposable += viewModel.resend(model.data.primaryStory.messageRecord).subscribe()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), preview, ViewCompat.getTransitionName(preview) ?: "")
|
||||
|
||||
val record = model.data.primaryStory.messageRecord as MmsMessageRecord
|
||||
val blur = record.slideDeck.thumbnailSlide?.placeholderBlur
|
||||
val (text: StoryTextPostModel?, image: Uri?) = if (record.storyType.isTextStory) {
|
||||
StoryTextPostModel.parseFrom(record) to null
|
||||
} else {
|
||||
null to record.slideDeck.thumbnailSlide?.uri
|
||||
}
|
||||
|
||||
startActivityIfAble(
|
||||
StoryViewerActivity.createIntent(
|
||||
context = requireContext(),
|
||||
storyViewerArgs = StoryViewerArgs(
|
||||
recipientId = model.data.storyRecipient.id,
|
||||
storyId = -1L,
|
||||
isInHiddenStoryMode = model.data.isHidden,
|
||||
storyThumbTextModel = text,
|
||||
storyThumbUri = image,
|
||||
storyThumbBlur = blur,
|
||||
recipientIds = viewModel.getRecipientIds(model.data.isHidden, model.data.storyViewState == StoryViewState.UNVIEWED),
|
||||
isUnviewedOnly = model.data.storyViewState == StoryViewState.UNVIEWED
|
||||
)
|
||||
),
|
||||
options.toBundle()
|
||||
)
|
||||
}
|
||||
openStoryViewer(model, preview, false)
|
||||
},
|
||||
onForwardStory = {
|
||||
MultiselectForwardFragmentArgs.create(requireContext(), it.data.primaryStory.multiselectCollection.toSet()) { args ->
|
||||
|
@ -271,10 +232,57 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
|||
},
|
||||
onDeleteStory = {
|
||||
handleDeleteStory(it)
|
||||
},
|
||||
onInfo = { model, preview ->
|
||||
openStoryViewer(model, preview, true)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun openStoryViewer(model: StoriesLandingItem.Model, preview: View, isFromInfoContextMenuAction: Boolean) {
|
||||
if (model.data.storyRecipient.isMyStory) {
|
||||
startActivityIfAble(Intent(requireContext(), MyStoriesActivity::class.java))
|
||||
} else if (model.data.primaryStory.messageRecord.isOutgoing && model.data.primaryStory.messageRecord.isFailed) {
|
||||
if (model.data.primaryStory.messageRecord.isIdentityMismatchFailure) {
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), model.data.primaryStory.messageRecord)
|
||||
.show(childFragmentManager)
|
||||
} else {
|
||||
StoryDialogs.resendStory(requireContext()) {
|
||||
lifecycleDisposable += viewModel.resend(model.data.primaryStory.messageRecord).subscribe()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), preview, ViewCompat.getTransitionName(preview) ?: "")
|
||||
|
||||
val record = model.data.primaryStory.messageRecord as MmsMessageRecord
|
||||
val blur = record.slideDeck.thumbnailSlide?.placeholderBlur
|
||||
val (text: StoryTextPostModel?, image: Uri?) = if (record.storyType.isTextStory) {
|
||||
StoryTextPostModel.parseFrom(record) to null
|
||||
} else {
|
||||
null to record.slideDeck.thumbnailSlide?.uri
|
||||
}
|
||||
|
||||
startActivityIfAble(
|
||||
StoryViewerActivity.createIntent(
|
||||
context = requireContext(),
|
||||
storyViewerArgs = StoryViewerArgs(
|
||||
recipientId = model.data.storyRecipient.id,
|
||||
storyId = -1L,
|
||||
isInHiddenStoryMode = model.data.isHidden,
|
||||
storyThumbTextModel = text,
|
||||
storyThumbUri = image,
|
||||
storyThumbBlur = blur,
|
||||
recipientIds = viewModel.getRecipientIds(model.data.isHidden, model.data.storyViewState == StoryViewState.UNVIEWED),
|
||||
isUnviewedOnly = model.data.storyViewState == StoryViewState.UNVIEWED,
|
||||
isFromInfoContextMenuAction = isFromInfoContextMenuAction
|
||||
)
|
||||
),
|
||||
options.toBundle()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeleteStory(model: StoriesLandingItem.Model) {
|
||||
lifecycleDisposable += StoryContextMenu.delete(requireContext(), setOf(model.data.primaryStory.messageRecord)).subscribe()
|
||||
}
|
||||
|
|
|
@ -49,7 +49,8 @@ object StoriesLandingItem {
|
|||
val onShareStory: (Model) -> Unit,
|
||||
val onGoToChat: (Model) -> Unit,
|
||||
val onSave: (Model) -> Unit,
|
||||
val onDeleteStory: (Model) -> Unit
|
||||
val onDeleteStory: (Model) -> Unit,
|
||||
val onInfo: (Model, View) -> Unit
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return data.storyRecipient.id == newItem.data.storyRecipient.id
|
||||
|
@ -267,7 +268,7 @@ object StoriesLandingItem {
|
|||
|
||||
private fun displayContext(model: Model) {
|
||||
itemView.isSelected = true
|
||||
StoryContextMenu.show(context, itemView, model) { itemView.isSelected = false }
|
||||
StoryContextMenu.show(context, itemView, storyPreview, model) { itemView.isSelected = false }
|
||||
}
|
||||
|
||||
private fun clearGlide() {
|
||||
|
|
|
@ -78,47 +78,7 @@ class MyStoriesFragment : DSLSettingsFragment(
|
|||
MyStoriesItem.Model(
|
||||
distributionStory = conversationMessage,
|
||||
onClick = { it, preview ->
|
||||
if (it.distributionStory.messageRecord.isOutgoing && it.distributionStory.messageRecord.isFailed) {
|
||||
if (it.distributionStory.messageRecord.isIdentityMismatchFailure) {
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), it.distributionStory.messageRecord)
|
||||
.show(childFragmentManager)
|
||||
} else {
|
||||
StoryDialogs.resendStory(requireContext()) {
|
||||
lifecycleDisposable += viewModel.resend(it.distributionStory.messageRecord).subscribe()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val recipient = if (it.distributionStory.messageRecord.recipient.isGroup) {
|
||||
it.distributionStory.messageRecord.recipient
|
||||
} else {
|
||||
Recipient.self()
|
||||
}
|
||||
|
||||
val record = it.distributionStory.messageRecord as MmsMessageRecord
|
||||
val blur = record.slideDeck.thumbnailSlide?.placeholderBlur
|
||||
val (text: StoryTextPostModel?, image: Uri?) = if (record.storyType.isTextStory) {
|
||||
StoryTextPostModel.parseFrom(record) to null
|
||||
} else {
|
||||
null to record.slideDeck.thumbnailSlide?.uri
|
||||
}
|
||||
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), preview, ViewCompat.getTransitionName(preview) ?: "")
|
||||
startActivity(
|
||||
StoryViewerActivity.createIntent(
|
||||
context = requireContext(),
|
||||
storyViewerArgs = StoryViewerArgs(
|
||||
recipientId = recipient.id,
|
||||
storyId = conversationMessage.messageRecord.id,
|
||||
isInHiddenStoryMode = recipient.shouldHideStory(),
|
||||
storyThumbTextModel = text,
|
||||
storyThumbUri = image,
|
||||
storyThumbBlur = blur
|
||||
)
|
||||
),
|
||||
options.toBundle()
|
||||
)
|
||||
}
|
||||
openStoryViewer(it, preview, false)
|
||||
},
|
||||
onLongClick = {
|
||||
Util.copyToClipboard(requireContext(), it.distributionStory.messageRecord.timestamp.toString())
|
||||
|
@ -139,6 +99,9 @@ class MyStoriesFragment : DSLSettingsFragment(
|
|||
},
|
||||
onShareClick = {
|
||||
StoryContextMenu.share(this@MyStoriesFragment, it.distributionStory.messageRecord as MediaMmsMessageRecord)
|
||||
},
|
||||
onInfoClick = { model, preview ->
|
||||
openStoryViewer(model, preview, true)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -151,6 +114,51 @@ class MyStoriesFragment : DSLSettingsFragment(
|
|||
}
|
||||
}
|
||||
|
||||
private fun openStoryViewer(it: MyStoriesItem.Model, preview: View, isFromInfoContextMenuAction: Boolean) {
|
||||
if (it.distributionStory.messageRecord.isOutgoing && it.distributionStory.messageRecord.isFailed) {
|
||||
if (it.distributionStory.messageRecord.isIdentityMismatchFailure) {
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), it.distributionStory.messageRecord)
|
||||
.show(childFragmentManager)
|
||||
} else {
|
||||
StoryDialogs.resendStory(requireContext()) {
|
||||
lifecycleDisposable += viewModel.resend(it.distributionStory.messageRecord).subscribe()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val recipient = if (it.distributionStory.messageRecord.recipient.isGroup) {
|
||||
it.distributionStory.messageRecord.recipient
|
||||
} else {
|
||||
Recipient.self()
|
||||
}
|
||||
|
||||
val record = it.distributionStory.messageRecord as MmsMessageRecord
|
||||
val blur = record.slideDeck.thumbnailSlide?.placeholderBlur
|
||||
val (text: StoryTextPostModel?, image: Uri?) = if (record.storyType.isTextStory) {
|
||||
StoryTextPostModel.parseFrom(record) to null
|
||||
} else {
|
||||
null to record.slideDeck.thumbnailSlide?.uri
|
||||
}
|
||||
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), preview, ViewCompat.getTransitionName(preview) ?: "")
|
||||
startActivity(
|
||||
StoryViewerActivity.createIntent(
|
||||
context = requireContext(),
|
||||
storyViewerArgs = StoryViewerArgs(
|
||||
recipientId = recipient.id,
|
||||
storyId = it.distributionStory.messageRecord.id,
|
||||
isInHiddenStoryMode = recipient.shouldHideStory(),
|
||||
storyThumbTextModel = text,
|
||||
storyThumbUri = image,
|
||||
storyThumbBlur = blur,
|
||||
isFromInfoContextMenuAction = isFromInfoContextMenuAction
|
||||
)
|
||||
),
|
||||
options.toBundle()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeleteClick(model: MyStoriesItem.Model) {
|
||||
lifecycleDisposable += StoryContextMenu.delete(requireContext(), setOf(model.distributionStory.messageRecord)).subscribe()
|
||||
}
|
||||
|
|
|
@ -42,7 +42,8 @@ object MyStoriesItem {
|
|||
val onSaveClick: (Model) -> Unit,
|
||||
val onDeleteClick: (Model) -> Unit,
|
||||
val onForwardClick: (Model) -> Unit,
|
||||
val onShareClick: (Model) -> Unit
|
||||
val onShareClick: (Model) -> Unit,
|
||||
val onInfoClick: (Model, View) -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return distributionStory.messageRecord.id == newItem.distributionStory.messageRecord.id
|
||||
|
@ -173,7 +174,8 @@ object MyStoriesItem {
|
|||
ActionItem(R.drawable.ic_delete_24_tinted, context.getString(R.string.delete)) { model.onDeleteClick(model) },
|
||||
ActionItem(R.drawable.ic_download_24_tinted, context.getString(R.string.save)) { model.onSaveClick(model) },
|
||||
ActionItem(R.drawable.ic_forward_24_tinted, context.getString(R.string.MyStories_forward)) { model.onForwardClick(model) },
|
||||
ActionItem(R.drawable.ic_share_24_tinted, context.getString(R.string.StoriesLandingItem__share)) { model.onShareClick(model) }
|
||||
ActionItem(R.drawable.ic_share_24_tinted, context.getString(R.string.StoriesLandingItem__share)) { model.onShareClick(model) },
|
||||
ActionItem(R.drawable.ic_info_outline_message_details_24, context.getString(R.string.StoriesLandingItem__info)) { model.onInfoClick(model, storyPreview) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -50,7 +50,8 @@ class StoryViewerFragment :
|
|||
storyViewerArgs.storyId,
|
||||
storyViewerArgs.isFromNotification,
|
||||
storyViewerArgs.groupReplyStartPosition,
|
||||
storyViewerArgs.isUnviewedOnly
|
||||
storyViewerArgs.isUnviewedOnly,
|
||||
storyViewerArgs.isFromInfoContextMenuAction
|
||||
)
|
||||
|
||||
storyPager.adapter = adapter
|
||||
|
|
|
@ -12,7 +12,8 @@ class StoryViewerPagerAdapter(
|
|||
private val initialStoryId: Long,
|
||||
private val isFromNotification: Boolean,
|
||||
private val groupReplyStartPosition: Int,
|
||||
private val isUnviewedOnly: Boolean
|
||||
private val isUnviewedOnly: Boolean,
|
||||
private val isFromInfoContextMenuAction: Boolean
|
||||
) : FragmentStateAdapter(fragment) {
|
||||
|
||||
private var pages: List<RecipientId> = emptyList()
|
||||
|
@ -33,7 +34,7 @@ class StoryViewerPagerAdapter(
|
|||
override fun getItemCount(): Int = pages.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return StoryViewerPageFragment.create(pages[position], initialStoryId, isFromNotification, groupReplyStartPosition, isUnviewedOnly)
|
||||
return StoryViewerPageFragment.create(pages[position], initialStoryId, isFromNotification, groupReplyStartPosition, isUnviewedOnly, isFromInfoContextMenuAction)
|
||||
}
|
||||
|
||||
private class Callback(
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.info
|
||||
|
||||
import android.content.DialogInterface
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
|
||||
/**
|
||||
* Bottom sheet which displays receipt information to the user for a given story.
|
||||
*/
|
||||
class StoryInfoBottomSheetDialogFragment : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
override val peekHeightPercentage: Float = 0.5f
|
||||
|
||||
companion object {
|
||||
private const val STORY_ID = "args.story.id"
|
||||
|
||||
fun create(storyId: Long): StoryInfoBottomSheetDialogFragment {
|
||||
return StoryInfoBottomSheetDialogFragment().apply {
|
||||
arguments = bundleOf(STORY_ID to storyId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val storyId: Long get() = requireArguments().getLong(STORY_ID)
|
||||
|
||||
private val viewModel: StoryInfoViewModel by viewModels(factoryProducer = {
|
||||
StoryInfoViewModel.Factory(storyId)
|
||||
})
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
StoryInfoHeader.register(adapter)
|
||||
StoryInfoRecipientRow.register(adapter)
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
lifecycleDisposable += viewModel.state.subscribe { state ->
|
||||
if (state.isLoaded) {
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: StoryInfoState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
StoryInfoHeader.Model(
|
||||
sentMillis = state.sentMillis,
|
||||
receivedMillis = state.receivedMillis,
|
||||
size = state.size
|
||||
)
|
||||
)
|
||||
|
||||
sectionHeaderPref(
|
||||
title = if (state.isOutgoing) {
|
||||
R.string.StoryInfoBottomSheetDialogFragment__sent_to
|
||||
} else {
|
||||
R.string.StoryInfoBottomSheetDialogFragment__sent_from
|
||||
}
|
||||
)
|
||||
|
||||
state.recipients.forEach {
|
||||
customPref(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
findListener<OnInfoSheetDismissedListener>()?.onInfoSheetDismissed()
|
||||
}
|
||||
|
||||
interface OnInfoSheetDismissedListener {
|
||||
fun onInfoSheetDismissed()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.info
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Holds information around the sent time, received time, and file size of a given story.
|
||||
*/
|
||||
object StoryInfoHeader {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.story_info_header))
|
||||
}
|
||||
|
||||
class Model(val sentMillis: Long, val receivedMillis: Long, val size: Long) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return newItem.sentMillis == sentMillis && newItem.receivedMillis == receivedMillis && newItem.size == size
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val sentView: TextView = itemView.findViewById(R.id.story_info_view_sent_label)
|
||||
private val recvView: TextView = itemView.findViewById(R.id.story_info_view_received_label)
|
||||
private val sizeView: TextView = itemView.findViewById(R.id.story_info_view_file_size_label)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
if (model.sentMillis > 0L) {
|
||||
sentView.visible = true
|
||||
sentView.text = DateUtils.getTimeString(context, Locale.getDefault(), model.sentMillis)
|
||||
} else {
|
||||
sentView.visible = false
|
||||
}
|
||||
|
||||
if (model.receivedMillis > 0L) {
|
||||
recvView.visible = true
|
||||
recvView.text = DateUtils.getTimeString(context, Locale.getDefault(), model.receivedMillis)
|
||||
} else {
|
||||
recvView.visible = false
|
||||
}
|
||||
|
||||
if (model.size > 0L) {
|
||||
sizeView.visible = true
|
||||
sizeView.text = Util.getPrettyFileSize(model.size)
|
||||
} else {
|
||||
sizeView.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.info
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Holds information needed to render a single recipient row in the info sheet.
|
||||
*/
|
||||
object StoryInfoRecipientRow {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.story_info_recipient_row))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val recipient: Recipient,
|
||||
val date: Long,
|
||||
val status: Int
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return recipient.id == newItem.recipient.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return recipient.hasSameContent(newItem.recipient) && date == newItem.date
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val avatarView: AvatarImageView = itemView.findViewById(R.id.story_info_avatar)
|
||||
private val nameView: TextView = itemView.findViewById(R.id.story_info_display_name)
|
||||
private val timestampView: TextView = itemView.findViewById(R.id.story_info_timestamp)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
avatarView.setRecipient(model.recipient)
|
||||
nameView.text = model.recipient.getDisplayName(context)
|
||||
timestampView.text = DateUtils.getTimeString(context, Locale.getDefault(), model.date)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.info
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
|
||||
/**
|
||||
* Gathers necessary message record and receipt data for a given story id.
|
||||
*/
|
||||
class StoryInfoRepository {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(StoryInfoRepository::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the StoryInfo for a given ID and emits a new item whenever the underlying
|
||||
* message record changes.
|
||||
*/
|
||||
fun getStoryInfo(storyId: Long): Observable<StoryInfo> {
|
||||
return observeMessageRecord(storyId)
|
||||
.switchMap { record ->
|
||||
getReceiptInfo(storyId).map { receiptInfo ->
|
||||
StoryInfo(record, receiptInfo)
|
||||
}.toObservable()
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun observeMessageRecord(storyId: Long): Observable<MessageRecord> {
|
||||
return Observable.create { emitter ->
|
||||
fun refresh() {
|
||||
try {
|
||||
emitter.onNext(SignalDatabase.mms.getMessageRecord(storyId))
|
||||
} catch (e: NoSuchMessageException) {
|
||||
Log.w(TAG, "The story message disappeared. Terminating emission.")
|
||||
emitter.onComplete()
|
||||
}
|
||||
}
|
||||
|
||||
val observer = DatabaseObserver.MessageObserver {
|
||||
if (it.mms && it.id == storyId) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(observer)
|
||||
emitter.setCancellable {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer)
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getReceiptInfo(storyId: Long): Single<List<GroupReceiptDatabase.GroupReceiptInfo>> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.groupReceipts.getGroupReceiptInfo(storyId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The message record and receipt info for a given story id.
|
||||
*/
|
||||
data class StoryInfo(val messageRecord: MessageRecord, val receiptInfo: List<GroupReceiptDatabase.GroupReceiptInfo>)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.info
|
||||
|
||||
/**
|
||||
* Contains the needed information to render the story info sheet.
|
||||
*/
|
||||
data class StoryInfoState(
|
||||
val sentMillis: Long = -1L,
|
||||
val receivedMillis: Long = -1L,
|
||||
val size: Long = -1L,
|
||||
val isOutgoing: Boolean = false,
|
||||
val recipients: List<StoryInfoRecipientRow.Model> = emptyList(),
|
||||
val isLoaded: Boolean = false
|
||||
)
|
|
@ -0,0 +1,66 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.info
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
/**
|
||||
* Gathers and stores the StoryInfoState which is used to render the story info sheet.
|
||||
*/
|
||||
class StoryInfoViewModel(storyId: Long, repository: StoryInfoRepository = StoryInfoRepository()) : ViewModel() {
|
||||
|
||||
private val store = RxStore(StoryInfoState())
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: Flowable<StoryInfoState> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
init {
|
||||
disposables += store.update(repository.getStoryInfo(storyId).toFlowable(BackpressureStrategy.LATEST)) { storyInfo, storyInfoState ->
|
||||
storyInfoState.copy(
|
||||
isLoaded = true,
|
||||
sentMillis = storyInfo.messageRecord.dateSent,
|
||||
receivedMillis = storyInfo.messageRecord.dateReceived,
|
||||
size = (storyInfo.messageRecord as? MmsMessageRecord)?.let { it.slideDeck.firstSlide?.fileSize } ?: -1L,
|
||||
isOutgoing = storyInfo.messageRecord.isOutgoing,
|
||||
recipients = buildRecipients(storyInfo)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRecipients(storyInfo: StoryInfoRepository.StoryInfo): List<StoryInfoRecipientRow.Model> {
|
||||
return if (storyInfo.messageRecord.isOutgoing) {
|
||||
storyInfo.receiptInfo.map {
|
||||
StoryInfoRecipientRow.Model(
|
||||
recipient = Recipient.resolved(it.recipientId),
|
||||
date = it.timestamp,
|
||||
status = it.status
|
||||
)
|
||||
}
|
||||
} else {
|
||||
listOf(
|
||||
StoryInfoRecipientRow.Model(
|
||||
recipient = storyInfo.messageRecord.individualRecipient,
|
||||
date = storyInfo.messageRecord.dateSent,
|
||||
status = -1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
class Factory(private val storyId: Long) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(StoryInfoViewModel(storyId)) as T
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ sealed class StoryViewerDialog(val type: Type) {
|
|||
FORWARD,
|
||||
DELETE,
|
||||
CONTEXT_MENU,
|
||||
VIEWS_AND_REPLIES
|
||||
VIEWS_AND_REPLIES,
|
||||
INFO
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.widget.TextView
|
|||
import androidx.cardview.widget.CardView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.animation.PathInterpolatorCompat
|
||||
import androidx.core.view.doOnNextLayout
|
||||
|
@ -58,6 +59,7 @@ import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView
|
|||
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryViewerViewModel
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryVolumeViewModel
|
||||
import org.thoughtcrime.securesms.stories.viewer.info.StoryInfoBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.direct.StoryDirectReplyDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.group.StoryGroupReplyBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.reaction.OnReactionSentView
|
||||
|
@ -86,7 +88,8 @@ class StoryViewerPageFragment :
|
|||
MultiselectForwardBottomSheet.Callback,
|
||||
StorySlateView.Callback,
|
||||
StoryTextPostPreviewFragment.Callback,
|
||||
StoryFirstTimeNavigationView.Callback {
|
||||
StoryFirstTimeNavigationView.Callback,
|
||||
StoryInfoBottomSheetDialogFragment.OnInfoSheetDismissedListener {
|
||||
|
||||
private val storyVolumeViewModel: StoryVolumeViewModel by viewModels(ownerProducer = { requireActivity() })
|
||||
|
||||
|
@ -147,6 +150,9 @@ class StoryViewerPageFragment :
|
|||
private val isUnviewedOnly: Boolean
|
||||
get() = requireArguments().getBoolean(ARG_IS_UNVIEWED_ONLY, false)
|
||||
|
||||
private val isFromInfoContextMenuAction: Boolean
|
||||
get() = requireArguments().getBoolean(ARG_IS_FROM_INFO_CONTEXT_MENU_ACTION, false)
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
callback = requireListener()
|
||||
|
@ -380,6 +386,8 @@ class StoryViewerPageFragment :
|
|||
sharedViewModel.consumeInitialState()
|
||||
if (isFromNotification) {
|
||||
startReply(isFromNotification = true, groupReplyStartPosition = groupReplyStartPosition)
|
||||
} else if (isFromInfoContextMenuAction && state.selectedPostIndex in state.posts.indices) {
|
||||
showInfo(state.posts[state.selectedPostIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -626,6 +634,11 @@ class StoryViewerPageFragment :
|
|||
replyFragment.showNow(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
private fun showInfo(storyPost: StoryPost) {
|
||||
viewModel.setIsDisplayingInfoDialog(true)
|
||||
StoryInfoBottomSheetDialogFragment.create(storyPost.id).show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
private fun markViewedIfAble() {
|
||||
val post = if (viewModel.hasPost()) viewModel.getPost() else null
|
||||
if (post?.content?.transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
|
@ -936,6 +949,9 @@ class StoryViewerPageFragment :
|
|||
viewModel.setIsDisplayingDeleteDialog(false)
|
||||
viewModel.refresh()
|
||||
}
|
||||
},
|
||||
onInfo = {
|
||||
showInfo(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -955,16 +971,25 @@ class StoryViewerPageFragment :
|
|||
private const val ARG_IS_FROM_NOTIFICATION = "is_from_notification"
|
||||
private const val ARG_GROUP_REPLY_START_POSITION = "group_reply_start_position"
|
||||
private const val ARG_IS_UNVIEWED_ONLY = "is_unviewed_only"
|
||||
private const val ARG_IS_FROM_INFO_CONTEXT_MENU_ACTION = "is_from_info_context_menu_action"
|
||||
|
||||
fun create(recipientId: RecipientId, initialStoryId: Long, isFromNotification: Boolean, groupReplyStartPosition: Int, isUnviewedOnly: Boolean): Fragment {
|
||||
fun create(
|
||||
recipientId: RecipientId,
|
||||
initialStoryId: Long,
|
||||
isFromNotification: Boolean,
|
||||
groupReplyStartPosition: Int,
|
||||
isUnviewedOnly: Boolean,
|
||||
isFromInfoContextMenuAction: Boolean
|
||||
): Fragment {
|
||||
return StoryViewerPageFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_STORY_RECIPIENT_ID, recipientId)
|
||||
putLong(ARG_STORY_ID, initialStoryId)
|
||||
putBoolean(ARG_IS_FROM_NOTIFICATION, isFromNotification)
|
||||
putInt(ARG_GROUP_REPLY_START_POSITION, groupReplyStartPosition)
|
||||
putBoolean(ARG_IS_UNVIEWED_ONLY, isUnviewedOnly)
|
||||
}
|
||||
arguments = bundleOf(
|
||||
ARG_STORY_RECIPIENT_ID to recipientId,
|
||||
ARG_STORY_ID to initialStoryId,
|
||||
ARG_IS_FROM_NOTIFICATION to isFromNotification,
|
||||
ARG_GROUP_REPLY_START_POSITION to groupReplyStartPosition,
|
||||
ARG_IS_UNVIEWED_ONLY to isUnviewedOnly,
|
||||
ARG_IS_FROM_INFO_CONTEXT_MENU_ACTION to isFromInfoContextMenuAction
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1147,4 +1172,8 @@ class StoryViewerPageFragment :
|
|||
SignalStore.storyValues().userHasSeenFirstNavView = true
|
||||
viewModel.setIsDisplayingFirstTimeNavigation(false)
|
||||
}
|
||||
|
||||
override fun onInfoSheetDismissed() {
|
||||
viewModel.setIsDisplayingInfoDialog(false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,10 +61,9 @@ class StoryViewerPageViewModel(
|
|||
disposables.clear()
|
||||
disposables += repository.getStoryPostsFor(recipientId, isUnviewedOnly).subscribe { posts ->
|
||||
store.update { state ->
|
||||
var isDisplayingInitialState = false
|
||||
val isDisplayingInitialState = state.posts.isEmpty() && posts.isNotEmpty()
|
||||
val startIndex = if (state.posts.isEmpty() && initialStoryId > 0) {
|
||||
val initialIndex = posts.indexOfFirst { it.id == initialStoryId }
|
||||
isDisplayingInitialState = true
|
||||
initialIndex.takeIf { it > -1 } ?: state.selectedPostIndex
|
||||
} else if (state.posts.isEmpty()) {
|
||||
val initialPost = getNextUnreadPost(posts)
|
||||
|
@ -244,6 +243,10 @@ class StoryViewerPageViewModel(
|
|||
storyViewerPlaybackStore.update { it.copy(isDisplayingFirstTimeNavigation = isDisplayingFirstTimeNavigation) }
|
||||
}
|
||||
|
||||
fun setIsDisplayingInfoDialog(isDisplayingInfoDialog: Boolean) {
|
||||
storyViewerPlaybackStore.update { it.copy(isDisplayingInfoDialog = isDisplayingInfoDialog) }
|
||||
}
|
||||
|
||||
private fun resolveSwipeToReplyState(state: StoryViewerPageState, index: Int): StoryViewerPageState.ReplyState {
|
||||
if (index !in state.posts.indices) {
|
||||
return StoryViewerPageState.ReplyState.NONE
|
||||
|
|
|
@ -16,7 +16,8 @@ data class StoryViewerPlaybackState(
|
|||
val isDisplayingLinkPreviewTooltip: Boolean = false,
|
||||
val isDisplayingReactionAnimation: Boolean = false,
|
||||
val isRunningSharedElementAnimation: Boolean = false,
|
||||
val isDisplayingFirstTimeNavigation: Boolean = false
|
||||
val isDisplayingFirstTimeNavigation: Boolean = false,
|
||||
val isDisplayingInfoDialog: Boolean = false
|
||||
) {
|
||||
val hideChromeImmediate: Boolean = isRunningSharedElementAnimation
|
||||
|
||||
|
@ -38,5 +39,6 @@ data class StoryViewerPlaybackState(
|
|||
isDisplayingLinkPreviewTooltip ||
|
||||
isDisplayingReactionAnimation ||
|
||||
isRunningSharedElementAnimation ||
|
||||
isDisplayingFirstTimeNavigation
|
||||
isDisplayingFirstTimeNavigation ||
|
||||
isDisplayingInfoDialog
|
||||
}
|
||||
|
|
83
app/src/main/res/layout/story_info_header.xml
Normal file
83
app/src/main/res/layout/story_info_header.xml
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/dsl_settings_gutter"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_info_view_sent_heading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/StoryInfoHeader__sent"
|
||||
android:textAppearance="@style/Signal.Text.LabelLarge"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_info_view_sent_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:textAppearance="@style/Signal.Text.BodySmall"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="@id/story_info_view_sent_heading"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/story_info_view_sent_heading"
|
||||
app:layout_constraintTop_toTopOf="@id/story_info_view_sent_heading"
|
||||
tools:text="Today 12:30 PM" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_info_view_received_heading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/StoryInfoHeader__received"
|
||||
android:textAppearance="@style/Signal.Text.LabelLarge"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/story_info_view_sent_heading" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_info_view_received_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:textAppearance="@style/Signal.Text.BodySmall"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="@id/story_info_view_received_heading"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/story_info_view_received_heading"
|
||||
app:layout_constraintTop_toTopOf="@id/story_info_view_received_heading"
|
||||
tools:text="Today 12:30 PM" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_info_view_file_size_heading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/StoryInfoHeader__file_size"
|
||||
android:textAppearance="@style/Signal.Text.LabelLarge"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/story_info_view_received_heading" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_info_view_file_size_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:textAppearance="@style/Signal.Text.BodySmall"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="@id/story_info_view_file_size_heading"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/story_info_view_file_size_heading"
|
||||
app:layout_constraintTop_toTopOf="@id/story_info_view_file_size_heading"
|
||||
tools:text="100 KB" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
44
app/src/main/res/layout/story_info_recipient_row.xml
Normal file
44
app/src/main/res/layout/story_info_recipient_row.xml
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="64dp"
|
||||
android:paddingHorizontal="@dimen/dsl_settings_gutter">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/story_info_avatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/story_info_display_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/story_info_timestamp"
|
||||
app:layout_constraintStart_toEndOf="@id/story_info_avatar"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_info_timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/Signal.Text.BodyMedium"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Today 12:30 PM" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -4553,6 +4553,8 @@
|
|||
<string name="StoriesLandingItem__share">Share…</string>
|
||||
<!-- Context menu option to go to story chat -->
|
||||
<string name="StoriesLandingItem__go_to_chat">Go to chat</string>
|
||||
<!-- Context menu option to go to story info -->
|
||||
<string name="StoriesLandingItem__info">Info</string>
|
||||
<!-- Label when a story is pending sending -->
|
||||
<string name="StoriesLandingItem__sending">Sending…</string>
|
||||
<!-- Label when multiple stories are pending sending -->
|
||||
|
@ -4994,6 +4996,19 @@
|
|||
<!-- Only with selected connections option for initial My Story settings configuration shown when sending to My Story for the first time -->
|
||||
<string name="ChooseInitialMyStoryMembershipFragment__only_share_with">Only share with…</string>
|
||||
|
||||
<!-- Story info header sent heading -->
|
||||
<string name="StoryInfoHeader__sent">Sent</string>
|
||||
<!-- Story info header received heading -->
|
||||
<string name="StoryInfoHeader__received">Received</string>
|
||||
<!-- Story info header file size heading -->
|
||||
<string name="StoryInfoHeader__file_size">File size</string>
|
||||
<!-- Story info "Sent to" header -->
|
||||
<string name="StoryInfoBottomSheetDialogFragment__sent_to">Sent to</string>
|
||||
<!-- Story info "Sent from" header -->
|
||||
<string name="StoryInfoBottomSheetDialogFragment__sent_from">Sent from</string>
|
||||
<!-- Story Info context menu label -->
|
||||
<string name="StoryInfoBottomSheetDialogFragment__info">Info</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Add table
Reference in a new issue