kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement Stories read receipt off state.
rodzic
f3873c8a7c
commit
e412cac419
|
@ -62,6 +62,7 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
|||
StartLocation.NOTIFICATION_PROFILE_DETAILS -> AppSettingsFragmentDirections.actionDirectToNotificationProfileDetails(
|
||||
EditNotificationProfileScheduleFragmentArgs.fromBundle(intent.getBundleExtra(START_ARGUMENTS)!!).profileId
|
||||
)
|
||||
StartLocation.PRIVACY -> AppSettingsFragmentDirections.actionDirectToPrivacy()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,6 +169,9 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
|||
@JvmStatic
|
||||
fun createNotificationProfile(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CREATE_NOTIFICATION_PROFILE)
|
||||
|
||||
@JvmStatic
|
||||
fun privacy(context: Context): Intent = getIntentForStartLocation(context, StartLocation.PRIVACY)
|
||||
|
||||
@JvmStatic
|
||||
fun notificationProfileDetails(context: Context, profileId: Long): Intent {
|
||||
val arguments = EditNotificationProfileScheduleFragmentArgs.Builder(profileId, false)
|
||||
|
@ -197,7 +201,8 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
|||
MANAGE_SUBSCRIPTIONS(8),
|
||||
NOTIFICATION_PROFILES(9),
|
||||
CREATE_NOTIFICATION_PROFILE(10),
|
||||
NOTIFICATION_PROFILE_DETAILS(11);
|
||||
NOTIFICATION_PROFILE_DETAILS(11),
|
||||
PRIVACY(12);
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: Int?): StartLocation {
|
||||
|
|
|
@ -339,7 +339,7 @@ class StoryViewerPageFragment :
|
|||
if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) {
|
||||
val post = state.posts[state.selectedPostIndex]
|
||||
|
||||
presentViewsAndReplies(post, state.replyState)
|
||||
presentViewsAndReplies(post, state.replyState, state.isReceiptsEnabled)
|
||||
presentSenderAvatar(senderAvatar, post)
|
||||
presentGroupAvatar(groupAvatar, post)
|
||||
presentFrom(from, post)
|
||||
|
@ -449,6 +449,7 @@ class StoryViewerPageFragment :
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.setIsFragmentResumed(true)
|
||||
viewModel.checkReadReceiptState()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
@ -823,7 +824,7 @@ class StoryViewerPageFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun presentViewsAndReplies(post: StoryPost, replyState: StoryViewerPageState.ReplyState) {
|
||||
private fun presentViewsAndReplies(post: StoryPost, replyState: StoryViewerPageState.ReplyState, isReceiptsEnabled: Boolean) {
|
||||
if (replyState == StoryViewerPageState.ReplyState.NONE) {
|
||||
viewsAndReplies.visible = false
|
||||
return
|
||||
|
@ -835,14 +836,25 @@ class StoryViewerPageFragment :
|
|||
val replies = resources.getQuantityString(R.plurals.StoryViewerFragment__d_replies, post.replyCount, post.replyCount)
|
||||
|
||||
if (Recipient.self() == post.sender) {
|
||||
if (post.replyCount == 0) {
|
||||
viewsAndReplies.setIconResource(R.drawable.ic_chevron_end_24)
|
||||
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_END
|
||||
viewsAndReplies.text = views
|
||||
if (isReceiptsEnabled) {
|
||||
if (post.replyCount == 0) {
|
||||
viewsAndReplies.setIconResource(R.drawable.ic_chevron_end_24)
|
||||
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_END
|
||||
viewsAndReplies.text = views
|
||||
} else {
|
||||
viewsAndReplies.setIconResource(R.drawable.ic_chevron_end_24)
|
||||
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_END
|
||||
viewsAndReplies.text = getString(R.string.StoryViewerFragment__s_s, views, replies)
|
||||
}
|
||||
} else {
|
||||
viewsAndReplies.setIconResource(R.drawable.ic_chevron_end_24)
|
||||
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_END
|
||||
viewsAndReplies.text = getString(R.string.StoryViewerFragment__s_s, views, replies)
|
||||
if (post.replyCount == 0) {
|
||||
viewsAndReplies.icon = null
|
||||
viewsAndReplies.setText(R.string.StoryViewerPageFragment__views_off)
|
||||
} else {
|
||||
viewsAndReplies.setIconResource(R.drawable.ic_chevron_end_24)
|
||||
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_END
|
||||
viewsAndReplies.text = replies
|
||||
}
|
||||
}
|
||||
} else if (post.replyCount > 0) {
|
||||
viewsAndReplies.setIconResource(R.drawable.ic_chevron_end_24)
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
|
||||
/**
|
||||
* Open for testing.
|
||||
|
@ -35,6 +36,8 @@ open class StoryViewerPageRepository(context: Context) {
|
|||
|
||||
private val context = context.applicationContext
|
||||
|
||||
fun isReadReceiptsEnabled(): Boolean = TextSecurePreferences.isReadReceiptsEnabled(context)
|
||||
|
||||
private fun getStoryRecords(recipientId: RecipientId, isUnviewedOnly: Boolean): Observable<List<MessageRecord>> {
|
||||
return Observable.create { emitter ->
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
|
|
|
@ -6,7 +6,8 @@ data class StoryViewerPageState(
|
|||
val replyState: ReplyState = ReplyState.NONE,
|
||||
val isFirstPage: Boolean = false,
|
||||
val isDisplayingInitialState: Boolean = false,
|
||||
val isReady: Boolean = false
|
||||
val isReady: Boolean = false,
|
||||
val isReceiptsEnabled: Boolean
|
||||
) {
|
||||
/**
|
||||
* Indicates which Reply method is available when the user swipes on the dialog
|
||||
|
|
|
@ -28,7 +28,7 @@ class StoryViewerPageViewModel(
|
|||
private val repository: StoryViewerPageRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = RxStore(StoryViewerPageState())
|
||||
private val store = RxStore(StoryViewerPageState(isReceiptsEnabled = repository.isReadReceiptsEnabled()))
|
||||
private val disposables = CompositeDisposable()
|
||||
private val storyViewerDialogSubject: Subject<Optional<StoryViewerDialog>> = PublishSubject.create()
|
||||
|
||||
|
@ -46,6 +46,16 @@ class StoryViewerPageViewModel(
|
|||
refresh()
|
||||
}
|
||||
|
||||
fun checkReadReceiptState() {
|
||||
val isReceiptsEnabledInState = getStateSnapshot().isReceiptsEnabled
|
||||
val isReceiptsEnabledInRepository = repository.isReadReceiptsEnabled()
|
||||
if (isReceiptsEnabledInState xor isReceiptsEnabledInRepository) {
|
||||
store.update {
|
||||
it.copy(isReceiptsEnabled = isReceiptsEnabledInRepository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
disposables += repository.getStoryPostsFor(recipientId, isUnviewedOnly).subscribe { posts ->
|
||||
|
|
|
@ -8,6 +8,7 @@ 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.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerChild
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerParent
|
||||
|
@ -37,15 +38,28 @@ class StoryViewsFragment :
|
|||
StoryViewItem.register(adapter)
|
||||
|
||||
val emptyNotice: View = requireView().findViewById(R.id.empty_notice)
|
||||
val disabledNotice: View = requireView().findViewById(R.id.disabled_notice)
|
||||
val disabledButton: View = requireView().findViewById(R.id.disabled_button)
|
||||
|
||||
disabledButton.setOnClickListener {
|
||||
startActivity(AppSettingsActivity.privacy(requireContext()))
|
||||
}
|
||||
|
||||
onPageSelected(findListener<StoryViewsAndRepliesPagerParent>()?.selectedChild ?: StoryViewsAndRepliesPagerParent.Child.VIEWS)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
emptyNotice.visible = it.loadState == StoryViewsState.LoadState.READY && it.views.isEmpty()
|
||||
disabledNotice.visible = it.loadState == StoryViewsState.LoadState.DISABLED
|
||||
recyclerView?.visible = it.loadState == StoryViewsState.LoadState.READY
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
override fun onPageSelected(child: StoryViewsAndRepliesPagerParent.Child) {
|
||||
recyclerView?.isNestedScrollingEnabled = child == StoryViewsAndRepliesPagerParent.Child.VIEWS
|
||||
}
|
||||
|
|
|
@ -7,8 +7,12 @@ import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
|||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
|
||||
class StoryViewsRepository {
|
||||
|
||||
fun isReadReceiptsEnabled(): Boolean = TextSecurePreferences.isReadReceiptsEnabled(ApplicationDependencies.getApplication())
|
||||
|
||||
fun getViews(storyId: Long): Observable<List<StoryViewItemData>> {
|
||||
return Observable.create<List<StoryViewItemData>> { emitter ->
|
||||
fun refresh() {
|
||||
|
|
|
@ -6,6 +6,7 @@ data class StoryViewsState(
|
|||
) {
|
||||
enum class LoadState {
|
||||
INIT,
|
||||
READY
|
||||
READY,
|
||||
DISABLED
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,19 +7,27 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class StoryViewsViewModel(storyId: Long, repository: StoryViewsRepository) : ViewModel() {
|
||||
class StoryViewsViewModel(private val storyId: Long, private val repository: StoryViewsRepository) : ViewModel() {
|
||||
|
||||
private val store = Store(StoryViewsState())
|
||||
private val store = Store(StoryViewsState(StoryViewsState.LoadState.INIT))
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: LiveData<StoryViewsState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
disposables += repository.getViews(storyId).subscribe { data ->
|
||||
fun refresh() {
|
||||
if (repository.isReadReceiptsEnabled()) {
|
||||
disposables += repository.getViews(storyId).subscribe { data ->
|
||||
store.update {
|
||||
it.copy(
|
||||
views = data,
|
||||
loadState = StoryViewsState.LoadState.READY
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
store.update {
|
||||
it.copy(
|
||||
views = data,
|
||||
loadState = StoryViewsState.LoadState.READY
|
||||
loadState = StoryViewsState.LoadState.DISABLED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/signal_colorOnSurface_12" android:state_enabled="false" />
|
||||
<item android:color="@color/signal_colorOutline" />
|
||||
</selector>
|
|
@ -17,6 +17,45 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/disabled_notice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="disabled_label,disabled_button"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/disabled_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/StoryViewsFragment__enable_read_receipts_to_see_whos_viewed_your_story"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
android:visibility="gone"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/disabled_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disabled_button"
|
||||
style="@style/Signal.Widget.Button.Medium.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/StoryViewsFragment__go_to_settings"
|
||||
android:textColor="@color/signal_colorOnSurface"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/disabled_label" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -535,6 +535,16 @@
|
|||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_privacy"
|
||||
app:destination="@id/privacySettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- Internal Settings -->
|
||||
|
|
|
@ -127,6 +127,11 @@
|
|||
<item name="strokeWidth">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.Widget.Button.Medium.OutlinedButton" parent="Signal.Widget.Button.Medium.Secondary">
|
||||
<item name="strokeColor">@color/button_outline_color_selector</item>
|
||||
<item name="strokeWidth">1.5dp</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.Widget.Button.Base.Tonal" parent="Widget.Material3.Button.TonalButton">
|
||||
<item name="android:insetTop">0dp</item>
|
||||
<item name="android:insetBottom">0dp</item>
|
||||
|
|
|
@ -4590,6 +4590,8 @@
|
|||
<item quantity="one">%1$d reply</item>
|
||||
<item quantity="other">%1$d replies</item>
|
||||
</plurals>
|
||||
<!-- 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 -->
|
||||
<string name="StoryViewerFragment__s_s">%1$s %2$s</string>
|
||||
<!-- Displayed when viewing a post you sent -->
|
||||
|
@ -4602,6 +4604,10 @@
|
|||
<string name="StoryViewerPageFragment__reply_to_group">Reply to group</string>
|
||||
<!-- Displayed when a story has no views -->
|
||||
<string name="StoryViewsFragment__no_views_yet">No views yet</string>
|
||||
<!-- Displayed when user has disabled receipts -->
|
||||
<string name="StoryViewsFragment__enable_read_receipts_to_see_whos_viewed_your_story">Enable read receipts to see who\'s viewed your stories.</string>
|
||||
<!-- Button label displayed when user has disabled receipts -->
|
||||
<string name="StoryViewsFragment__go_to_settings">Go to settings</string>
|
||||
<!-- Displayed when a story has no replies yet -->
|
||||
<string name="StoryGroupReplyFragment__no_replies_yet">No replies yet</string>
|
||||
<!-- Displayed for each user that reacted to a story when viewing replies -->
|
||||
|
|
Ładowanie…
Reference in New Issue