mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
Add Reminders and Conversation Banner to CFv2.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -240,7 +240,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
private ConversationScrollToView scrollToBottomButton;
|
||||
private ConversationScrollToView scrollToMentionButton;
|
||||
private TextView scrollDateHeader;
|
||||
private ConversationBannerView conversationBanner;
|
||||
private ConversationHeaderView conversationHeader;
|
||||
private MessageRequestViewModel messageRequestViewModel;
|
||||
private MessageCountsViewModel messageCountsViewModel;
|
||||
private ConversationViewModel conversationViewModel;
|
||||
@@ -350,7 +350,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
getViewLifecycleOwner().getLifecycle().addObserver(multiselectItemDecoration);
|
||||
|
||||
snapToTopDataObserver = new ConversationSnapToTopDataObserver(list, new ConversationScrollRequestValidator());
|
||||
conversationBanner = (ConversationBannerView) inflater.inflate(R.layout.conversation_item_banner, container, false);
|
||||
conversationHeader = (ConversationHeaderView) inflater.inflate(R.layout.conversation_item_banner, container, false);
|
||||
topLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
|
||||
bottomLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
|
||||
|
||||
@@ -586,7 +586,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
listener.onMessageRequest(messageRequestViewModel);
|
||||
|
||||
messageRequestViewModel.getRecipientInfo().observe(getViewLifecycleOwner(), recipientInfo -> {
|
||||
presentMessageRequestProfileView(requireContext(), recipientInfo, conversationBanner);
|
||||
presentMessageRequestProfileView(requireContext(), recipientInfo, conversationHeader);
|
||||
});
|
||||
|
||||
messageRequestViewModel.getMessageData().observe(getViewLifecycleOwner(), data -> {
|
||||
@@ -597,7 +597,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
});
|
||||
}
|
||||
|
||||
private void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationBannerView conversationBanner) {
|
||||
private void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationHeaderView conversationBanner) {
|
||||
if (conversationBanner == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1223,7 +1223,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
return;
|
||||
}
|
||||
|
||||
adapter.setFooterView(conversationBanner);
|
||||
adapter.setFooterView(conversationHeader);
|
||||
|
||||
Runnable afterScroll = () -> {
|
||||
if (!conversation.getMessageRequestData().isMessageRequestAccepted()) {
|
||||
@@ -1384,7 +1384,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
Rect rect = new Rect();
|
||||
toolbar.getGlobalVisibleRect(rect);
|
||||
conversationViewModel.setToolbarBottom(rect.bottom);
|
||||
ViewUtil.setTopMargin(conversationBanner, rect.bottom + ViewUtil.dpToPx(16));
|
||||
ViewUtil.setTopMargin(conversationHeader, rect.bottom + ViewUtil.dpToPx(16));
|
||||
toolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
|
||||
public class ConversationBannerView extends ConstraintLayout {
|
||||
public class ConversationHeaderView extends ConstraintLayout {
|
||||
|
||||
private AvatarImageView contactAvatar;
|
||||
private TextView contactTitle;
|
||||
@@ -35,15 +35,15 @@ public class ConversationBannerView extends ConstraintLayout {
|
||||
private View tapToView;
|
||||
private BadgeImageView contactBadge;
|
||||
|
||||
public ConversationBannerView(Context context) {
|
||||
public ConversationHeaderView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public ConversationBannerView(Context context, AttributeSet attrs) {
|
||||
public ConversationHeaderView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ConversationBannerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
public ConversationHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
inflate(getContext(), R.layout.conversation_banner_view, this);
|
||||
@@ -1736,7 +1736,7 @@ public class ConversationParentFragment extends Fragment
|
||||
reminderView.get().setOnActionClickListener(this::handleReminderAction);
|
||||
} else if (ServiceOutageReminder.isEligible(context)) {
|
||||
ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob());
|
||||
reminderView.get().showReminder(new ServiceOutageReminder(context));
|
||||
reminderView.get().showReminder(new ServiceOutageReminder());
|
||||
} else if (SignalStore.account().isRegistered() &&
|
||||
TextSecurePreferences.isShowInviteReminders(context) &&
|
||||
!viewModel.isPushAvailable() &&
|
||||
@@ -1746,14 +1746,14 @@ public class ConversationParentFragment extends Fragment
|
||||
reminderView.get().setOnDismissListener(() -> inviteReminderModel.dismissReminder());
|
||||
reminderView.get().showReminder(inviteReminder.get());
|
||||
} else if (actionableRequestingMembers != null && actionableRequestingMembers > 0) {
|
||||
reminderView.get().showReminder(PendingGroupJoinRequestsReminder.create(context, actionableRequestingMembers));
|
||||
reminderView.get().showReminder(new PendingGroupJoinRequestsReminder(actionableRequestingMembers));
|
||||
reminderView.get().setOnActionClickListener(id -> {
|
||||
if (id == R.id.reminder_action_review_join_requests) {
|
||||
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(context, getRecipient().getGroupId().get().requireV2()));
|
||||
}
|
||||
});
|
||||
} else if (gv1MigrationSuggestions != null && gv1MigrationSuggestions.size() > 0 && recipient.get().isPushV2Group()) {
|
||||
reminderView.get().showReminder(new GroupsV1MigrationSuggestionsReminder(context, gv1MigrationSuggestions));
|
||||
reminderView.get().showReminder(new GroupsV1MigrationSuggestionsReminder(gv1MigrationSuggestions));
|
||||
reminderView.get().setOnActionClickListener(actionId -> {
|
||||
if (actionId == R.id.reminder_action_gv1_suggestion_add_members) {
|
||||
GroupsV1MigrationSuggestionsDialog.show(requireActivity(), recipient.get().requireGroupId().requireV2(), gv1MigrationSuggestions);
|
||||
@@ -1764,12 +1764,12 @@ public class ConversationParentFragment extends Fragment
|
||||
reminderView.get().setOnDismissListener(() -> {
|
||||
});
|
||||
} else if (isInBubble() && !SignalStore.tooltips().hasSeenBubbleOptOutTooltip() && Build.VERSION.SDK_INT > 29) {
|
||||
reminderView.get().showReminder(new BubbleOptOutReminder(context));
|
||||
reminderView.get().showReminder(new BubbleOptOutReminder());
|
||||
reminderView.get().setOnActionClickListener(actionId -> {
|
||||
SignalStore.tooltips().markBubbleOptOutTooltipSeen();
|
||||
reminderView.get().hide();
|
||||
|
||||
if (actionId == R.id.reminder_action_turn_off) {
|
||||
if (actionId == R.id.reminder_action_bubble_turn_off) {
|
||||
Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().getPackageName())
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
@@ -3162,14 +3162,14 @@ public class ConversationParentFragment extends Fragment
|
||||
Reminder reminder = new ExpiredBuildReminder(requireContext());
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(reminder.getText())
|
||||
.setMessage(reminder.getText(requireContext()))
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss());
|
||||
|
||||
List<Reminder.Action> actions = reminder.getActions();
|
||||
if (actions.size() == 1) {
|
||||
Reminder.Action action = actions.get(0);
|
||||
|
||||
builder.setNeutralButton(action.getTitle(), (d, i) -> {
|
||||
builder.setNeutralButton(action.getTitle(requireContext()), (d, i) -> {
|
||||
if (action.getActionId() == R.id.reminder_action_update_now) {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.BindableConversationItem
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
|
||||
import org.thoughtcrime.securesms.conversation.ConversationBannerView
|
||||
import org.thoughtcrime.securesms.conversation.ConversationHeaderView
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable
|
||||
@@ -366,7 +366,7 @@ class ConversationAdapterV2(
|
||||
}
|
||||
|
||||
inner class ThreadHeaderViewHolder(itemView: View) : MappingViewHolder<ThreadHeader>(itemView) {
|
||||
private val conversationBanner: ConversationBannerView = itemView as ConversationBannerView
|
||||
private val conversationBanner: ConversationHeaderView = itemView as ConversationHeaderView
|
||||
|
||||
override fun bind(model: ThreadHeader) {
|
||||
val (recipient, groupInfo, sharedGroups, messageRequestState) = model.recipientInfo
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.transition.Slide
|
||||
import android.transition.TransitionManager
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.transition.addListener
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder
|
||||
import org.thoughtcrime.securesms.components.reminder.ReminderView
|
||||
|
||||
/**
|
||||
* Responsible for showing the various "banner" views at the top of a conversation
|
||||
*
|
||||
* - Expired Build
|
||||
* - Unregistered
|
||||
* - Group join requests
|
||||
* - GroupV1 suggestions
|
||||
* - Disable Chat Bubbles setting
|
||||
* - Service outage
|
||||
*/
|
||||
class ConversationBannerView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : LinearLayoutCompat(context, attrs, defStyleAttr) {
|
||||
|
||||
private val inflater: LayoutInflater by lazy { LayoutInflater.from(context) }
|
||||
|
||||
private var reminderView: ReminderView? = null
|
||||
private var currentView: View? = null
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
}
|
||||
|
||||
fun showAsReminder(reminder: Reminder) {
|
||||
reminderView = show(
|
||||
existingView = reminderView,
|
||||
create = { ReminderView(context) },
|
||||
bind = {
|
||||
showReminder(reminder)
|
||||
setOnActionClickListener {
|
||||
when (it) {
|
||||
R.id.reminder_action_update_now -> listener?.updateAppAction()
|
||||
R.id.reminder_action_re_register -> listener?.reRegisterAction()
|
||||
R.id.reminder_action_review_join_requests -> listener?.reviewJoinRequestsAction()
|
||||
R.id.reminder_action_gv1_suggestion_no_thanks -> listener?.gv1SuggestionsAction(it)
|
||||
R.id.reminder_action_bubble_not_now, R.id.reminder_action_bubble_turn_off -> {
|
||||
listener?.changeBubbleSettingAction(disableSetting = it == R.id.reminder_action_bubble_turn_off)
|
||||
}
|
||||
}
|
||||
}
|
||||
setOnHideListener {
|
||||
removeIfNotNull(reminderView)
|
||||
reminderView = null
|
||||
true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
removeAllViews()
|
||||
reminderView = null
|
||||
currentView = null
|
||||
}
|
||||
|
||||
private fun <V : View> show(existingView: V?, create: () -> V, bind: V.() -> Unit = {}): V {
|
||||
if (existingView != currentView) {
|
||||
removeIfNotNull(currentView)
|
||||
}
|
||||
|
||||
val view: V = if (existingView != null) {
|
||||
existingView
|
||||
} else {
|
||||
val newView: V = create()
|
||||
|
||||
TransitionManager.beginDelayedTransition(this, Slide(Gravity.TOP))
|
||||
addView(newView, defaultLayoutParams())
|
||||
newView
|
||||
}
|
||||
|
||||
view.bind()
|
||||
|
||||
currentView = view
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun defaultLayoutParams(): LayoutParams {
|
||||
return LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
private fun removeIfNotNull(view: View?) {
|
||||
if (view != null) {
|
||||
val transition = Slide(Gravity.TOP).apply {
|
||||
addListener(
|
||||
onEnd = {
|
||||
layoutParams = layoutParams.apply { height = LayoutParams.WRAP_CONTENT }
|
||||
}
|
||||
)
|
||||
}
|
||||
layoutParams = layoutParams.apply { height = this@ConversationBannerView.height }
|
||||
TransitionManager.beginDelayedTransition(this, transition)
|
||||
removeView(view)
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun updateAppAction()
|
||||
fun reRegisterAction()
|
||||
fun reviewJoinRequestsAction()
|
||||
fun gv1SuggestionsAction(actionId: Int)
|
||||
fun changeBubbleSettingAction(disableSetting: Boolean)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.KeyEvent
|
||||
@@ -141,10 +142,12 @@ import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
|
||||
import org.thoughtcrime.securesms.invites.InviteActions
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -225,11 +228,11 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
private val binding by ViewBinderDelegate(V2ConversationFragmentBinding::bind)
|
||||
private val viewModel: ConversationViewModel by viewModel {
|
||||
ConversationViewModel(
|
||||
args.threadId,
|
||||
args.startingPosition,
|
||||
ConversationRepository(requireContext()),
|
||||
conversationRecipientRepository,
|
||||
messageRequestRepository
|
||||
threadId = args.threadId,
|
||||
requestedStartingPosition = args.startingPosition,
|
||||
repository = ConversationRepository(context = requireContext(), isInBubble = args.conversationScreenType == ConversationScreenType.BUBBLE),
|
||||
recipientRepository = conversationRecipientRepository,
|
||||
messageRequestRepository = messageRequestRepository
|
||||
)
|
||||
}
|
||||
|
||||
@@ -321,12 +324,14 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
conversationToolbarOnScrollHelper.attach(binding.conversationItemRecycler)
|
||||
presentWallpaper(args.wallpaper)
|
||||
presentChatColors(args.chatColors)
|
||||
presentConversationTitle(viewModel.recipientSnapshot)
|
||||
presentActionBarMenu()
|
||||
|
||||
observeConversationThread()
|
||||
|
||||
viewModel
|
||||
.inputReadyState
|
||||
.distinctUntilChanged()
|
||||
.subscribeBy(
|
||||
onNext = this::presentInputReadyState
|
||||
)
|
||||
@@ -400,6 +405,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
|
||||
disposables += viewModel.recipient
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.distinctUntilChanged { r1, r2 -> r1 === r2 || r1.hasSameContent(r2) }
|
||||
.subscribeBy(onNext = this::onRecipientChanged)
|
||||
|
||||
disposables += viewModel.markReadRequests
|
||||
@@ -473,9 +479,23 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
val conversationReactionStub = Stub<ConversationReactionOverlay>(binding.conversationReactionScrubberStub)
|
||||
reactionDelegate = ConversationReactionDelegate(conversationReactionStub)
|
||||
reactionDelegate.setOnReactionSelectedListener(OnReactionsSelectedListener())
|
||||
|
||||
binding.conversationBanner.listener = ConversationBannerListener()
|
||||
viewModel
|
||||
.reminder
|
||||
.subscribeBy { reminder ->
|
||||
if (reminder.isPresent) {
|
||||
binding.conversationBanner.showAsReminder(reminder.get())
|
||||
} else {
|
||||
binding.conversationBanner.clear()
|
||||
}
|
||||
}
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
private fun presentInputReadyState(inputReadyState: InputReadyState) {
|
||||
presentConversationTitle(inputReadyState.conversationRecipient)
|
||||
|
||||
val disabledInputView = binding.conversationDisabledInput
|
||||
|
||||
var inputDisabled = true
|
||||
@@ -564,7 +584,11 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentConversationTitle(recipient: Recipient) {
|
||||
private fun presentConversationTitle(recipient: Recipient?) {
|
||||
if (recipient == null) {
|
||||
return
|
||||
}
|
||||
|
||||
binding.conversationTitleView.root.setTitle(GlideApp.with(this), recipient)
|
||||
}
|
||||
|
||||
@@ -1778,6 +1802,49 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
}
|
||||
|
||||
//region Conversation Banner Callbacks
|
||||
|
||||
private inner class ConversationBannerListener : ConversationBannerView.Listener {
|
||||
override fun updateAppAction() {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
|
||||
}
|
||||
|
||||
override fun reRegisterAction() {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
|
||||
}
|
||||
|
||||
override fun reviewJoinRequestsAction() {
|
||||
viewModel.recipientSnapshot?.let { recipient ->
|
||||
val intent = ManagePendingAndRequestingMembersActivity.newIntent(requireContext(), recipient.requireGroupId().requireV2())
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun gv1SuggestionsAction(actionId: Int) {
|
||||
if (actionId == R.id.reminder_action_gv1_suggestion_add_members) {
|
||||
conversationGroupViewModel.groupRecordSnapshot?.let { groupRecord ->
|
||||
GroupsV1MigrationSuggestionsDialog.show(requireActivity(), groupRecord.id.requireV2(), groupRecord.gv1MigrationSuggestions)
|
||||
}
|
||||
} else if (actionId == R.id.reminder_action_gv1_suggestion_no_thanks) {
|
||||
conversationGroupViewModel.onSuggestedMembersBannerDismissed()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun changeBubbleSettingAction(disableSetting: Boolean) {
|
||||
SignalStore.tooltips().markBubbleOptOutTooltipSeen()
|
||||
|
||||
if (disableSetting) {
|
||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region Disabled Input Callbacks
|
||||
|
||||
private inner class DisabledInputListener : DisabledInputView.Listener {
|
||||
|
||||
@@ -6,25 +6,38 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.toOptional
|
||||
import org.signal.libsignal.protocol.InvalidMessageException
|
||||
import org.signal.paging.PagedData
|
||||
import org.signal.paging.PagingConfig
|
||||
import org.thoughtcrime.securesms.components.reminder.BubbleOptOutReminder
|
||||
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder
|
||||
import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationSuggestionsReminder
|
||||
import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsReminder
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder
|
||||
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder
|
||||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder
|
||||
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
|
||||
import org.thoughtcrime.securesms.conversation.colors.NameColor
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationDataSource
|
||||
import org.thoughtcrime.securesms.database.RxDatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.Quote
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
@@ -33,10 +46,14 @@ import org.thoughtcrime.securesms.recipients.RecipientFormattingException
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||
import java.util.Optional
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class ConversationRepository(context: Context) {
|
||||
class ConversationRepository(
|
||||
context: Context,
|
||||
private val isInBubble: Boolean
|
||||
) {
|
||||
|
||||
private val applicationContext = context.applicationContext
|
||||
private val oldConversationRepository = org.thoughtcrime.securesms.conversation.ConversationRepository()
|
||||
@@ -184,6 +201,31 @@ class ConversationRepository(context: Context) {
|
||||
return SignalDatabase.messages.getUnreadMentionCount(threadId)
|
||||
}
|
||||
|
||||
fun getReminder(groupRecord: GroupRecord?): Maybe<Optional<Reminder>> {
|
||||
return Maybe.fromCallable {
|
||||
val reminder: Reminder? = when {
|
||||
ExpiredBuildReminder.isEligible() -> ExpiredBuildReminder(applicationContext)
|
||||
UnauthorizedReminder.isEligible(applicationContext) -> UnauthorizedReminder(applicationContext)
|
||||
ServiceOutageReminder.isEligible(applicationContext) -> {
|
||||
ApplicationDependencies.getJobManager().add(ServiceOutageDetectionJob())
|
||||
ServiceOutageReminder()
|
||||
}
|
||||
groupRecord != null && groupRecord.actionableRequestingMembersCount > 0 -> {
|
||||
PendingGroupJoinRequestsReminder(groupRecord.actionableRequestingMembersCount)
|
||||
}
|
||||
groupRecord != null && groupRecord.gv1MigrationSuggestions.isNotEmpty() -> {
|
||||
GroupsV1MigrationSuggestionsReminder(groupRecord.gv1MigrationSuggestions)
|
||||
}
|
||||
isInBubble && !SignalStore.tooltips().hasSeenBubbleOptOutTooltip() && Build.VERSION.SDK_INT > 29 -> {
|
||||
BubbleOptOutReminder()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
reminder.toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
data class MessageCounts(
|
||||
val unread: Int,
|
||||
val mentions: Int
|
||||
|
||||
@@ -17,9 +17,12 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.signal.core.util.concurrent.subscribeWithSubject
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.paging.ProxyPagingController
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder
|
||||
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
|
||||
import org.thoughtcrime.securesms.conversation.colors.NameColor
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
@@ -41,6 +44,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.hasGiftBadge
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* ConversationViewModel, which operates solely off of a thread id that never changes.
|
||||
@@ -63,8 +67,7 @@ class ConversationViewModel(
|
||||
val showScrollButtonsSnapshot: Boolean
|
||||
get() = scrollButtonStateStore.state.showScrollButtons
|
||||
|
||||
private val _recipient: BehaviorSubject<Recipient> = BehaviorSubject.create()
|
||||
val recipient: Observable<Recipient> = _recipient
|
||||
val recipient: Observable<Recipient> = recipientRepository.conversationRecipient
|
||||
|
||||
private val _conversationThreadState: Subject<ConversationThreadState> = BehaviorSubject.create()
|
||||
val conversationThreadState: Single<ConversationThreadState> = _conversationThreadState.firstOrError()
|
||||
@@ -76,13 +79,14 @@ class ConversationViewModel(
|
||||
|
||||
val pagingController = ProxyPagingController<ConversationElementKey>()
|
||||
|
||||
val nameColorsMap: Observable<Map<RecipientId, NameColor>> = _recipient.flatMap { repository.getNameColorsMap(it, groupAuthorNameColorHelper) }
|
||||
val nameColorsMap: Observable<Map<RecipientId, NameColor>> = recipient.flatMap { repository.getNameColorsMap(it, groupAuthorNameColorHelper) }
|
||||
|
||||
val recipientSnapshot: Recipient?
|
||||
get() = _recipient.value
|
||||
@Volatile
|
||||
var recipientSnapshot: Recipient? = null
|
||||
private set
|
||||
|
||||
val wallpaperSnapshot: ChatWallpaper?
|
||||
get() = _recipient.value?.wallpaper
|
||||
get() = recipientSnapshot?.wallpaper
|
||||
|
||||
val inputReadyState: Observable<InputReadyState>
|
||||
|
||||
@@ -90,12 +94,15 @@ class ConversationViewModel(
|
||||
val hasMessageRequestState: Boolean
|
||||
get() = hasMessageRequestStateSubject.value ?: false
|
||||
|
||||
private val refreshReminder: Subject<Unit> = PublishSubject.create()
|
||||
|
||||
val reminder: Observable<Optional<Reminder>>
|
||||
|
||||
init {
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
.subscribeBy(onNext = {
|
||||
_recipient.onNext(it)
|
||||
})
|
||||
disposables += recipient
|
||||
.subscribeBy {
|
||||
recipientSnapshot = it
|
||||
}
|
||||
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
@@ -156,6 +163,13 @@ class ConversationViewModel(
|
||||
}.doOnNext {
|
||||
hasMessageRequestStateSubject.onNext(it.messageRequestState != MessageRequestState.NONE)
|
||||
}.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
recipientRepository.conversationRecipient.map { Unit }.subscribeWithSubject(refreshReminder, disposables)
|
||||
|
||||
reminder = Observable.combineLatest(refreshReminder.startWithItem(Unit), recipientRepository.groupRecord) { _, groupRecord -> groupRecord }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMapMaybe { groupRecord -> repository.getReminder(groupRecord.orNull()) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
||||
@@ -153,15 +153,15 @@ class DisabledInputView @JvmOverloads constructor(
|
||||
announcementGroupOnly = null
|
||||
}
|
||||
|
||||
private fun <VIEW : View> show(existingView: VIEW?, create: () -> VIEW, bind: VIEW.() -> Unit = {}): VIEW {
|
||||
private fun <V : View> show(existingView: V?, create: () -> V, bind: V.() -> Unit = {}): V {
|
||||
if (existingView != currentView) {
|
||||
removeIfNotNull(currentView)
|
||||
}
|
||||
|
||||
val view: VIEW = if (existingView != null) {
|
||||
val view: V = if (existingView != null) {
|
||||
existingView
|
||||
} else {
|
||||
val newView: VIEW = create()
|
||||
val newView: V = create()
|
||||
addView(newView, defaultLayoutParams())
|
||||
newView
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.addTo
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
@@ -14,13 +15,11 @@ import org.signal.core.util.concurrent.subscribeWithSubject
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationRecipientRepository
|
||||
import org.thoughtcrime.securesms.database.GroupTable
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupManagementRepository
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Manages group state and actions for conversations.
|
||||
@@ -32,13 +31,14 @@ class ConversationGroupViewModel(
|
||||
) : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private val _groupRecord: Subject<GroupRecord>
|
||||
private val _groupRecord: BehaviorSubject<GroupRecord>
|
||||
private val _reviewState: Subject<ConversationGroupReviewState>
|
||||
|
||||
private val _groupActiveState: Subject<ConversationGroupActiveState> = BehaviorSubject.create()
|
||||
private val _memberLevel: BehaviorSubject<ConversationGroupMemberLevel> = BehaviorSubject.create()
|
||||
private val _actionableRequestingMembersCount: Subject<Int> = BehaviorSubject.create()
|
||||
private val _gv1MigrationSuggestions: Subject<List<RecipientId>> = BehaviorSubject.create()
|
||||
|
||||
val groupRecordSnapshot: GroupRecord?
|
||||
get() = _groupRecord.value
|
||||
|
||||
init {
|
||||
_groupRecord = recipientRepository
|
||||
@@ -66,8 +66,6 @@ class ConversationGroupViewModel(
|
||||
disposables += _groupRecord.subscribe { groupRecord ->
|
||||
_groupActiveState.onNext(ConversationGroupActiveState(groupRecord.isActive, groupRecord.isV2Group))
|
||||
_memberLevel.onNext(ConversationGroupMemberLevel(groupRecord.memberLevel(Recipient.self()), groupRecord.isAnnouncementGroup))
|
||||
_actionableRequestingMembersCount.onNext(getActionableRequestingMembersCount(groupRecord))
|
||||
_gv1MigrationSuggestions.onNext(getGv1MigrationSuggestions(groupRecord))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,28 +87,6 @@ class ConversationGroupViewModel(
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
private fun getActionableRequestingMembersCount(groupRecord: GroupRecord): Int {
|
||||
return if (groupRecord.isV2Group && groupRecord.memberLevel(Recipient.self()) == GroupTable.MemberLevel.ADMINISTRATOR) {
|
||||
groupRecord.requireV2GroupProperties()
|
||||
.decryptedGroup
|
||||
.requestingMembersCount
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGv1MigrationSuggestions(groupRecord: GroupRecord): List<RecipientId> {
|
||||
return if (!groupRecord.isActive || !groupRecord.isV2Group || groupRecord.isPendingMember(Recipient.self())) {
|
||||
emptyList()
|
||||
} else {
|
||||
groupRecord.unmigratedV1Members
|
||||
.filterNot { groupRecord.members.contains(it) }
|
||||
.map { Recipient.resolved(it) }
|
||||
.filter { GroupsV1MigrationUtil.isAutoMigratable(it) }
|
||||
.map { it.id }
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelJoinRequest(): Single<Result<Unit, GroupChangeFailureReason>> {
|
||||
return _groupRecord
|
||||
.firstOrError()
|
||||
@@ -120,6 +96,16 @@ class ConversationGroupViewModel(
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun onSuggestedMembersBannerDismissed() {
|
||||
_groupRecord
|
||||
.firstOrError()
|
||||
.flatMapCompletable { group ->
|
||||
groupManagementRepository.removeUnmigratedV1Members(group.id.requireV2())
|
||||
}
|
||||
.subscribe()
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
class Factory(private val threadId: Long, private val recipientRepository: ConversationRecipientRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(ConversationGroupViewModel(threadId, recipientRepository = recipientRepository)) as T
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user