Skip to content

Commit

Permalink
Implement Stories read receipt off state.
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-signal authored and greyson-signal committed Jul 5, 2022
1 parent f3873c8 commit e412cac
Show file tree
Hide file tree
Showing 14 changed files with 142 additions and 19 deletions.
Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
Expand Up @@ -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)
Expand Down Expand Up @@ -449,6 +449,7 @@ class StoryViewerPageFragment :
override fun onResume() {
super.onResume()
viewModel.setIsFragmentResumed(true)
viewModel.checkReadReceiptState()
}

override fun onPause() {
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -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()

Expand All @@ -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 ->
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Expand Up @@ -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() {
Expand Down
Expand Up @@ -6,6 +6,7 @@ data class StoryViewsState(
) {
enum class LoadState {
INIT,
READY
READY,
DISABLED
}
}
Expand Up @@ -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
)
}
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/color/button_outline_color_selector.xml
@@ -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>
39 changes: 39 additions & 0 deletions app/src/main/res/layout/stories_views_fragment.xml
Expand Up @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/navigation/app_settings.xml
Expand Up @@ -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 -->
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values/signal_styles.xml
Expand Up @@ -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>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Expand Up @@ -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 -->
Expand All @@ -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 -->
Expand Down

0 comments on commit e412cac

Please sign in to comment.