Implement story error slates.

Co-authored-by: Rashad Sookram <rashad@signal.org>
fork-5.53.8
Alex Hart 2022-03-02 14:20:49 -04:00
rodzic 34bbb98c96
commit 2483a92975
11 zmienionych plików z 415 dodań i 35 usunięć

Wyświetl plik

@ -20,7 +20,8 @@ import java.util.Objects;
public abstract class MediaPreviewFragment extends Fragment {
static final String DATA_URI = "DATA_URI";
public static final String DATA_URI = "DATA_URI";
static final String DATA_SIZE = "DATA_SIZE";
static final String DATA_CONTENT_TYPE = "DATA_CONTENT_TYPE";
static final String AUTO_PLAY = "AUTO_PLAY";

Wyświetl plik

@ -0,0 +1,169 @@
package org.thoughtcrime.securesms.stories
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.util.visible
/**
* Displays loading / error slate in Story viewer.
*/
class StorySlateView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
companion object {
private val TAG = Log.tag(StorySlateView::class.java)
}
var callback: Callback? = null
var state: State = State.HIDDEN
private set
private var postId: Long = 0L
init {
inflate(context, R.layout.stories_slate_view, this)
}
private val background: View = findViewById(R.id.background)
private val loadingSpinner: View = findViewById(R.id.loading_spinner)
private val errorCircle: View = findViewById(R.id.error_circle)
private val unavailableText: View = findViewById(R.id.unavailable)
private val errorText: TextView = findViewById(R.id.error_text)
fun moveToState(state: State, postId: Long) {
if (this.state == state && this.postId == postId) {
return
}
if (this.postId != postId) {
this.postId = postId
moveToHiddenState()
callback?.onStateChanged(State.HIDDEN, postId)
}
if (this.state.isValidTransitionTo(state)) {
when (state) {
State.LOADING -> moveToProgressState(State.LOADING)
State.ERROR -> moveToErrorState()
State.RETRY -> moveToProgressState(State.RETRY)
State.NOT_FOUND -> moveToNotFoundState()
State.HIDDEN -> moveToHiddenState()
}
callback?.onStateChanged(state, postId)
} else {
Log.d(TAG, "Invalid state transfer: ${this.state} -> $state")
}
}
private fun moveToProgressState(state: State) {
this.state = state
visible = true
background.visible = true
loadingSpinner.visible = true
errorCircle.visible = false
unavailableText.visible = false
errorText.visible = false
}
private fun moveToErrorState() {
state = State.ERROR
visible = true
background.visible = true
loadingSpinner.visible = false
errorCircle.visible = true
unavailableText.visible = false
errorText.visible = true
if (NetworkConstraint.isMet(ApplicationDependencies.getApplication())) {
errorText.setText(R.string.StorySlateView__couldnt_load_content)
} else {
errorText.setText(R.string.StorySlateView__no_internet_connection)
}
}
private fun moveToNotFoundState() {
state = State.NOT_FOUND
visible = true
background.visible = true
loadingSpinner.visible = false
errorCircle.visible = false
unavailableText.visible = true
errorText.visible = false
}
private fun moveToHiddenState() {
state = State.HIDDEN
visible = false
}
override fun onSaveInstanceState(): Parcelable {
val rootState = super.onSaveInstanceState()
return Bundle().apply {
putParcelable("ROOT", rootState)
putInt("STATE", state.code)
putLong("ID", postId)
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is Bundle) {
val rootState: Parcelable? = state.getParcelable("ROOT")
this.state = State.fromCode(state.getInt("STATE", State.HIDDEN.code))
this.postId = state.getLong("ID")
super.onRestoreInstanceState(rootState)
} else {
super.onRestoreInstanceState(state)
}
}
init {
errorCircle.setOnClickListener { moveToState(State.RETRY, postId) }
}
interface Callback {
fun onStateChanged(state: State, postId: Long)
}
enum class State(val code: Int) {
LOADING(0),
ERROR(1),
RETRY(2),
NOT_FOUND(3),
HIDDEN(4);
fun isValidTransitionTo(newState: State): Boolean {
if (newState in listOf(HIDDEN, NOT_FOUND)) {
return true
}
return when (this) {
LOADING -> newState == ERROR
ERROR -> newState == RETRY
RETRY -> newState == ERROR
HIDDEN -> newState == LOADING
else -> false
}
}
companion object {
fun fromCode(code: Int): State {
return values().firstOrNull {
it.code == code
} ?: HIDDEN
}
}
}
}

