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

@@ -1,13 +1,12 @@
package org.thoughtcrime.securesms.components.reminder
import android.content.Context
import org.thoughtcrime.securesms.R
class BubbleOptOutReminder(context: Context) : Reminder(null, context.getString(R.string.BubbleOptOutTooltip__description)) {
class BubbleOptOutReminder : Reminder(R.string.BubbleOptOutTooltip__description) {
init {
addAction(Action(context.getString(R.string.BubbleOptOutTooltip__turn_off), R.id.reminder_action_turn_off))
addAction(Action(context.getString(R.string.BubbleOptOutTooltip__not_now), R.id.reminder_action_not_now))
addAction(Action(R.string.BubbleOptOutTooltip__turn_off, R.id.reminder_action_bubble_turn_off))
addAction(Action(R.string.BubbleOptOutTooltip__not_now, R.id.reminder_action_bubble_not_now))
}
override fun isDismissable(): Boolean {

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components.reminder
import android.content.Context
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import kotlin.time.Duration.Companion.days
@@ -8,12 +7,12 @@ import kotlin.time.Duration.Companion.days
/**
* Reminder shown when CDS is in a permanent error state, preventing us from doing a sync.
*/
class CdsPermanentErrorReminder(context: Context) : Reminder(null, context.getString(R.string.reminder_cds_permanent_error_body)) {
class CdsPermanentErrorReminder : Reminder(R.string.reminder_cds_permanent_error_body) {
init {
addAction(
Action(
context.getString(R.string.reminder_cds_permanent_error_learn_more),
R.string.reminder_cds_permanent_error_learn_more,
R.id.reminder_action_cds_permanent_error_learn_more
)
)

View File

@@ -1,18 +1,17 @@
package org.thoughtcrime.securesms.components.reminder
import android.content.Context
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* Reminder shown when CDS is rate-limited, preventing us from temporarily doing a refresh.
*/
class CdsTemporyErrorReminder(context: Context) : Reminder(null, context.getString(R.string.reminder_cds_warning_body)) {
class CdsTemporaryErrorReminder : Reminder(R.string.reminder_cds_warning_body) {
init {
addAction(
Action(
context.getString(R.string.reminder_cds_warning_learn_more),
R.string.reminder_cds_warning_learn_more,
R.id.reminder_action_cds_temporary_error_learn_more
)
)

View File

@@ -19,10 +19,9 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
@SuppressLint("BatteryLife")
public class DozeReminder extends Reminder {
@RequiresApi(api = Build.VERSION_CODES.M)
@RequiresApi(api = 23)
public DozeReminder(@NonNull final Context context) {
super(context.getString(R.string.DozeReminder_optimize_for_missing_play_services),
context.getString(R.string.DozeReminder_this_device_does_not_support_play_services_tap_to_disable_system_battery));
super(R.string.DozeReminder_optimize_for_missing_play_services, R.string.DozeReminder_this_device_does_not_support_play_services_tap_to_disable_system_battery);
setOkListener(v -> {
TextSecurePreferences.setPromptedOptimizeDoze(context, true);
@@ -40,5 +39,4 @@ public class DozeReminder extends Reminder {
Build.VERSION.SDK_INT >= 23 &&
!((PowerManager)context.getSystemService(Context.POWER_SERVICE)).isIgnoringBatteryOptimizations(context.getPackageName());
}
}

View File

@@ -8,13 +8,10 @@ import org.thoughtcrime.securesms.util.PlayStoreUtil
/**
* Banner to update app to the latest version because of enclave failure
*/
class EnclaveFailureReminder(context: Context) : Reminder(
null,
context.getString(R.string.EnclaveFailureReminder_update_signal)
) {
class EnclaveFailureReminder(context: Context) : Reminder(R.string.EnclaveFailureReminder_update_signal) {
init {
addAction(Action(context.getString(R.string.ExpiredBuildReminder_update_now), R.id.reminder_action_update_now))
addAction(Action(R.string.ExpiredBuildReminder_update_now, R.id.reminder_action_update_now))
okListener = View.OnClickListener { PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context) }
}

View File

@@ -19,8 +19,8 @@ import java.util.List;
public class ExpiredBuildReminder extends Reminder {
public ExpiredBuildReminder(final Context context) {
super(null, context.getString(R.string.ExpiredBuildReminder_this_version_of_signal_has_expired));
addAction(new Action(context.getString(R.string.ExpiredBuildReminder_update_now), R.id.reminder_action_update_now));
super(R.string.ExpiredBuildReminder_this_version_of_signal_has_expired);
addAction(new Action(R.string.ExpiredBuildReminder_update_now, R.id.reminder_action_update_now));
}
@Override
@@ -28,11 +28,6 @@ public class ExpiredBuildReminder extends Reminder {
return false;
}
@Override
public List<Action> getActions() {
return super.getActions();
}
@Override
public @NonNull Importance getImportance() {
return Importance.TERMINAL;

View File

@@ -5,17 +5,21 @@ import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.recipients.Recipient;
public final class FirstInviteReminder extends Reminder {
public FirstInviteReminder(final @NonNull Context context,
final @NonNull Recipient recipient,
final int percentIncrease) {
super(context.getString(R.string.FirstInviteReminder__title),
context.getString(R.string.FirstInviteReminder__description, percentIncrease));
private final int percentIncrease;
addAction(new Action(context.getString(R.string.InsightsReminder__invite), R.id.reminder_action_invite));
addAction(new Action(context.getString(R.string.InsightsReminder__view_insights), R.id.reminder_action_view_insights));
public FirstInviteReminder(final int percentIncrease) {
super(R.string.FirstInviteReminder__title, NO_RESOURCE);
this.percentIncrease = percentIncrease;
addAction(new Action(R.string.InsightsReminder__invite, R.id.reminder_action_invite));
addAction(new Action(R.string.InsightsReminder__view_insights, R.id.reminder_action_view_insights));
}
@Override
public @NonNull CharSequence getText(@NonNull Context context) {
return context.getString(R.string.FirstInviteReminder__description, percentIncrease);
}
}

View File

@@ -13,14 +13,36 @@ import java.util.List;
* Shows a reminder to add anyone that might have been missed in GV1->GV2 migration.
*/
public class GroupsV1MigrationSuggestionsReminder extends Reminder {
public GroupsV1MigrationSuggestionsReminder(@NonNull Context context, @NonNull List<RecipientId> suggestions) {
super(null, context.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsReminder_members_couldnt_be_added_to_the_new_group, suggestions.size(), suggestions.size()));
addAction(new Action(context.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsReminder_add_members, suggestions.size()), R.id.reminder_action_gv1_suggestion_add_members));
addAction(new Action(context.getResources().getString(R.string.GroupsV1MigrationSuggestionsReminder_no_thanks), R.id.reminder_action_gv1_suggestion_no_thanks));
private final int suggestionsSize;
public GroupsV1MigrationSuggestionsReminder(@NonNull List<RecipientId> suggestions) {
this.suggestionsSize = suggestions.size();
addAction(new AddMembersAction(suggestionsSize));
addAction(new Action(R.string.GroupsV1MigrationSuggestionsReminder_no_thanks, R.id.reminder_action_gv1_suggestion_no_thanks));
}
@Override
public @NonNull CharSequence getText(@NonNull Context context) {
return context.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsReminder_members_couldnt_be_added_to_the_new_group, suggestionsSize, suggestionsSize);
}
@Override
public boolean isDismissable() {
return false;
}
private static class AddMembersAction extends Action {
private final int suggestionsSize;
public AddMembersAction(int suggestionsSize) {
super(NO_RESOURCE, R.id.reminder_action_gv1_suggestion_add_members);
this.suggestionsSize = suggestionsSize;
}
@Override
public CharSequence getTitle(@NonNull Context context) {
return context.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsReminder_add_members, suggestionsSize);
}
}
}

View File

@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -15,13 +17,12 @@ import java.util.concurrent.TimeUnit;
public class OutdatedBuildReminder extends Reminder {
public OutdatedBuildReminder(final Context context) {
super(null, getPluralsText(context));
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
addAction(new Action(context.getString(R.string.OutdatedBuildReminder_update_now), R.id.reminder_action_update_now));
addAction(new Action(R.string.OutdatedBuildReminder_update_now, R.id.reminder_action_update_now));
}
private static CharSequence getPluralsText(final Context context) {
@Override
public @NonNull CharSequence getText(@NonNull Context context) {
int days = getDaysUntilExpiry();
if (days == 0) {

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
@@ -12,19 +11,17 @@ import org.thoughtcrime.securesms.R;
*/
public final class PendingGroupJoinRequestsReminder extends Reminder {
private PendingGroupJoinRequestsReminder(@Nullable CharSequence title,
@NonNull CharSequence text)
{
super(title, text);
private final int count;
public PendingGroupJoinRequestsReminder(int count) {
this.count = count;
addAction(new Action(R.string.PendingGroupJoinRequestsReminder_view, R.id.reminder_action_review_join_requests));
}
public static Reminder create(@NonNull Context context, int count) {
String message = context.getResources().getQuantityString(R.plurals.PendingGroupJoinRequestsReminder_d_pending_member_requests, count, count);
Reminder reminder = new PendingGroupJoinRequestsReminder(null, message);
reminder.addAction(new Action(context.getString(R.string.PendingGroupJoinRequestsReminder_view), R.id.reminder_action_review_join_requests));
return reminder;
@Override
public @NonNull CharSequence getText(@NonNull Context context) {
return context.getResources().getQuantityString(R.plurals.PendingGroupJoinRequestsReminder_d_pending_member_requests, count, count);
}
@Override

View File

@@ -9,8 +9,7 @@ import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
public class PushRegistrationReminder extends Reminder {
public PushRegistrationReminder(final Context context) {
super(context.getString(R.string.reminder_header_push_title),
context.getString(R.string.reminder_header_push_text));
super(R.string.reminder_header_push_title, R.string.reminder_header_push_text);
setOkListener(v -> context.startActivity(RegistrationNavigationActivity.newIntentForReRegistration(context)));
}
@@ -20,7 +19,7 @@ public class PushRegistrationReminder extends Reminder {
return false;
}
public static boolean isEligible(Context context) {
public static boolean isEligible() {
return !SignalStore.account().isRegistered();
}
}

View File

@@ -1,36 +1,57 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import android.view.View.OnClickListener;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import org.whispersystems.signalservice.api.util.Preconditions;
import java.util.LinkedList;
import java.util.List;
public abstract class Reminder {
private CharSequence title;
private CharSequence text;
public static final int NO_RESOURCE = -1;
private int title;
private int text;
private OnClickListener okListener;
private OnClickListener dismissListener;
private final List<Action> actions = new LinkedList<>();
public Reminder(@Nullable CharSequence title,
@NonNull CharSequence text)
{
this.title = title;
this.text = text;
/**
* For a reminder that wishes to generate it's own strings by overwriting
* {@link #getText(Context)} and {@link #getTitle(Context)}
*/
public Reminder() {
this(NO_RESOURCE, NO_RESOURCE);
}
public @Nullable CharSequence getTitle() {
return title;
public Reminder(@StringRes int textRes) {
this(NO_RESOURCE, textRes);
}
public CharSequence getText() {
return text;
public Reminder(@StringRes int titleRes, @StringRes int textRes) {
this.title = titleRes;
this.text = textRes;
}
public @Nullable CharSequence getTitle(@NonNull Context context) {
if (title == NO_RESOURCE) {
return null;
}
return context.getString(title);
}
public @NonNull CharSequence getText(@NonNull Context context) {
Preconditions.checkArgument(text != NO_RESOURCE);
return context.getString(text);
}
public OnClickListener getOkListener() {
@@ -73,17 +94,22 @@ public abstract class Reminder {
NORMAL, ERROR, TERMINAL
}
public static final class Action {
private final CharSequence title;
private final int actionId;
public static class Action {
private final int title;
private final int actionId;
public Action(CharSequence title, @IdRes int actionId) {
public Action(@IdRes int actionId) {
this(NO_RESOURCE, actionId);
}
public Action(@StringRes int title, @IdRes int actionId) {
this.title = title;
this.actionId = actionId;
}
public CharSequence getTitle() {
return title;
public CharSequence getTitle(@NonNull Context context) {
Preconditions.checkArgument(title != NO_RESOURCE);
return context.getText(title);
}
public int getActionId() {

View File

@@ -47,7 +47,7 @@ final class ReminderActionsAdapter extends RecyclerView.Adapter<ReminderActionsA
public void onBindViewHolder(@NonNull ActionViewHolder holder, int position) {
final Reminder.Action action = actions.get(position);
((Button) holder.itemView).setText(action.getTitle());
((Button) holder.itemView).setText(action.getTitle(holder.itemView.getContext()));
holder.itemView.setOnClickListener(v -> {
if (holder.getAdapterPosition() == RecyclerView.NO_POSITION) return;

View File

@@ -38,6 +38,7 @@ public final class ReminderView extends FrameLayout {
private Space space;
private RecyclerView actionsRecycler;
private OnActionClickListener actionClickListener;
private OnHideListener onHideListener;
public ReminderView(Context context) {
super(context);
@@ -67,8 +68,8 @@ public final class ReminderView extends FrameLayout {
}
public void showReminder(final Reminder reminder) {
if (!TextUtils.isEmpty(reminder.getTitle())) {
title.setText(reminder.getTitle());
if (!TextUtils.isEmpty(reminder.getTitle(getContext()))) {
title.setText(reminder.getTitle(getContext()));
title.setVisibility(VISIBLE);
space.setVisibility(GONE);
} else {
@@ -82,7 +83,7 @@ public final class ReminderView extends FrameLayout {
space.setVisibility(GONE);
}
text.setText(reminder.getText());
text.setText(reminder.getText(getContext()));
switch (reminder.getImportance()) {
case NORMAL:
title.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurface));
@@ -153,11 +154,19 @@ public final class ReminderView extends FrameLayout {
this.actionClickListener = actionClickListener;
}
public void setOnHideListener(@Nullable OnHideListener onHideListener) {
this.onHideListener = onHideListener;
}
public void requestDismiss() {
closeButton.performClick();
}
public void hide() {
if (onHideListener != null && onHideListener.onHide()) {
return;
}
container.setVisibility(View.GONE);
}
@@ -168,4 +177,8 @@ public final class ReminderView extends FrameLayout {
public interface OnActionClickListener {
void onActionClick(@IdRes int actionId);
}
public interface OnHideListener {
boolean onHide();
}
}

View File

@@ -9,19 +9,25 @@ import org.thoughtcrime.securesms.recipients.Recipient;
public final class SecondInviteReminder extends Reminder {
private final int progress;
private final Recipient recipient;
private final int progress;
public SecondInviteReminder(final @NonNull Context context,
final @NonNull Recipient recipient,
final int percent)
{
super(context.getString(R.string.SecondInviteReminder__title),
context.getString(R.string.SecondInviteReminder__description, recipient.getDisplayName(context)));
super(R.string.SecondInviteReminder__title, NO_RESOURCE);
this.recipient = recipient;
this.progress = percent;
addAction(new Action(context.getString(R.string.InsightsReminder__invite), R.id.reminder_action_invite));
addAction(new Action(context.getString(R.string.InsightsReminder__view_insights), R.id.reminder_action_view_insights));
addAction(new Action(R.string.InsightsReminder__invite, R.id.reminder_action_invite));
addAction(new Action(R.string.InsightsReminder__view_insights, R.id.reminder_action_view_insights));
}
@Override
public @NonNull CharSequence getText(@NonNull Context context) {
return context.getString(R.string.SecondInviteReminder__description, recipient.getDisplayName(context));
}
@Override

View File

@@ -9,9 +9,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class ServiceOutageReminder extends Reminder {
public ServiceOutageReminder(@NonNull Context context) {
super(null,
context.getString(R.string.reminder_header_service_outage_text));
public ServiceOutageReminder() {
super(R.string.reminder_header_service_outage_text);
}
public static boolean isEligible(@NonNull Context context) {

View File

@@ -11,14 +11,13 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class UnauthorizedReminder extends Reminder {
public UnauthorizedReminder(final Context context) {
super(null,
context.getString(R.string.UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device));
super(R.string.UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device);
setOkListener(v -> {
context.startActivity(RegistrationNavigationActivity.newIntentForReRegistration(context));
});
addAction(new Action(context.getString(R.string.UnauthorizedReminder_reregister_action), R.id.reminder_action_re_register));
addAction(new Action(R.string.UnauthorizedReminder_reregister_action, R.id.reminder_action_re_register));
}
@Override

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components.reminder
import android.content.Context
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.FeatureFlags
@@ -9,15 +8,12 @@ import org.thoughtcrime.securesms.util.FeatureFlags
* Displays a reminder message when the local username gets out of sync with
* what the server thinks our username is.
*/
class UsernameOutOfSyncReminder(context: Context) : Reminder(
null,
context.getString(R.string.UsernameOutOfSyncReminder__something_went_wrong)
) {
class UsernameOutOfSyncReminder : Reminder(R.string.UsernameOutOfSyncReminder__something_went_wrong) {
init {
addAction(
Action(
context.getString(R.string.UsernameOutOfSyncReminder__fix_now),
R.string.UsernameOutOfSyncReminder__fix_now,
R.id.reminder_action_fix_username
)
)

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

View File

@@ -100,7 +100,7 @@ import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
import org.thoughtcrime.securesms.components.reminder.CdsPermanentErrorReminder;
import org.thoughtcrime.securesms.components.reminder.CdsTemporyErrorReminder;
import org.thoughtcrime.securesms.components.reminder.CdsTemporaryErrorReminder;
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
import org.thoughtcrime.securesms.components.reminder.OutdatedBuildReminder;
@@ -1051,19 +1051,19 @@ public class ConversationListFragment extends MainFragment implements ActionMode
return Optional.of(new UnauthorizedReminder(context));
} else if (ServiceOutageReminder.isEligible(context)) {
ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob());
return Optional.of(new ServiceOutageReminder(context));
return Optional.of(new ServiceOutageReminder());
} else if (OutdatedBuildReminder.isEligible()) {
return Optional.of(new OutdatedBuildReminder(context));
} else if (PushRegistrationReminder.isEligible(context)) {
} else if (PushRegistrationReminder.isEligible()) {
return Optional.of((new PushRegistrationReminder(context)));
} else if (DozeReminder.isEligible(context)) {
return Optional.of(new DozeReminder(context));
} else if (CdsTemporyErrorReminder.isEligible()) {
return Optional.of(new CdsTemporyErrorReminder(context));
} else if (CdsTemporaryErrorReminder.isEligible()) {
return Optional.of(new CdsTemporaryErrorReminder());
} else if (CdsPermanentErrorReminder.isEligible()) {
return Optional.of(new CdsPermanentErrorReminder(context));
return Optional.of(new CdsPermanentErrorReminder());
} else if (UsernameOutOfSyncReminder.isEligible()) {
return Optional.of(new UsernameOutOfSyncReminder(context));
return Optional.of(new UsernameOutOfSyncReminder());
} else {
return Optional.<Reminder>empty();
}

View File

@@ -7,6 +7,7 @@ import org.signal.storageservice.protos.groups.local.EnabledState
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.groups.GroupAccessControl
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -118,6 +119,28 @@ class GroupRecord(
}
}
val actionableRequestingMembersCount: Int by lazy {
if (isV2Group && memberLevel(Recipient.self()) == GroupTable.MemberLevel.ADMINISTRATOR) {
requireV2GroupProperties()
.decryptedGroup
.requestingMembersCount
} else {
0
}
}
val gv1MigrationSuggestions: List<RecipientId> by lazy {
if (!isActive || !isV2Group || isPendingMember(Recipient.self())) {
emptyList()
} else {
unmigratedV1Members
.filterNot { members.contains(it) }
.map { Recipient.resolved(it) }
.filter { GroupsV1MigrationUtil.isAutoMigratable(it) }
.map { it.id }
}
}
fun hasAvatar(): Boolean {
return avatarId != 0L
}

View File

@@ -2,12 +2,14 @@ package org.thoughtcrime.securesms.groups.v2
import android.content.Context
import androidx.core.util.Consumer
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.Result
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupChangeBusyException
import org.thoughtcrime.securesms.groups.GroupChangeException
@@ -98,4 +100,10 @@ class GroupManagementRepository @JvmOverloads constructor(private val context: C
}
}.subscribeOn(Schedulers.io())
}
fun removeUnmigratedV1Members(groupId: GroupId.V2): Completable {
return Completable.fromCallable {
SignalDatabase.groups.removeUnmigratedV1Members(groupId)
}.subscribeOn(Schedulers.io())
}
}

View File

@@ -57,7 +57,7 @@ public final class InviteReminderModel {
if (conversationCount >= SECOND_INVITE_REMINDER_MESSAGE_THRESHOLD && !resolved.hasSeenSecondInviteReminder()) {
return new SecondInviteReminderInfo(context, resolved, repository, repository.getPercentOfInsecureMessages(conversationCount));
} else if (conversationCount >= FIRST_INVITE_REMINDER_MESSAGE_THRESHOLD && !resolved.hasSeenFirstInviteReminder()) {
return new FirstInviteReminderInfo(context, resolved, repository, repository.getPercentOfInsecureMessages(conversationCount));
return new FirstInviteReminderInfo(resolved, repository, repository.getPercentOfInsecureMessages(conversationCount));
}
}
return new NoReminderInfo();
@@ -108,8 +108,8 @@ public final class InviteReminderModel {
private final Repository repository;
private final Recipient recipient;
private FirstInviteReminderInfo(@NonNull Context context, @NonNull Recipient recipient, @NonNull Repository repository, int percentInsecure) {
super(new FirstInviteReminder(context, recipient, percentInsecure));
private FirstInviteReminderInfo(@NonNull Recipient recipient, @NonNull Repository repository, int percentInsecure) {
super(new FirstInviteReminder(percentInsecure));
this.recipient = recipient;
this.repository = repository;