From 6b91e525dbef09113880010c5510efb805d06898 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 24 May 2023 22:47:05 -0400 Subject: [PATCH] Add Reminders and Conversation Banner to CFv2. --- .../reminder/BubbleOptOutReminder.kt | 7 +- .../reminder/CdsPermanentErrorReminder.kt | 5 +- ...minder.kt => CdsTemporaryErrorReminder.kt} | 5 +- .../components/reminder/DozeReminder.java | 6 +- .../reminder/EnclaveFailureReminder.kt | 7 +- .../reminder/ExpiredBuildReminder.java | 9 +- .../reminder/FirstInviteReminder.java | 20 +-- .../GroupsV1MigrationSuggestionsReminder.java | 30 +++- .../reminder/OutdatedBuildReminder.java | 9 +- .../PendingGroupJoinRequestsReminder.java | 21 ++- .../reminder/PushRegistrationReminder.java | 5 +- .../components/reminder/Reminder.java | 60 +++++--- .../reminder/ReminderActionsAdapter.java | 2 +- .../components/reminder/ReminderView.java | 19 ++- .../reminder/SecondInviteReminder.java | 16 ++- .../reminder/ServiceOutageReminder.java | 5 +- .../reminder/UnauthorizedReminder.java | 5 +- .../reminder/UsernameOutOfSyncReminder.kt | 8 +- .../conversation/ConversationFragment.java | 12 +- ...rView.java => ConversationHeaderView.java} | 8 +- .../ConversationParentFragment.java | 14 +- .../conversation/v2/ConversationAdapterV2.kt | 4 +- .../conversation/v2/ConversationBannerView.kt | 128 ++++++++++++++++++ .../conversation/v2/ConversationFragment.kt | 79 ++++++++++- .../conversation/v2/ConversationRepository.kt | 44 +++++- .../conversation/v2/ConversationViewModel.kt | 36 +++-- .../conversation/v2/DisabledInputView.kt | 6 +- .../v2/groups/ConversationGroupViewModel.kt | 44 ++---- .../ConversationListFragment.java | 14 +- .../securesms/database/model/GroupRecord.kt | 23 ++++ .../groups/v2/GroupManagementRepository.kt | 8 ++ .../invites/InviteReminderModel.java | 6 +- .../res/layout/conversation_item_banner.xml | 2 +- .../res/layout/v2_conversation_fragment.xml | 12 +- app/src/main/res/values/ids.xml | 4 +- 35 files changed, 501 insertions(+), 182 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/components/reminder/{CdsTemporyErrorReminder.kt => CdsTemporaryErrorReminder.kt} (78%) rename app/src/main/java/org/thoughtcrime/securesms/conversation/{ConversationBannerView.java => ConversationHeaderView.java} (95%) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationBannerView.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/BubbleOptOutReminder.kt b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/BubbleOptOutReminder.kt index 75492a2e00..e377474c1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/BubbleOptOutReminder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/BubbleOptOutReminder.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/CdsPermanentErrorReminder.kt b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/CdsPermanentErrorReminder.kt index d8b6d206d1..90fbb69532 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/CdsPermanentErrorReminder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/CdsPermanentErrorReminder.kt @@ -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 ) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/CdsTemporyErrorReminder.kt b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/CdsTemporaryErrorReminder.kt similarity index 78% rename from app/src/main/java/org/thoughtcrime/securesms/components/reminder/CdsTemporyErrorReminder.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/reminder/CdsTemporaryErrorReminder.kt index 06854f94f1..98c61c7c2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/CdsTemporyErrorReminder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/CdsTemporaryErrorReminder.kt @@ -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 ) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/DozeReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/DozeReminder.java index 05f274ea7c..48b192adca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/DozeReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/DozeReminder.java @@ -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()); } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/EnclaveFailureReminder.kt b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/EnclaveFailureReminder.kt index 96a70e1d53..8244b7a62a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/EnclaveFailureReminder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/EnclaveFailureReminder.kt @@ -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) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java index 612aafb835..b34f17e124 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java @@ -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 getActions() { - return super.getActions(); - } - @Override public @NonNull Importance getImportance() { return Importance.TERMINAL; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/FirstInviteReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/FirstInviteReminder.java index b8709c9e83..5c1e6859d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/FirstInviteReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/FirstInviteReminder.java @@ -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); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/GroupsV1MigrationSuggestionsReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/GroupsV1MigrationSuggestionsReminder.java index cfad166bd7..58582deb1a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/GroupsV1MigrationSuggestionsReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/GroupsV1MigrationSuggestionsReminder.java @@ -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 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 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); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java index 5a68662d8c..eef2b70eb2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PendingGroupJoinRequestsReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PendingGroupJoinRequestsReminder.java index f707009e28..ce85802b42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PendingGroupJoinRequestsReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PendingGroupJoinRequestsReminder.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PushRegistrationReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PushRegistrationReminder.java index 2adc8b37a6..aecfd82762 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PushRegistrationReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/PushRegistrationReminder.java @@ -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(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java index 0250c5f117..83dde25e8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java @@ -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 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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderActionsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderActionsAdapter.java index cb357a566e..cb2a0baf50 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderActionsAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderActionsAdapter.java @@ -47,7 +47,7 @@ final class ReminderActionsAdapter extends RecyclerView.Adapter { if (holder.getAdapterPosition() == RecyclerView.NO_POSITION) return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java index 1ff0125f7c..88e31dcf20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java @@ -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(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/SecondInviteReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/SecondInviteReminder.java index 0aede6213f..fb6a7af35b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/SecondInviteReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/SecondInviteReminder.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ServiceOutageReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ServiceOutageReminder.java index 4d98f7ebbf..6accba6108 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ServiceOutageReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ServiceOutageReminder.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UnauthorizedReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UnauthorizedReminder.java index 6a82dbd852..ff46370a90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UnauthorizedReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UnauthorizedReminder.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UsernameOutOfSyncReminder.kt b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UsernameOutOfSyncReminder.kt index fa7b36825c..40b40f5044 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UsernameOutOfSyncReminder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UsernameOutOfSyncReminder.kt @@ -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 ) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 0825b35c24..55e38367ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -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); } }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java rename to app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java index be13bafacf..328be627b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java index 45d5bbdd18..3b8a936c70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -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 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()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt index 0cf35137d3..7119187284 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt @@ -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(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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationBannerView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationBannerView.kt new file mode 100644 index 0000000000..ca6141c314 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationBannerView.kt @@ -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 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) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index d1daf56527..f30e4899d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -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(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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt index 8e681bf47c..e27e37d453 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt @@ -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> { + 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 4d5029f0bf..2415ab51e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -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 = BehaviorSubject.create() - val recipient: Observable = _recipient + val recipient: Observable = recipientRepository.conversationRecipient private val _conversationThreadState: Subject = BehaviorSubject.create() val conversationThreadState: Single = _conversationThreadState.firstOrError() @@ -76,13 +79,14 @@ class ConversationViewModel( val pagingController = ProxyPagingController() - val nameColorsMap: Observable> = _recipient.flatMap { repository.getNameColorsMap(it, groupAuthorNameColorHelper) } + val nameColorsMap: Observable> = 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 @@ -90,12 +94,15 @@ class ConversationViewModel( val hasMessageRequestState: Boolean get() = hasMessageRequestStateSubject.value ?: false + private val refreshReminder: Subject = PublishSubject.create() + + val reminder: Observable> + 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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt index f3dc3989b2..8d51906535 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt @@ -153,15 +153,15 @@ class DisabledInputView @JvmOverloads constructor( announcementGroupOnly = null } - private fun show(existingView: VIEW?, create: () -> VIEW, bind: VIEW.() -> Unit = {}): VIEW { + private fun 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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/groups/ConversationGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/groups/ConversationGroupViewModel.kt index 8cd63524a3..4682dbe272 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/groups/ConversationGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/groups/ConversationGroupViewModel.kt @@ -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 + private val _groupRecord: BehaviorSubject private val _reviewState: Subject private val _groupActiveState: Subject = BehaviorSubject.create() private val _memberLevel: BehaviorSubject = BehaviorSubject.create() - private val _actionableRequestingMembersCount: Subject = BehaviorSubject.create() - private val _gv1MigrationSuggestions: Subject> = 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 { - 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> { 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 create(modelClass: Class): T { return modelClass.cast(ConversationGroupViewModel(threadId, recipientRepository = recipientRepository)) as T diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 69221f7ff9..3e423a4f8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -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.empty(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt index 5efdd9dcc5..226bf439f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt @@ -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 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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupManagementRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupManagementRepository.kt index 5584ce03ca..44f09a9807 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupManagementRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupManagementRepository.kt @@ -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()) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/invites/InviteReminderModel.java b/app/src/main/java/org/thoughtcrime/securesms/invites/InviteReminderModel.java index 01c72c46a2..d78759cd0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/invites/InviteReminderModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/invites/InviteReminderModel.java @@ -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; diff --git a/app/src/main/res/layout/conversation_item_banner.xml b/app/src/main/res/layout/conversation_item_banner.xml index a6eb1d7cf4..0ef3b5fe52 100644 --- a/app/src/main/res/layout/conversation_item_banner.xml +++ b/app/src/main/res/layout/conversation_item_banner.xml @@ -1,5 +1,5 @@ - - - + - - + +