Allow users to remove viewers directly from stories.
This commit is contained in:
parent
2674fd2df4
commit
8f12b2041a
9 changed files with 133 additions and 6 deletions
|
@ -1,9 +1,13 @@
|
||||||
package org.thoughtcrime.securesms.stories.viewer.views
|
package org.thoughtcrime.securesms.stories.viewer.views
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import org.signal.core.util.DimensionUnit
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
|
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||||
|
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||||
|
@ -21,7 +25,10 @@ object StoryViewItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Model(
|
class Model(
|
||||||
val storyViewItemData: StoryViewItemData
|
val storyViewItemData: StoryViewItemData,
|
||||||
|
val canRemoveMember: Boolean,
|
||||||
|
val goToChat: (Model) -> Unit,
|
||||||
|
val removeFromStory: (Model) -> Unit
|
||||||
) : PreferenceModel<Model>() {
|
) : PreferenceModel<Model>() {
|
||||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
return storyViewItemData.recipient == newItem.storyViewItemData.recipient
|
return storyViewItemData.recipient == newItem.storyViewItemData.recipient
|
||||||
|
@ -30,6 +37,7 @@ object StoryViewItem {
|
||||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
return storyViewItemData == newItem.storyViewItemData &&
|
return storyViewItemData == newItem.storyViewItemData &&
|
||||||
storyViewItemData.recipient.hasSameContent(newItem.storyViewItemData.recipient) &&
|
storyViewItemData.recipient.hasSameContent(newItem.storyViewItemData.recipient) &&
|
||||||
|
canRemoveMember == newItem.canRemoveMember &&
|
||||||
super.areContentsTheSame(newItem)
|
super.areContentsTheSame(newItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,10 +52,47 @@ object StoryViewItem {
|
||||||
avatarView.setAvatar(model.storyViewItemData.recipient)
|
avatarView.setAvatar(model.storyViewItemData.recipient)
|
||||||
nameView.text = model.storyViewItemData.recipient.getDisplayName(context)
|
nameView.text = model.storyViewItemData.recipient.getDisplayName(context)
|
||||||
viewedAtView.text = formatDate(model.storyViewItemData.timeViewedInMillis)
|
viewedAtView.text = formatDate(model.storyViewItemData.timeViewedInMillis)
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
showContextMenu(model)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatDate(dateInMilliseconds: Long): String {
|
private fun formatDate(dateInMilliseconds: Long): String {
|
||||||
return DateUtils.formatDateWithDayOfWeek(Locale.getDefault(), dateInMilliseconds)
|
return DateUtils.formatDateWithDayOfWeek(Locale.getDefault(), dateInMilliseconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showContextMenu(model: Model) {
|
||||||
|
itemView.isSelected = true
|
||||||
|
|
||||||
|
val actions = mutableListOf<ActionItem>()
|
||||||
|
|
||||||
|
actions.add(
|
||||||
|
ActionItem(
|
||||||
|
iconRes = R.drawable.ic_open_24_tinted,
|
||||||
|
title = context.getString(R.string.StoriesLandingItem__go_to_chat),
|
||||||
|
action = {
|
||||||
|
model.goToChat(model)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (model.canRemoveMember) {
|
||||||
|
actions.add(
|
||||||
|
ActionItem(
|
||||||
|
iconRes = R.drawable.ic_minus_circle_20,
|
||||||
|
title = context.getString(R.string.StoryViewItem__remove_viewer),
|
||||||
|
action = {
|
||||||
|
model.removeFromStory(model)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup)
|
||||||
|
.offsetY(DimensionUnit.DP.toPixels(16f).toInt())
|
||||||
|
.onDismiss { itemView.isSelected = false }
|
||||||
|
.show(actions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,15 @@ import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||||
import org.thoughtcrime.securesms.components.settings.configure
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerChild
|
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerChild
|
||||||
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerParent
|
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerParent
|
||||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||||
|
@ -66,12 +69,37 @@ class StoryViewsFragment :
|
||||||
|
|
||||||
private fun getConfiguration(state: StoryViewsState): DSLConfiguration {
|
private fun getConfiguration(state: StoryViewsState): DSLConfiguration {
|
||||||
return configure {
|
return configure {
|
||||||
state.views.forEach {
|
state.views.forEach { storyViewItemData ->
|
||||||
customPref(StoryViewItem.Model(it))
|
customPref(
|
||||||
|
StoryViewItem.Model(
|
||||||
|
storyViewItemData = storyViewItemData,
|
||||||
|
canRemoveMember = state.storyRecipient?.isDistributionList ?: false,
|
||||||
|
goToChat = {
|
||||||
|
val chatIntent = ConversationIntents.createBuilder(requireContext(), it.storyViewItemData.recipient.id, -1L).build()
|
||||||
|
startActivity(chatIntent)
|
||||||
|
},
|
||||||
|
removeFromStory = {
|
||||||
|
if (state.storyRecipient?.isDistributionList == true) {
|
||||||
|
confirmRemoveFromStory(it.storyViewItemData.recipient, state.storyRecipient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun confirmRemoveFromStory(user: Recipient, story: Recipient) {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.StoryViewsFragment__remove_viewer)
|
||||||
|
.setMessage(getString(R.string.StoryViewsFragment__s_will_still_be_able, user.getShortDisplayName(requireContext()), story.getDisplayName(requireContext())))
|
||||||
|
.setPositiveButton(R.string.StoryViewsFragment__remove) { _, _ ->
|
||||||
|
viewModel.removeUserFromStory(user, story)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_STORY_ID = "arg.story.id"
|
private const val ARG_STORY_ID = "arg.story.id"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package org.thoughtcrime.securesms.stories.viewer.views
|
package org.thoughtcrime.securesms.stories.viewer.views
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
@ -11,8 +14,20 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
|
||||||
class StoryViewsRepository {
|
class StoryViewsRepository {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(StoryViewsRepository::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
fun isReadReceiptsEnabled(): Boolean = TextSecurePreferences.isReadReceiptsEnabled(ApplicationDependencies.getApplication())
|
fun isReadReceiptsEnabled(): Boolean = TextSecurePreferences.isReadReceiptsEnabled(ApplicationDependencies.getApplication())
|
||||||
|
|
||||||
|
fun getStoryRecipient(storyId: Long): Single<Recipient> {
|
||||||
|
return Single.fromCallable {
|
||||||
|
val record = SignalDatabase.mms.getMessageRecord(storyId)
|
||||||
|
|
||||||
|
record.recipient
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
fun getViews(storyId: Long): Observable<List<StoryViewItemData>> {
|
fun getViews(storyId: Long): Observable<List<StoryViewItemData>> {
|
||||||
return Observable.create<List<StoryViewItemData>> { emitter ->
|
return Observable.create<List<StoryViewItemData>> { emitter ->
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
|
@ -38,4 +53,15 @@ class StoryViewsRepository {
|
||||||
refresh()
|
refresh()
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeUserFromStory(user: Recipient, story: Recipient): Completable {
|
||||||
|
return Completable.fromAction {
|
||||||
|
val distributionListRecord = SignalDatabase.distributionLists.getList(story.requireDistributionListId())!!
|
||||||
|
if (user.id in distributionListRecord.members) {
|
||||||
|
SignalDatabase.distributionLists.excludeFromStory(user.id, distributionListRecord)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "User is no longer in the distribution list.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package org.thoughtcrime.securesms.stories.viewer.views
|
package org.thoughtcrime.securesms.stories.viewer.views
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
|
||||||
data class StoryViewsState(
|
data class StoryViewsState(
|
||||||
val loadState: LoadState = LoadState.INIT,
|
val loadState: LoadState = LoadState.INIT,
|
||||||
|
val storyRecipient: Recipient? = null,
|
||||||
val views: List<StoryViewItemData> = emptyList()
|
val views: List<StoryViewItemData> = emptyList()
|
||||||
) {
|
) {
|
||||||
enum class LoadState {
|
enum class LoadState {
|
||||||
|
|
|
@ -5,17 +5,24 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
|
||||||
class StoryViewsViewModel(private val storyId: Long, private val repository: StoryViewsRepository) : ViewModel() {
|
class StoryViewsViewModel(private val storyId: Long, private val repository: StoryViewsRepository) : ViewModel() {
|
||||||
|
|
||||||
private val store = Store(StoryViewsState(StoryViewsState.LoadState.INIT))
|
private val store = Store(StoryViewsState())
|
||||||
private val disposables = CompositeDisposable()
|
private val disposables = CompositeDisposable()
|
||||||
|
|
||||||
val state: LiveData<StoryViewsState> = store.stateLiveData
|
val state: LiveData<StoryViewsState> = store.stateLiveData
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
if (repository.isReadReceiptsEnabled()) {
|
if (repository.isReadReceiptsEnabled()) {
|
||||||
|
disposables += repository.getStoryRecipient(storyId).subscribe { storyRecipient ->
|
||||||
|
store.update {
|
||||||
|
it.copy(storyRecipient = storyRecipient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disposables += repository.getViews(storyId).subscribe { data ->
|
disposables += repository.getViews(storyId).subscribe { data ->
|
||||||
store.update {
|
store.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
@ -37,6 +44,10 @@ class StoryViewsViewModel(private val storyId: Long, private val repository: Sto
|
||||||
disposables.clear()
|
disposables.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeUserFromStory(user: Recipient, story: Recipient) {
|
||||||
|
repository.removeUserFromStory(user, story).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
private val storyId: Long,
|
private val storyId: Long,
|
||||||
private val repository: StoryViewsRepository
|
private val repository: StoryViewsRepository
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="@dimen/dsl_settings_gutter"
|
android:layout_marginHorizontal="@dimen/selectable_list_item_margin"
|
||||||
android:minHeight="64dp">
|
android:background="@drawable/selectable_list_item_background"
|
||||||
|
android:minHeight="64dp"
|
||||||
|
android:paddingHorizontal="@dimen/selectable_list_item_padding">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
android:id="@+id/avatar"
|
android:id="@+id/avatar"
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
<dimen name="media_overview_toggle_gutter">10dp</dimen>
|
<dimen name="media_overview_toggle_gutter">10dp</dimen>
|
||||||
<dimen name="wallpaper_selection_gutter">16dp</dimen>
|
<dimen name="wallpaper_selection_gutter">16dp</dimen>
|
||||||
<dimen name="safety_number_recipient_row_item_gutter">12dp</dimen>
|
<dimen name="safety_number_recipient_row_item_gutter">12dp</dimen>
|
||||||
|
<dimen name="selectable_list_item_margin">16dp</dimen>
|
||||||
|
<dimen name="selectable_list_item_padding">8dp</dimen>
|
||||||
|
|
||||||
<dimen name="chat_colors_preview_bubble_max_width">260dp</dimen>
|
<dimen name="chat_colors_preview_bubble_max_width">260dp</dimen>
|
||||||
|
|
||||||
|
|
|
@ -200,6 +200,8 @@
|
||||||
<dimen name="media_overview_toggle_gutter">2dp</dimen>
|
<dimen name="media_overview_toggle_gutter">2dp</dimen>
|
||||||
<dimen name="wallpaper_selection_gutter">8dp</dimen>
|
<dimen name="wallpaper_selection_gutter">8dp</dimen>
|
||||||
<dimen name="safety_number_recipient_row_item_gutter">4dp</dimen>
|
<dimen name="safety_number_recipient_row_item_gutter">4dp</dimen>
|
||||||
|
<dimen name="selectable_list_item_margin">8dp</dimen>
|
||||||
|
<dimen name="selectable_list_item_padding">8dp</dimen>
|
||||||
|
|
||||||
<dimen name="chat_colors_preview_bubble_max_width">240dp</dimen>
|
<dimen name="chat_colors_preview_bubble_max_width">240dp</dimen>
|
||||||
|
|
||||||
|
|
|
@ -4628,6 +4628,14 @@
|
||||||
<string name="StoryViewsFragment__enable_read_receipts_to_see_whos_viewed_your_story">Enable read receipts to see who\'s viewed your stories.</string>
|
<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 -->
|
<!-- Button label displayed when user has disabled receipts -->
|
||||||
<string name="StoryViewsFragment__go_to_settings">Go to settings</string>
|
<string name="StoryViewsFragment__go_to_settings">Go to settings</string>
|
||||||
|
<!-- Dialog action to remove viewer from a story -->
|
||||||
|
<string name="StoryViewsFragment__remove">Remove</string>
|
||||||
|
<!-- Dialog title when removing a viewer from a story -->
|
||||||
|
<string name="StoryViewsFragment__remove_viewer">Remove viewer?</string>
|
||||||
|
<!-- Dialog message when removing a viewer from a story -->
|
||||||
|
<string name="StoryViewsFragment__s_will_still_be_able">%1$s will still be able to view this post, but will not be able to view any future posts you share to %2$s.</string>
|
||||||
|
<!-- Story View context menu action to remove them from a story -->
|
||||||
|
<string name="StoryViewItem__remove_viewer">Remove viewer</string>
|
||||||
<!-- Displayed when a story has no replies yet -->
|
<!-- Displayed when a story has no replies yet -->
|
||||||
<string name="StoryGroupReplyFragment__no_replies_yet">No replies yet</string>
|
<string name="StoryGroupReplyFragment__no_replies_yet">No replies yet</string>
|
||||||
<!-- Displayed for each user that reacted to a story when viewing replies -->
|
<!-- Displayed for each user that reacted to a story when viewing replies -->
|
||||||
|
|
Loading…
Add table
Reference in a new issue