Add Reminders and Conversation Banner to CFv2.

This commit is contained in:
Cody Henthorne
2023-05-24 22:47:05 -04:00
parent 0aca03a919
commit 6b91e525db
35 changed files with 501 additions and 182 deletions

View File

@@ -240,7 +240,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
private ConversationScrollToView scrollToBottomButton;
private ConversationScrollToView scrollToMentionButton;
private TextView scrollDateHeader;
private ConversationBannerView conversationBanner;
private ConversationHeaderView conversationHeader;
private MessageRequestViewModel messageRequestViewModel;
private MessageCountsViewModel messageCountsViewModel;
private ConversationViewModel conversationViewModel;
@@ -350,7 +350,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
getViewLifecycleOwner().getLifecycle().addObserver(multiselectItemDecoration);
snapToTopDataObserver = new ConversationSnapToTopDataObserver(list, new ConversationScrollRequestValidator());
conversationBanner = (ConversationBannerView) inflater.inflate(R.layout.conversation_item_banner, container, false);
conversationHeader = (ConversationHeaderView) inflater.inflate(R.layout.conversation_item_banner, container, false);
topLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
bottomLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
@@ -586,7 +586,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
listener.onMessageRequest(messageRequestViewModel);
messageRequestViewModel.getRecipientInfo().observe(getViewLifecycleOwner(), recipientInfo -> {
presentMessageRequestProfileView(requireContext(), recipientInfo, conversationBanner);
presentMessageRequestProfileView(requireContext(), recipientInfo, conversationHeader);
});
messageRequestViewModel.getMessageData().observe(getViewLifecycleOwner(), data -> {
@@ -597,7 +597,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
});
}
private void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationBannerView conversationBanner) {
private void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationHeaderView conversationBanner) {
if (conversationBanner == null) {
return;
}
@@ -1223,7 +1223,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
return;
}
adapter.setFooterView(conversationBanner);
adapter.setFooterView(conversationHeader);
Runnable afterScroll = () -> {
if (!conversation.getMessageRequestData().isMessageRequestAccepted()) {
@@ -1384,7 +1384,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
Rect rect = new Rect();
toolbar.getGlobalVisibleRect(rect);
conversationViewModel.setToolbarBottom(rect.bottom);
ViewUtil.setTopMargin(conversationBanner, rect.bottom + ViewUtil.dpToPx(16));
ViewUtil.setTopMargin(conversationHeader, rect.bottom + ViewUtil.dpToPx(16));
toolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});

View File

