diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewItem.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewItem.kt index b18565bb5..77b851ad2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewItem.kt @@ -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() { 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() + + 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) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsFragment.kt index c2f948222..e27580fe1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsFragment.kt @@ -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,12 +69,37 @@ 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 { private const val ARG_STORY_ID = "arg.story.id" diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsRepository.kt index b1324bcec..9b032b2dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsRepository.kt @@ -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 { + return Single.fromCallable { + val record = SignalDatabase.mms.getMessageRecord(storyId) + + record.recipient + }.subscribeOn(Schedulers.io()) + } + fun getViews(storyId: Long): Observable> { return Observable.create> { 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.") + } + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsState.kt index b066ebebb..9d5432867 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsState.kt @@ -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 = emptyList() ) { enum class LoadState { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsViewModel.kt index b19180ad3..839fb077d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsViewModel.kt @@ -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 = 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 diff --git a/app/src/main/res/layout/stories_story_view_item.xml b/app/src/main/res/layout/stories_story_view_item.xml index 212cd91c5..d49f4b066 100644 --- a/app/src/main/res/layout/stories_story_view_item.xml +++ b/app/src/main/res/layout/stories_story_view_item.xml @@ -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"> 10dp 16dp 12dp + 16dp + 8dp 260dp diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index f5f4ddcbd..2a7c2eab5 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -200,6 +200,8 @@ 2dp 8dp 4dp + 8dp + 8dp 240dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a836ad9a..74d45fc30 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4628,6 +4628,14 @@ Enable read receipts to see who\'s viewed your stories. Go to settings + + Remove + + Remove viewer? + + %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. + + Remove viewer No replies yet