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
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
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.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
|
@ -21,7 +25,10 @@ object StoryViewItem {
|
|||
}
|
||||
|
||||
class Model(
|
||||
val storyViewItemData: StoryViewItemData
|
||||
val storyViewItemData: StoryViewItemData,
|
||||
val canRemoveMember: Boolean,
|
||||
val goToChat: (Model) -> Unit,
|
||||
val removeFromStory: (Model) -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return storyViewItemData.recipient == newItem.storyViewItemData.recipient
|
||||
|
@ -30,6 +37,7 @@ object StoryViewItem {
|
|||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return storyViewItemData == newItem.storyViewItemData &&
|
||||
storyViewItemData.recipient.hasSameContent(newItem.storyViewItemData.recipient) &&
|
||||
canRemoveMember == newItem.canRemoveMember &&
|
||||
super.areContentsTheSame(newItem)
|
||||
}
|
||||
}
|
||||
|
@ -44,10 +52,47 @@ object StoryViewItem {
|
|||
avatarView.setAvatar(model.storyViewItemData.recipient)
|
||||
nameView.text = model.storyViewItemData.recipient.getDisplayName(context)
|
||||
viewedAtView.text = formatDate(model.storyViewItemData.timeViewedInMillis)
|
||||
|
||||
itemView.setOnClickListener {
|
||||
showContextMenu(model)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDate(dateInMilliseconds: Long): String {
|
||||
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 androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
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.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerChild
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerParent
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
|
@ -66,10 +69,35 @@ class StoryViewsFragment :
|
|||
|
||||
private fun getConfiguration(state: StoryViewsState): DSLConfiguration {
|
||||
return configure {
|
||||
state.views.forEach {
|
||||
customPref(StoryViewItem.Model(it))
|
||||
state.views.forEach { storyViewItemData ->
|
||||
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 {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.views
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
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.SignalDatabase
|
||||
|
@ -11,8 +14,20 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
|||
|
||||
class StoryViewsRepository {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(StoryViewsRepository::class.java)
|
||||
}
|
||||
|
||||
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>> {
|
||||
return Observable.create<List<StoryViewItemData>> { emitter ->
|
||||
fun refresh() {
|
||||
|
@ -38,4 +53,15 @@ class StoryViewsRepository {
|
|||
refresh()
|
||||
}.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
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class StoryViewsState(
|
||||
val loadState: LoadState = LoadState.INIT,
|
||||
val storyRecipient: Recipient? = null,
|
||||
val views: List<StoryViewItemData> = emptyList()
|
||||
) {
|
||||
enum class LoadState {
|
||||
|
|
|
@ -5,17 +5,24 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
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()
|
||||
|
||||
val state: LiveData<StoryViewsState> = store.stateLiveData
|
||||
|
||||
fun refresh() {
|
||||
if (repository.isReadReceiptsEnabled()) {
|
||||
disposables += repository.getStoryRecipient(storyId).subscribe { storyRecipient ->
|
||||
store.update {
|
||||
it.copy(storyRecipient = storyRecipient)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += repository.getViews(storyId).subscribe { data ->
|
||||
store.update {
|
||||
it.copy(
|
||||
|
@ -37,6 +44,10 @@ class StoryViewsViewModel(private val storyId: Long, private val repository: Sto
|
|||
disposables.clear()
|
||||
}
|
||||
|
||||
fun removeUserFromStory(user: Recipient, story: Recipient) {
|
||||
repository.removeUserFromStory(user, story).subscribe()
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val storyId: Long,
|
||||
private val repository: StoryViewsRepository
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/dsl_settings_gutter"
|
||||
android:minHeight="64dp">
|
||||
android:layout_marginHorizontal="@dimen/selectable_list_item_margin"
|
||||
android:background="@drawable/selectable_list_item_background"
|
||||
android:minHeight="64dp"
|
||||
android:paddingHorizontal="@dimen/selectable_list_item_padding">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/avatar"
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
<dimen name="media_overview_toggle_gutter">10dp</dimen>
|
||||
<dimen name="wallpaper_selection_gutter">16dp</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>
|
||||
|
||||
|
|
|
@ -200,6 +200,8 @@
|
|||
<dimen name="media_overview_toggle_gutter">2dp</dimen>
|
||||
<dimen name="wallpaper_selection_gutter">8dp</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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
<!-- Button label displayed when user has disabled receipts -->
|
||||
<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 -->
|
||||
<string name="StoryGroupReplyFragment__no_replies_yet">No replies yet</string>
|
||||
<!-- Displayed for each user that reacted to a story when viewing replies -->
|
||||
|
|
Loading…
Add table
Reference in a new issue