Wyświetl plik

@ -5,6 +5,7 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
@ -18,6 +19,8 @@ import androidx.core.view.doOnNextLayout
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
@ -31,11 +34,13 @@ import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardBottomSheet
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment
import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stories.StorySlateView
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
import org.thoughtcrime.securesms.stories.viewer.StoryViewerViewModel
import org.thoughtcrime.securesms.stories.viewer.reply.direct.StoryDirectReplyDialogFragment
@ -53,9 +58,11 @@ import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.abs
class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), MediaPreviewFragment.Events, MultiselectForwardBottomSheet.Callback {
class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), MediaPreviewFragment.Events, MultiselectForwardBottomSheet.Callback, StorySlateView.Callback {
private lateinit var progressBar: SegmentedProgressBar
private lateinit var storySlate: StorySlateView
private lateinit var viewsAndReplies: TextView
private lateinit var callback: Callback
@ -75,6 +82,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
private val videoControlsDelegate = VideoControlsDelegate()
private val lifecycleDisposable = LifecycleDisposable()
private val timeoutDisposable = LifecycleDisposable()
private val storyRecipientId: RecipientId
get() = requireArguments().getParcelable(ARG_STORY_RECIPIENT_ID)!!
@ -90,14 +98,17 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
val date: TextView = view.findViewById(R.id.date)
val moreButton: View = view.findViewById(R.id.more)
val distributionList: TextView = view.findViewById(R.id.distribution_list)
val viewsAndReplies: TextView = view.findViewById(R.id.views_and_replies_bar)
val cardWrapper: TouchInterceptingFrameLayout = view.findViewById(R.id.story_content_card_touch_interceptor)
val card: CardView = view.findViewById(R.id.story_content_card)
val caption: TextView = view.findViewById(R.id.story_caption)
val largeCaption: TextView = view.findViewById(R.id.story_large_caption)
val largeCaptionOverlay: View = view.findViewById(R.id.story_large_caption_overlay)
storySlate = view.findViewById(R.id.story_slate)
progressBar = view.findViewById(R.id.progress)
viewsAndReplies = view.findViewById(R.id.views_and_replies_bar)
storySlate.callback = this
chrome = listOf(
closeView,
@ -122,12 +133,13 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
requireContext(),
StoryGestureListener(
cardWrapper,
progressBar,
viewModel::goToNextPost,
viewModel::goToPreviousPost,
this::startReply
)
)
cardWrapper.setOnInterceptTouchEventListener { true }
cardWrapper.setOnInterceptTouchEventListener { storySlate.state == StorySlateView.State.HIDDEN }
cardWrapper.setOnTouchListener { _, event ->
val result = gestureDetector.onTouchEvent(event)
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
@ -151,10 +163,6 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
override fun onPage(oldPageIndex: Int, newPageIndex: Int) {
if (oldPageIndex != newPageIndex && context != null) {
viewModel.setSelectedPostIndex(newPageIndex)
childFragmentManager.beginTransaction()
.replace(R.id.story_content_container, createFragmentForPost(viewModel.getPostAt(newPageIndex)))
.commit()
}
if (oldPageIndex == newPageIndex) {
@ -163,7 +171,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
}
override fun onFinished() {
callback.onFinishedPosts(storyRecipientId)
viewModel.goToNextPost()
}
}
@ -190,7 +198,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
if (state.posts.isNotEmpty() && state.selectedPostIndex < state.posts.size) {
val post = state.posts[state.selectedPostIndex]
presentViewsAndReplies(viewsAndReplies, post)
presentViewsAndReplies(post)
presentSenderAvatar(senderAvatar, post)
presentGroupAvatar(groupAvatar, post)
presentFrom(from, post)
@ -209,6 +217,9 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
progressBar.segmentDurations = durations
}
presentStory(post, state.selectedPostIndex)
presentSlate(post)
viewModel.setAreSegmentsInitialized(true)
} else if (state.selectedPostIndex >= state.posts.size) {
callback.onFinishedPosts(storyRecipientId)
@ -223,6 +234,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
}
}
timeoutDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += viewModel.groupDirectReplyObservable.subscribe { opt ->
if (opt.isPresent) {
@ -304,9 +316,12 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
}
private fun resumeProgress() {
if (progressBar.segmentCount != 0) {
progressBar.start()
videoControlsDelegate.resume(viewModel.getPost().attachment.uri!!)
if (progressBar.segmentCount != 0 && viewModel.hasPost()) {
val postUri = viewModel.getPost().attachment.uri
if (postUri != null) {
progressBar.start()
videoControlsDelegate.resume(postUri)
}
}
}
@ -349,6 +364,64 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
}
}
private fun presentStory(post: StoryPost, index: Int) {
val fragment = childFragmentManager.findFragmentById(R.id.story_content_container)
if (fragment != null && fragment.requireArguments().getParcelable<Uri>(MediaPreviewFragment.DATA_URI) == post.attachment.uri) {
progressBar.setPosition(index)
return
}
if (post.attachment.uri == null) {
progressBar.setPosition(index)
progressBar.invalidate()
} else {
progressBar.setPosition(index)
storySlate.moveToState(StorySlateView.State.HIDDEN, post.id)
viewModel.setIsDisplayingSlate(false)
childFragmentManager.beginTransaction()
.replace(R.id.story_content_container, createFragmentForPost(post))
.commitNow()
}
}
private fun presentSlate(post: StoryPost) {
when (post.attachment.transferState) {
AttachmentDatabase.TRANSFER_PROGRESS_DONE -> {
storySlate.moveToState(StorySlateView.State.HIDDEN, post.id)
viewModel.setIsDisplayingSlate(false)
}
AttachmentDatabase.TRANSFER_PROGRESS_PENDING -> {
storySlate.moveToState(StorySlateView.State.LOADING, post.id)
viewModel.setIsDisplayingSlate(true)
}
AttachmentDatabase.TRANSFER_PROGRESS_STARTED -> {
storySlate.moveToState(StorySlateView.State.LOADING, post.id)
viewModel.setIsDisplayingSlate(true)
}
AttachmentDatabase.TRANSFER_PROGRESS_FAILED -> {
storySlate.moveToState(StorySlateView.State.NOT_FOUND, post.id)
viewModel.setIsDisplayingSlate(true)
}
}
}
override fun onStateChanged(state: StorySlateView.State, postId: Long) {
if (state == StorySlateView.State.LOADING || state == StorySlateView.State.RETRY) {
timeoutDisposable.disposables.clear()
timeoutDisposable += Observable.interval(10, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
storySlate.moveToState(StorySlateView.State.ERROR, postId)
}
viewModel.forceDownloadSelectedPost()
} else {
timeoutDisposable.disposables.clear()
}
viewsAndReplies.visible = state == StorySlateView.State.HIDDEN
}
private fun presentDistributionList(distributionList: TextView, storyPost: StoryPost) {
distributionList.text = storyPost.distributionList?.getDisplayName(requireContext())
distributionList.visible = storyPost.distributionList != null && !storyPost.distributionList.isMyStory
@ -445,7 +518,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
}
}
private fun presentViewsAndReplies(viewsAndReplies: TextView, post: StoryPost) {
private fun presentViewsAndReplies(post: StoryPost) {
val views = resources.getQuantityString(R.plurals.StoryViewerFragment__d_views, post.viewCount, post.viewCount)
val replies = resources.getQuantityString(R.plurals.StoryViewerFragment__d_replies, post.replyCount, post.replyCount)
@ -527,7 +600,8 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
private class StoryGestureListener(
private val container: View,
private val progress: SegmentedProgressBar,
private val onGoToNext: () -> Unit,
private val onGoToPrevious: () -> Unit,
private val onReplyToPost: () -> Unit
) : GestureDetector.SimpleOnGestureListener() {
@ -561,18 +635,18 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
}
private fun performLeftAction() {
if (progress.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
progress.next()
if (container.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
onGoToNext()
} else {
progress.previous()
onGoToPrevious()
}
}
private fun performRightAction() {
if (progress.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
progress.previous()
if (container.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
onGoToPrevious()
} else {
progress.next()
onGoToNext()
}
}
}
@ -610,7 +684,6 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
}
override fun mediaNotAvailable() {
// TODO [stories] -- Display appropriate error slate
}
interface Callback {

Wyświetl plik

@ -5,6 +5,7 @@ import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.NoSuchMessageException
@ -13,6 +14,7 @@ import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob
import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob
import org.thoughtcrime.securesms.recipients.Recipient
@ -94,6 +96,11 @@ class StoryViewerPageRepository(context: Context) {
}
}
val conversationObserver = DatabaseObserver.Observer {
refresh(SignalDatabase.mms.getMessageRecord(record.id))
}
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(record.threadId, conversationObserver)
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageUpdateObserver)
val messageInsertObserver = DatabaseObserver.MessageObserver {
@ -105,6 +112,7 @@ class StoryViewerPageRepository(context: Context) {
}
emitter.setCancellable {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver)
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver)
if (recipient.isGroup) {
@ -116,6 +124,12 @@ class StoryViewerPageRepository(context: Context) {
}
}
fun forceDownload(post: StoryPost) {
ApplicationDependencies.getJobManager().add(
AttachmentDownloadJob(post.id, (post.attachment as DatabaseAttachment).attachmentId, true)
)
}
fun getStoryPostsFor(recipientId: RecipientId): Observable<List<StoryPost>> {
return getStoryRecords(recipientId)
.switchMap { records ->

Wyświetl plik

@ -13,6 +13,7 @@ import io.reactivex.rxjava3.subjects.Subject
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.livedata.Store
import java.util.Optional
import kotlin.math.max
import kotlin.math.min
/**
@ -59,10 +60,6 @@ class StoryViewerPageViewModel(
}
}
fun kickPlaybackState() {
storyViewerPlaybackStore.update { it }
}
override fun onCleared() {
disposables.clear()
}
@ -72,7 +69,12 @@ class StoryViewerPageViewModel(
}
fun setSelectedPostIndex(index: Int) {
repository.markViewed(getPostAt(index))
val selectedPost = getPostAt(index)
if (selectedPost != null) {
repository.markViewed(selectedPost)
}
store.update {
it.copy(
selectedPostIndex = index,
@ -81,6 +83,16 @@ class StoryViewerPageViewModel(
}
}
fun goToNextPost() {
val postIndex = store.state.selectedPostIndex
setSelectedPostIndex(postIndex + 1)
}
fun goToPreviousPost() {
val postIndex = store.state.selectedPostIndex
setSelectedPostIndex(max(0, postIndex - 1))
}
fun getRestartIndex(): Int {
return min(store.state.selectedPostIndex, store.state.posts.lastIndex)
}
@ -89,10 +101,18 @@ class StoryViewerPageViewModel(
return store.state.replyState
}
fun hasPost(): Boolean {
return store.state.selectedPostIndex in store.state.posts.indices
}
fun getPost(): StoryPost {
return store.state.posts[store.state.selectedPostIndex]
}
fun forceDownloadSelectedPost() {
repository.forceDownload(getPost())
}
fun startDirectReply(storyId: Long, recipientId: RecipientId) {
storyViewerDialogSubject.onNext(Optional.of(StoryViewerDialog.GroupDirectReply(recipientId, storyId)))
}
@ -101,6 +121,10 @@ class StoryViewerPageViewModel(
storyViewerPlaybackStore.update { it.copy(isUserScrollingParent = isUserScrollingParent) }
}
fun setIsDisplayingSlate(isDisplayingSlate: Boolean) {
storyViewerPlaybackStore.update { it.copy(isDisplayingSlate = isDisplayingSlate) }
}
fun setIsSelectedPage(isSelectedPage: Boolean) {
storyViewerPlaybackStore.update { it.copy(isSelectedPage = isSelectedPage) }
}
@ -153,8 +177,8 @@ class StoryViewerPageViewModel(
}
}
fun getPostAt(index: Int): StoryPost {
return store.state.posts[index]
fun getPostAt(index: Int): StoryPost? {
return store.state.posts.getOrNull(index)
}
class Factory(private val recipientId: RecipientId, private val repository: StoryViewerPageRepository) : ViewModelProvider.Factory {

Wyświetl plik

@ -10,7 +10,8 @@ data class StoryViewerPlaybackState(
val isDisplayingDirectReplyDialog: Boolean = false,
val isDisplayingCaptionOverlay: Boolean = false,
val isUserScrollingParent: Boolean = false,
val isSelectedPage: Boolean = false
val isSelectedPage: Boolean = false,
val isDisplayingSlate: Boolean = false
) {
val isPaused: Boolean = !areSegmentsInitialized ||
isUserTouching ||
@ -22,5 +23,6 @@ data class StoryViewerPlaybackState(
isDisplayingDirectReplyDialog ||
isDisplayingCaptionOverlay ||
isUserScrollingParent ||
!isSelectedPage
!isSelectedPage ||
isDisplayingSlate
}

Wyświetl plik

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="3dp"
android:color="@color/core_grey_60" />
</shape>

Wyświetl plik

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/transparent_black_60" />
<corners android:radius="24dp" />
</shape>

Wyświetl plik

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<merge 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="match_parent"
tools:parentTag="android.widget.FrameLayout">
<View
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/core_grey_80"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loading_spinner"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="@color/core_grey_05"
app:indicatorInset="0dp"
app:indicatorSize="64dp"
app:trackColor="@color/transparent"
app:trackThickness="3dp"
tools:visibility="visible" />
<View
android:id="@+id/error_circle"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:background="@drawable/stories_slate_indicator_ring"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/unavailable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:alpha="0.6"
android:gravity="center_horizontal"
android:paddingHorizontal="46dp"
android:text="@string/StorySlateView__this_story_is_no_longer_available"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/core_grey_05"
android:visibility="gone"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/error_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="68dp"
android:background="@drawable/stories_slate_view_error_message_background"
android:drawablePadding="13dp"
android:paddingHorizontal="13dp"
android:paddingTop="13dp"
android:paddingBottom="13dp"
android:textAppearance="@style/Signal.Text.Body"
android:textColor="@color/white"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_error_outline_24"
app:drawableTint="@color/core_white"
tools:text="@string/StorySlateView__couldnt_load_content"
tools:visibility="visible" />
</merge>

Wyświetl plik

@ -38,6 +38,12 @@
android:layout_height="160dp"
android:layout_gravity="bottom"
android:background="@drawable/story_gradient_bottom" />
<org.thoughtcrime.securesms.stories.StorySlateView
android:id="@+id/story_slate"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
</org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout>
@ -95,12 +101,11 @@
android:id="@+id/story_large_caption_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="@color/transparent_black_40" />
android:background="@color/transparent_black_40"
android:visibility="gone" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/story_large_caption"
android:visibility="gone"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
@ -110,6 +115,7 @@
android:gravity="bottom"
android:textAppearance="@style/Signal.Text.Body"
android:textStyle="bold"
android:visibility="gone"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@id/story_from_barrier"
app:layout_constraintEnd_toEndOf="parent"

Wyświetl plik

@ -4590,6 +4590,12 @@
<string name="StoryViewerPageFragment__see_more">… See More</string>
<!-- Displayed in toast after sending a direct reply -->
<string name="StoryDirectReplyDialogFragment__reply_sent">Reply sent</string>
<!-- Displayed in the viewer when a story is no longer available -->
<string name="StorySlateView__this_story_is_no_longer_available">This story is no longer available.</string>
<!-- Displayed in the viewer when the network is not available -->
<string name="StorySlateView__no_internet_connection">No Internet Connection</string>
<!-- Displayed in the viewer when network is available but content could not be downloaded -->
<string name="StorySlateView__couldnt_load_content">Couldn\'t Load Content</string>
<!-- endregion -->
<!-- Content description for expand contacts chevron -->