@@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.SpanUtil;
public class ConversationBannerView extends ConstraintLayout {
public class ConversationHeaderView extends ConstraintLayout {
private AvatarImageView contactAvatar;
private TextView contactTitle;
@@ -35,15 +35,15 @@ public class ConversationBannerView extends ConstraintLayout {
private View tapToView;
private BadgeImageView contactBadge;
public ConversationBannerView(Context context) {
public ConversationHeaderView(Context context) {
this(context, null);
}
public ConversationBannerView(Context context, AttributeSet attrs) {
public ConversationHeaderView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ConversationBannerView(Context context, AttributeSet attrs, int defStyleAttr) {
public ConversationHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(getContext(), R.layout.conversation_banner_view, this);

View File

@@ -1736,7 +1736,7 @@ public class ConversationParentFragment extends Fragment
reminderView.get().setOnActionClickListener(this::handleReminderAction);
} else if (ServiceOutageReminder.isEligible(context)) {
ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob());
reminderView.get().showReminder(new ServiceOutageReminder(context));
reminderView.get().showReminder(new ServiceOutageReminder());
} else if (SignalStore.account().isRegistered() &&
TextSecurePreferences.isShowInviteReminders(context) &&
!viewModel.isPushAvailable() &&
@@ -1746,14 +1746,14 @@ public class ConversationParentFragment extends Fragment
reminderView.get().setOnDismissListener(() -> inviteReminderModel.dismissReminder());
reminderView.get().showReminder(inviteReminder.get());
} else if (actionableRequestingMembers != null && actionableRequestingMembers > 0) {
reminderView.get().showReminder(PendingGroupJoinRequestsReminder.create(context, actionableRequestingMembers));
reminderView.get().showReminder(new PendingGroupJoinRequestsReminder(actionableRequestingMembers));
reminderView.get().setOnActionClickListener(id -> {
if (id == R.id.reminder_action_review_join_requests) {
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(context, getRecipient().getGroupId().get().requireV2()));
}
});
} else if (gv1MigrationSuggestions != null && gv1MigrationSuggestions.size() > 0 && recipient.get().isPushV2Group()) {
reminderView.get().showReminder(new GroupsV1MigrationSuggestionsReminder(context, gv1MigrationSuggestions));
reminderView.get().showReminder(new GroupsV1MigrationSuggestionsReminder(gv1MigrationSuggestions));
reminderView.get().setOnActionClickListener(actionId -> {
if (actionId == R.id.reminder_action_gv1_suggestion_add_members) {
GroupsV1MigrationSuggestionsDialog.show(requireActivity(), recipient.get().requireGroupId().requireV2(), gv1MigrationSuggestions);
@@ -1764,12 +1764,12 @@ public class ConversationParentFragment extends Fragment
reminderView.get().setOnDismissListener(() -> {
});
} else if (isInBubble() && !SignalStore.tooltips().hasSeenBubbleOptOutTooltip() && Build.VERSION.SDK_INT > 29) {
reminderView.get().showReminder(new BubbleOptOutReminder(context));
reminderView.get().showReminder(new BubbleOptOutReminder());
reminderView.get().setOnActionClickListener(actionId -> {
SignalStore.tooltips().markBubbleOptOutTooltipSeen();
reminderView.get().hide();
if (actionId == R.id.reminder_action_turn_off) {
if (actionId == R.id.reminder_action_bubble_turn_off) {
Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().getPackageName())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -3162,14 +3162,14 @@ public class ConversationParentFragment extends Fragment
Reminder reminder = new ExpiredBuildReminder(requireContext());
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
.setMessage(reminder.getText())
.setMessage(reminder.getText(requireContext()))
.setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss());
List<Reminder.Action> actions = reminder.getActions();
if (actions.size() == 1) {
Reminder.Action action = actions.get(0);
builder.setNeutralButton(action.getTitle(), (d, i) -> {
builder.setNeutralButton(action.getTitle(requireContext()), (d, i) -> {
if (action.getActionId() == R.id.reminder_action_update_now) {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
}

View File

@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.BindableConversationItem
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.ConversationAdapter
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
import org.thoughtcrime.securesms.conversation.ConversationBannerView
import org.thoughtcrime.securesms.conversation.ConversationHeaderView
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.Colorizable
@@ -366,7 +366,7 @@ class ConversationAdapterV2(
}
inner class ThreadHeaderViewHolder(itemView: View) : MappingViewHolder<ThreadHeader>(itemView) {
private val conversationBanner: ConversationBannerView = itemView as ConversationBannerView
private val conversationBanner: ConversationHeaderView = itemView as ConversationHeaderView
override fun bind(model: ThreadHeader) {
val (recipient, groupInfo, sharedGroups, messageRequestState) = model.recipientInfo

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
import android.transition.Slide
import android.transition.TransitionManager
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.transition.addListener
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.reminder.Reminder
import org.thoughtcrime.securesms.components.reminder.ReminderView
/**
* Responsible for showing the various "banner" views at the top of a conversation
*
* - Expired Build
* - Unregistered
* - Group join requests
* - GroupV1 suggestions
* - Disable Chat Bubbles setting
* - Service outage
*/
class ConversationBannerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayoutCompat(context, attrs, defStyleAttr) {
private val inflater: LayoutInflater by lazy { LayoutInflater.from(context) }
private var reminderView: ReminderView? = null
private var currentView: View? = null
var listener: Listener? = null
init {
orientation = VERTICAL
}
fun showAsReminder(reminder: Reminder) {
reminderView = show(
existingView = reminderView,
create = { ReminderView(context) },
bind = {
showReminder(reminder)
setOnActionClickListener {
when (it) {
R.id.reminder_action_update_now -> listener?.updateAppAction()
R.id.reminder_action_re_register -> listener?.reRegisterAction()
R.id.reminder_action_review_join_requests -> listener?.reviewJoinRequestsAction()
R.id.reminder_action_gv1_suggestion_no_thanks -> listener?.gv1SuggestionsAction(it)
R.id.reminder_action_bubble_not_now, R.id.reminder_action_bubble_turn_off -> {
listener?.changeBubbleSettingAction(disableSetting = it == R.id.reminder_action_bubble_turn_off)
}
}
}
setOnHideListener {
removeIfNotNull(reminderView)
reminderView = null
true
}
}
)
}
fun clear() {
removeAllViews()
reminderView = null
currentView = null
}
private fun <V : View> show(existingView: V?, create: () -> V, bind: V.() -> Unit = {}): V {
if (existingView != currentView) {
removeIfNotNull(currentView)
}
val view: V = if (existingView != null) {
existingView
} else {
val newView: V = create()
TransitionManager.beginDelayedTransition(this, Slide(Gravity.TOP))
addView(newView, defaultLayoutParams())
newView
}
view.bind()
currentView = view
return view
}
private fun defaultLayoutParams(): LayoutParams {
return LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
}
private fun removeIfNotNull(view: View?) {
if (view != null) {
val transition = Slide(Gravity.TOP).apply {
addListener(
onEnd = {
layoutParams = layoutParams.apply { height = LayoutParams.WRAP_CONTENT }
}
)
}
layoutParams = layoutParams.apply { height = this@ConversationBannerView.height }
TransitionManager.beginDelayedTransition(this, transition)
removeView(view)
}
}
interface Listener {
fun updateAppAction()
fun reRegisterAction()
fun reviewJoinRequestsAction()
fun gv1SuggestionsAction(actionId: Int)
fun changeBubbleSettingAction(disableSetting: Boolean)
}
}

View File

@@ -13,6 +13,7 @@ import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
@@ -141,10 +142,12 @@ import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
import org.thoughtcrime.securesms.groups.ui.GroupErrors
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
import org.thoughtcrime.securesms.invites.InviteActions
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -225,11 +228,11 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
private val binding by ViewBinderDelegate(V2ConversationFragmentBinding::bind)
private val viewModel: ConversationViewModel by viewModel {
ConversationViewModel(
args.threadId,
args.startingPosition,
ConversationRepository(requireContext()),
conversationRecipientRepository,
messageRequestRepository
threadId = args.threadId,
requestedStartingPosition = args.startingPosition,
repository = ConversationRepository(context = requireContext(), isInBubble = args.conversationScreenType == ConversationScreenType.BUBBLE),
recipientRepository = conversationRecipientRepository,
messageRequestRepository = messageRequestRepository
)
}
@@ -321,12 +324,14 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
conversationToolbarOnScrollHelper.attach(binding.conversationItemRecycler)
presentWallpaper(args.wallpaper)
presentChatColors(args.chatColors)
presentConversationTitle(viewModel.recipientSnapshot)
presentActionBarMenu()
observeConversationThread()
viewModel
.inputReadyState
.distinctUntilChanged()
.subscribeBy(
onNext = this::presentInputReadyState
)
@@ -400,6 +405,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
disposables += viewModel.recipient
.observeOn(AndroidSchedulers.mainThread())
.distinctUntilChanged { r1, r2 -> r1 === r2 || r1.hasSameContent(r2) }
.subscribeBy(onNext = this::onRecipientChanged)
disposables += viewModel.markReadRequests
@@ -473,9 +479,23 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
val conversationReactionStub = Stub<ConversationReactionOverlay>(binding.conversationReactionScrubberStub)
reactionDelegate = ConversationReactionDelegate(conversationReactionStub)
reactionDelegate.setOnReactionSelectedListener(OnReactionsSelectedListener())
binding.conversationBanner.listener = ConversationBannerListener()
viewModel
.reminder
.subscribeBy { reminder ->
if (reminder.isPresent) {
binding.conversationBanner.showAsReminder(reminder.get())
} else {
binding.conversationBanner.clear()
}
}
.addTo(disposables)
}
private fun presentInputReadyState(inputReadyState: InputReadyState) {
presentConversationTitle(inputReadyState.conversationRecipient)
val disabledInputView = binding.conversationDisabledInput
var inputDisabled = true
@@ -564,7 +584,11 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
}
}
private fun presentConversationTitle(recipient: Recipient) {
private fun presentConversationTitle(recipient: Recipient?) {
if (recipient == null) {
return
}
binding.conversationTitleView.root.setTitle(GlideApp.with(this), recipient)
}
@@ -1778,6 +1802,49 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
}
}
//region Conversation Banner Callbacks
private inner class ConversationBannerListener : ConversationBannerView.Listener {
override fun updateAppAction() {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
}
override fun reRegisterAction() {
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
}
override fun reviewJoinRequestsAction() {
viewModel.recipientSnapshot?.let { recipient ->
val intent = ManagePendingAndRequestingMembersActivity.newIntent(requireContext(), recipient.requireGroupId().requireV2())
startActivity(intent)
}
}
override fun gv1SuggestionsAction(actionId: Int) {
if (actionId == R.id.reminder_action_gv1_suggestion_add_members) {
conversationGroupViewModel.groupRecordSnapshot?.let { groupRecord ->
GroupsV1MigrationSuggestionsDialog.show(requireActivity(), groupRecord.id.requireV2(), groupRecord.gv1MigrationSuggestions)
}
} else if (actionId == R.id.reminder_action_gv1_suggestion_no_thanks) {
conversationGroupViewModel.onSuggestedMembersBannerDismissed()
}
}
@SuppressLint("InlinedApi")
override fun changeBubbleSettingAction(disableSetting: Boolean) {
SignalStore.tooltips().markBubbleOptOutTooltipSeen()
if (disableSetting) {
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
}
}
//endregion
//region Disabled Input Callbacks
private inner class DisabledInputListener : DisabledInputView.Listener {

View File

@@ -6,25 +6,38 @@
package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
import android.os.Build
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.toOptional
import org.signal.libsignal.protocol.InvalidMessageException
import org.signal.paging.PagedData
import org.signal.paging.PagingConfig
import org.thoughtcrime.securesms.components.reminder.BubbleOptOutReminder
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder
import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationSuggestionsReminder
import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsReminder
import org.thoughtcrime.securesms.components.reminder.Reminder
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.v2.data.ConversationDataSource
import org.thoughtcrime.securesms.database.RxDatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.Quote
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.mms.SlideDeck
@@ -33,10 +46,14 @@ import org.thoughtcrime.securesms.recipients.RecipientFormattingException
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.SignalLocalMetrics
import java.util.Optional
import kotlin.math.max
import kotlin.time.Duration.Companion.seconds
class ConversationRepository(context: Context) {
class ConversationRepository(
context: Context,
private val isInBubble: Boolean
) {
private val applicationContext = context.applicationContext
private val oldConversationRepository = org.thoughtcrime.securesms.conversation.ConversationRepository()
@@ -184,6 +201,31 @@ class ConversationRepository(context: Context) {
return SignalDatabase.messages.getUnreadMentionCount(threadId)
}
fun getReminder(groupRecord: GroupRecord?): Maybe<Optional<Reminder>> {
return Maybe.fromCallable {
val reminder: Reminder? = when {
ExpiredBuildReminder.isEligible() -> ExpiredBuildReminder(applicationContext)
UnauthorizedReminder.isEligible(applicationContext) -> UnauthorizedReminder(applicationContext)
ServiceOutageReminder.isEligible(applicationContext) -> {
ApplicationDependencies.getJobManager().add(ServiceOutageDetectionJob())
ServiceOutageReminder()
}
groupRecord != null && groupRecord.actionableRequestingMembersCount > 0 -> {
PendingGroupJoinRequestsReminder(groupRecord.actionableRequestingMembersCount)
}
groupRecord != null && groupRecord.gv1MigrationSuggestions.isNotEmpty() -> {
GroupsV1MigrationSuggestionsReminder(groupRecord.gv1MigrationSuggestions)
}
isInBubble && !SignalStore.tooltips().hasSeenBubbleOptOutTooltip() && Build.VERSION.SDK_INT > 29 -> {
BubbleOptOutReminder()
}
else -> null
}
reminder.toOptional()
}
}
data class MessageCounts(
val unread: Int,
val mentions: Int

View File

@@ -17,9 +17,12 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.signal.core.util.concurrent.subscribeWithSubject
import org.signal.core.util.orNull
import org.signal.paging.ProxyPagingController
import org.thoughtcrime.securesms.components.reminder.Reminder
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
@@ -41,6 +44,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.hasGiftBadge
import org.thoughtcrime.securesms.util.rx.RxStore
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import java.util.Optional
/**
* ConversationViewModel, which operates solely off of a thread id that never changes.
@@ -63,8 +67,7 @@ class ConversationViewModel(
val showScrollButtonsSnapshot: Boolean
get() = scrollButtonStateStore.state.showScrollButtons
private val _recipient: BehaviorSubject<Recipient> = BehaviorSubject.create()
val recipient: Observable<Recipient> = _recipient
val recipient: Observable<Recipient> = recipientRepository.conversationRecipient
private val _conversationThreadState: Subject<ConversationThreadState> = BehaviorSubject.create()
val conversationThreadState: Single<ConversationThreadState> = _conversationThreadState.firstOrError()
@@ -76,13 +79,14 @@ class ConversationViewModel(
val pagingController = ProxyPagingController<ConversationElementKey>()
val nameColorsMap: Observable<Map<RecipientId, NameColor>> = _recipient.flatMap { repository.getNameColorsMap(it, groupAuthorNameColorHelper) }
val nameColorsMap: Observable<Map<RecipientId, NameColor>> = recipient.flatMap { repository.getNameColorsMap(it, groupAuthorNameColorHelper) }
val recipientSnapshot: Recipient?
get() = _recipient.value
@Volatile
var recipientSnapshot: Recipient? = null
private set
val wallpaperSnapshot: ChatWallpaper?
get() = _recipient.value?.wallpaper
get() = recipientSnapshot?.wallpaper
val inputReadyState: Observable<InputReadyState>
@@ -90,12 +94,15 @@ class ConversationViewModel(
val hasMessageRequestState: Boolean
get() = hasMessageRequestStateSubject.value ?: false
private val refreshReminder: Subject<Unit> = PublishSubject.create()
val reminder: Observable<Optional<Reminder>>
init {
disposables += recipientRepository
.conversationRecipient
.subscribeBy(onNext = {
_recipient.onNext(it)
})
disposables += recipient
.subscribeBy {
recipientSnapshot = it
}
disposables += recipientRepository
.conversationRecipient
@@ -156,6 +163,13 @@ class ConversationViewModel(
}.doOnNext {
hasMessageRequestStateSubject.onNext(it.messageRequestState != MessageRequestState.NONE)
}.observeOn(AndroidSchedulers.mainThread())
recipientRepository.conversationRecipient.map { Unit }.subscribeWithSubject(refreshReminder, disposables)
reminder = Observable.combineLatest(refreshReminder.startWithItem(Unit), recipientRepository.groupRecord) { _, groupRecord -> groupRecord }
.subscribeOn(Schedulers.io())
.flatMapMaybe { groupRecord -> repository.getReminder(groupRecord.orNull()) }
.observeOn(AndroidSchedulers.mainThread())
}
override fun onCleared() {

View File

@@ -153,15 +153,15 @@ class DisabledInputView @JvmOverloads constructor(
announcementGroupOnly = null
}
private fun <VIEW : View> show(existingView: VIEW?, create: () -> VIEW, bind: VIEW.() -> Unit = {}): VIEW {
private fun <V : View> show(existingView: V?, create: () -> V, bind: V.() -> Unit = {}): V {
if (existingView != currentView) {
removeIfNotNull(currentView)
}
val view: VIEW = if (existingView != null) {
val view: V = if (existingView != null) {
existingView
} else {
val newView: VIEW = create()
val newView: V = create()
addView(newView, defaultLayoutParams())
newView
}

View File

@@ -6,6 +6,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.addTo
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.Subject
@@ -14,13 +15,11 @@ import org.signal.core.util.concurrent.subscribeWithSubject
import org.thoughtcrime.securesms.conversation.v2.ConversationRecipientRepository
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
import org.thoughtcrime.securesms.groups.v2.GroupManagementRepository
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Manages group state and actions for conversations.
@@ -32,13 +31,14 @@ class ConversationGroupViewModel(
) : ViewModel() {
private val disposables = CompositeDisposable()
private val _groupRecord: Subject<GroupRecord>
private val _groupRecord: BehaviorSubject<GroupRecord>
private val _reviewState: Subject<ConversationGroupReviewState>
private val _groupActiveState: Subject<ConversationGroupActiveState> = BehaviorSubject.create()
private val _memberLevel: BehaviorSubject<ConversationGroupMemberLevel> = BehaviorSubject.create()
private val _actionableRequestingMembersCount: Subject<Int> = BehaviorSubject.create()
private val _gv1MigrationSuggestions: Subject<List<RecipientId>> = BehaviorSubject.create()
val groupRecordSnapshot: GroupRecord?
get() = _groupRecord.value
init {
_groupRecord = recipientRepository
@@ -66,8 +66,6 @@ class ConversationGroupViewModel(
disposables += _groupRecord.subscribe { groupRecord ->
_groupActiveState.onNext(ConversationGroupActiveState(groupRecord.isActive, groupRecord.isV2Group))
_memberLevel.onNext(ConversationGroupMemberLevel(groupRecord.memberLevel(Recipient.self()), groupRecord.isAnnouncementGroup))
_actionableRequestingMembersCount.onNext(getActionableRequestingMembersCount(groupRecord))
_gv1MigrationSuggestions.onNext(getGv1MigrationSuggestions(groupRecord))
}
}
@@ -89,28 +87,6 @@ class ConversationGroupViewModel(
.observeOn(AndroidSchedulers.mainThread())
}
private fun getActionableRequestingMembersCount(groupRecord: GroupRecord): Int {
return if (groupRecord.isV2Group && groupRecord.memberLevel(Recipient.self()) == GroupTable.MemberLevel.ADMINISTRATOR) {
groupRecord.requireV2GroupProperties()
.decryptedGroup
.requestingMembersCount
} else {
0
}
}
private fun getGv1MigrationSuggestions(groupRecord: GroupRecord): List<RecipientId> {
return if (!groupRecord.isActive || !groupRecord.isV2Group || groupRecord.isPendingMember(Recipient.self())) {
emptyList()
} else {
groupRecord.unmigratedV1Members
.filterNot { groupRecord.members.contains(it) }
.map { Recipient.resolved(it) }
.filter { GroupsV1MigrationUtil.isAutoMigratable(it) }
.map { it.id }
}
}
fun cancelJoinRequest(): Single<Result<Unit, GroupChangeFailureReason>> {
return _groupRecord
.firstOrError()
@@ -120,6 +96,16 @@ class ConversationGroupViewModel(
.observeOn(AndroidSchedulers.mainThread())
}
fun onSuggestedMembersBannerDismissed() {
_groupRecord
.firstOrError()
.flatMapCompletable { group ->
groupManagementRepository.removeUnmigratedV1Members(group.id.requireV2())
}
.subscribe()
.addTo(disposables)
}
class Factory(private val threadId: Long, private val recipientRepository: ConversationRecipientRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(ConversationGroupViewModel(threadId, recipientRepository = recipientRepository)) as T