mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Add Reminders and Conversation Banner to CFv2.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user