Use megaphones for PIN reminders.

This commit is contained in:
Greyson Parrelli 2020-02-07 20:37:35 -05:00
parent 38e4733433
commit ddc01b539f
20 changed files with 496 additions and 244 deletions

View File

@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.conversationlist;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
@ -107,7 +108,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity; import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.megaphone.Megaphone; import org.thoughtcrime.securesms.megaphone.Megaphone;
import org.thoughtcrime.securesms.megaphone.MegaphoneListener; import org.thoughtcrime.securesms.megaphone.MegaphoneActionController;
import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder; import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder;
import org.thoughtcrime.securesms.megaphone.Megaphones; import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
@ -138,7 +139,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
ItemClickListener, ItemClickListener,
ConversationListSearchAdapter.EventListener, ConversationListSearchAdapter.EventListener,
MainNavigator.BackHandler, MainNavigator.BackHandler,
MegaphoneListener MegaphoneActionController
{ {
private static final String TAG = Log.tag(ConversationListFragment.class); private static final String TAG = Log.tag(ConversationListFragment.class);
@ -367,13 +368,18 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
} }
@Override @Override
public void onMegaphoneSnooze(@NonNull Megaphone megaphone) { public @NonNull Activity getMegaphoneActivity() {
viewModel.onMegaphoneSnoozed(megaphone); return requireActivity();
} }
@Override @Override
public void onMegaphoneCompleted(@NonNull Megaphone megaphone) { public void onMegaphoneSnooze(@NonNull Megaphones.Event event) {
viewModel.onMegaphoneCompleted(megaphone.getEvent()); viewModel.onMegaphoneSnoozed(event);
}
@Override
public void onMegaphoneCompleted(@NonNull Megaphones.Event event) {
viewModel.onMegaphoneCompleted(event);
} }
private void initializeProfileIcon(@NonNull Recipient recipient) { private void initializeProfileIcon(@NonNull Recipient recipient) {
@ -713,7 +719,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
@Override @Override
public void onItemLongClick(ConversationListItem item) { public void onItemLongClick(ConversationListItem item) {
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(ConversationListFragment.this); actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this);
defaultAdapter.initializeBatchMode(true); defaultAdapter.initializeBatchMode(true);
defaultAdapter.toggleThreadInBatchSet(item.getThreadId()); defaultAdapter.toggleThreadInBatchSet(item.getThreadId());

View File

@ -69,8 +69,8 @@ class ConversationListViewModel extends ViewModel {
megaphoneRepository.markFinished(event); megaphoneRepository.markFinished(event);
} }
void onMegaphoneSnoozed(@NonNull Megaphone snoozed) { void onMegaphoneSnoozed(@NonNull Megaphones.Event event) {
megaphoneRepository.markSeen(snoozed.getEvent()); megaphoneRepository.markSeen(event);
megaphone.postValue(null); megaphone.postValue(null);
} }

View File

@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.keyvalue;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.lock.SignalPinReminders;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public final class PinValues {
private static final String TAG = Log.tag(PinValues.class);
private static final String LAST_SUCCESSFUL_ENTRY = "pin.last_successful_entry";
private static final String NEXT_INTERVAL = "pin.interval_index";
private final KeyValueStore store;
PinValues(KeyValueStore store) {
this.store = store;
}
public void onEntrySuccess() {
long nextInterval = SignalPinReminders.getNextInterval(getCurrentInterval());
Log.w(TAG, "onEntrySuccess() nextInterval: " + nextInterval);
store.beginWrite()
.putLong(LAST_SUCCESSFUL_ENTRY, System.currentTimeMillis())
.putLong(NEXT_INTERVAL, nextInterval)
.apply();
}
public void onEntryFailure() {
long nextInterval = SignalPinReminders.getPreviousInterval(getCurrentInterval());
Log.w(TAG, "onEntryFailure() nextInterval: " + nextInterval);
store.beginWrite()
.putLong(NEXT_INTERVAL, nextInterval)
.apply();
}
public void onPinChange() {
long nextInterval = SignalPinReminders.INITIAL_INTERVAL;
Log.w(TAG, "onPinChange() nextInterval: " + nextInterval);
store.beginWrite()
.putLong(NEXT_INTERVAL, nextInterval)
.putLong(LAST_SUCCESSFUL_ENTRY, System.currentTimeMillis())
.apply();
}
public long getCurrentInterval() {
return store.getLong(NEXT_INTERVAL, TextSecurePreferences.getRegistrationLockNextReminderInterval(ApplicationDependencies.getApplication()));
}
public long getLastSuccessfulEntryTime() {
return store.getLong(LAST_SUCCESSFUL_ENTRY, TextSecurePreferences.getRegistrationLockLastReminderTime(ApplicationDependencies.getApplication()));
}
}

View File

@ -17,14 +17,18 @@ public final class SignalStore {
private SignalStore() {} private SignalStore() {}
public static KbsValues kbsValues() { public static @NonNull KbsValues kbsValues() {
return new KbsValues(getStore()); return new KbsValues(getStore());
} }
public static RegistrationValues registrationValues() { public static @NonNull RegistrationValues registrationValues() {
return new RegistrationValues(getStore()); return new RegistrationValues(getStore());
} }
public static @NonNull PinValues pinValues() {
return new PinValues(getStore());
}
public static String getRemoteConfig() { public static String getRemoteConfig() {
return getStore().getString(REMOTE_CONFIG, null); return getStore().getString(REMOTE_CONFIG, null);
} }

View File

@ -6,7 +6,6 @@ import android.graphics.Typeface;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.text.Editable; import android.text.Editable;
import android.text.InputType;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
@ -27,19 +26,15 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.app.DialogCompat; import androidx.core.app.DialogCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.google.android.material.textfield.TextInputLayout;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KbsValues; import org.thoughtcrime.securesms.keyvalue.KbsValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsConstants; import org.thoughtcrime.securesms.lock.v2.KbsConstants;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob; import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
@ -66,98 +61,25 @@ public final class RegistrationLockDialog {
public static void showReminderIfNecessary(@NonNull Fragment fragment) { public static void showReminderIfNecessary(@NonNull Fragment fragment) {
final Context context = fragment.requireContext(); final Context context = fragment.requireContext();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return; if (!TextSecurePreferences.isV1RegistrationLockEnabled(context) && !SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
if (!RegistrationLockReminders.needsReminder(context)) return; return;
}
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context) && if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
!SignalStore.kbsValues().isV2RegistrationLockEnabled()) { return;
// Neither v1 or v2 to check against }
Log.w(TAG, "Reg lock enabled, but no pin stored to verify against");
if (!RegistrationLockReminders.needsReminder(context)) {
return; return;
} }
if (FeatureFlags.pinsForAll()) { if (FeatureFlags.pinsForAll()) {
showReminder(context, fragment); return;
} else {
showLegacyPinReminder(context);
}
}
private static void showReminder(@NonNull Context context, @NonNull Fragment fragment) {
AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark_SignalAccent : R.style.RationaleDialogLight_SignalAccent)
.setView(R.layout.kbs_pin_reminder_view)
.setCancelable(false)
.setOnCancelListener(d -> RegistrationLockReminders.scheduleReminder(context, false))
.create();
WindowManager windowManager = ServiceUtil.getWindowManager(context);
Display display = windowManager.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
dialog.show();
dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT);
TextInputLayout pinWrapper = (TextInputLayout) DialogCompat.requireViewById(dialog, R.id.pin_wrapper);
EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin);
TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder);
View skip = DialogCompat.requireViewById(dialog, R.id.skip);
View submit = DialogCompat.requireViewById(dialog, R.id.submit);
SpannableString reminderText = new SpannableString(context.getString(R.string.KbsReminderDialog__to_help_you_memorize_your_pin));
SpannableString forgotText = new SpannableString(context.getString(R.string.KbsReminderDialog__forgot_pin));
pinEditText.post(() -> {
if (pinEditText.requestFocus()) {
ServiceUtil.getInputMethodManager(pinEditText.getContext()).showSoftInput(pinEditText, 0);
}
});
switch (SignalStore.kbsValues().getKeyboardType()) {
case NUMERIC:
pinEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
break;
case ALPHA_NUMERIC:
pinEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
break;
} }
ClickableSpan clickableSpan = new ClickableSpan() { showLegacyPinReminder(context);
@Override
public void onClick(@NonNull View widget) {
dialog.dismiss();
RegistrationLockReminders.scheduleReminder(context, true);
fragment.startActivityForResult(CreateKbsPinActivity.getIntentForPinChangeFromForgotPin(context), CreateKbsPinActivity.REQUEST_NEW_PIN);
}
};
forgotText.setSpan(clickableSpan, 0, forgotText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
reminder.setText(new SpannableStringBuilder(reminderText).append(" ").append(forgotText));
reminder.setMovementMethod(LinkMovementMethod.getInstance());
skip.setOnClickListener(v -> {
dialog.dismiss();
RegistrationLockReminders.scheduleReminder(context, false);
});
PinVerifier.Callback callback = getPinWatcherCallback(context, dialog, pinWrapper);
PinVerifier verifier = SignalStore.kbsValues().isV2RegistrationLockEnabled()
? new V2PinVerifier()
: new V1PinVerifier(context);
submit.setOnClickListener(v -> {
Editable pinEditable = pinEditText.getText();
verifier.verifyPin(pinEditable == null ? null : pinEditable.toString(), callback);
});
} }
/**
* @deprecated TODO [alex]: Remove after pins for all live.
*/
@Deprecated
private static void showLegacyPinReminder(@NonNull Context context) { private static void showLegacyPinReminder(@NonNull Context context) {
AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark : R.style.RationaleDialogLight) AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark : R.style.RationaleDialogLight)
.setView(R.layout.registration_lock_reminder_view) .setView(R.layout.registration_lock_reminder_view)
@ -403,79 +325,4 @@ public final class RegistrationLockDialog {
dialog.show(); dialog.show();
} }
private static PinVerifier.Callback getPinWatcherCallback(@NonNull Context context,
@NonNull AlertDialog dialog,
@NonNull TextInputLayout inputWrapper)
{
return new PinVerifier.Callback() {
@Override
public void onPinCorrect() {
dialog.dismiss();
RegistrationLockReminders.scheduleReminder(context, true);
}
@Override
public void onPinWrong() {
inputWrapper.setError(context.getString(R.string.KbsReminderDialog__incorrect_pin_try_again));
}
};
}
private static final class V1PinVerifier implements PinVerifier {
private final String pinInPreferences;
private V1PinVerifier(@NonNull Context context) {
//noinspection deprecation Acceptable to check the old pin in a reminder on a non-migrated system.
this.pinInPreferences = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
}
@Override
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
if (pin != null && pin.replace(" ", "").equals(pinInPreferences)) {
callback.onPinCorrect();
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
} else {
callback.onPinWrong();
}
}
}
private static final class V2PinVerifier implements PinVerifier {
private final String localPinHash;
V2PinVerifier() {
localPinHash = SignalStore.kbsValues().getLocalPinHash();
if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
}
@Override
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
if (pin == null) return;
if (TextUtils.isEmpty(pin)) return;
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
callback.onPinCorrect();
} else {
callback.onPinWrong();
}
}
}
private interface PinVerifier {
void verifyPin(@Nullable String pin, @NonNull PinVerifier.Callback callback);
interface Callback {
void onPinCorrect();
void onPinWrong();
}
}
} }

View File

@ -25,9 +25,6 @@ public class RegistrationLockReminders {
public static final long INITIAL_INTERVAL = INTERVALS.first(); public static final long INITIAL_INTERVAL = INTERVALS.first();
public static boolean needsReminder(@NonNull Context context) { public static boolean needsReminder(@NonNull Context context) {
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context) &&
!SignalStore.kbsValues().isV2RegistrationLockEnabled()) return false;
long lastReminderTime = TextSecurePreferences.getRegistrationLockLastReminderTime(context); long lastReminderTime = TextSecurePreferences.getRegistrationLockLastReminderTime(context);
long nextIntervalTime = TextSecurePreferences.getRegistrationLockNextReminderInterval(context); long nextIntervalTime = TextSecurePreferences.getRegistrationLockNextReminderInterval(context);

View File

@ -0,0 +1,205 @@
package org.thoughtcrime.securesms.lock;
import android.content.Context;
import android.content.Intent;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.DialogCompat;
import com.google.android.material.textfield.TextInputLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
public final class SignalPinReminderDialog {
private static final String TAG = Log.tag(SignalPinReminderDialog.class);
public static void show(@NonNull Context context, @NonNull Launcher launcher, @NonNull Callback mainCallback) {
AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark_SignalAccent : R.style.RationaleDialogLight_SignalAccent)
.setView(R.layout.kbs_pin_reminder_view)
.setCancelable(false)
.setOnCancelListener(d -> RegistrationLockReminders.scheduleReminder(context, false))
.create();
WindowManager windowManager = ServiceUtil.getWindowManager(context);
Display display = windowManager.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
dialog.show();
dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT);
TextInputLayout pinWrapper = (TextInputLayout) DialogCompat.requireViewById(dialog, R.id.pin_wrapper);
EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin);
TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder);
View skip = DialogCompat.requireViewById(dialog, R.id.skip);
View submit = DialogCompat.requireViewById(dialog, R.id.submit);
SpannableString reminderText = new SpannableString(context.getString(R.string.KbsReminderDialog__to_help_you_memorize_your_pin));
SpannableString forgotText = new SpannableString(context.getString(R.string.KbsReminderDialog__forgot_pin));
pinEditText.post(() -> {
if (pinEditText.requestFocus()) {
ServiceUtil.getInputMethodManager(pinEditText.getContext()).showSoftInput(pinEditText, 0);
}
});
switch (SignalStore.kbsValues().getKeyboardType()) {
case NUMERIC:
pinEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
break;
case ALPHA_NUMERIC:
pinEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
break;
}
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
dialog.dismiss();
launcher.launch(CreateKbsPinActivity.getIntentForPinChangeFromForgotPin(context), CreateKbsPinActivity.REQUEST_NEW_PIN);
}
};
forgotText.setSpan(clickableSpan, 0, forgotText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
reminder.setText(new SpannableStringBuilder(reminderText).append(" ").append(forgotText));
reminder.setMovementMethod(LinkMovementMethod.getInstance());
PinVerifier.Callback callback = getPinWatcherCallback(context, dialog, pinWrapper, mainCallback);
PinVerifier verifier = SignalStore.kbsValues().isV2RegistrationLockEnabled()
? new V2PinVerifier()
: new V1PinVerifier(context);
skip.setOnClickListener(v -> {
dialog.dismiss();
mainCallback.onReminderDismissed(callback.hadWrongGuess());
});
submit.setOnClickListener(v -> {
Editable pinEditable = pinEditText.getText();
verifier.verifyPin(pinEditable == null ? null : pinEditable.toString(), callback);
});
}
private static PinVerifier.Callback getPinWatcherCallback(@NonNull Context context,
@NonNull AlertDialog dialog,
@NonNull TextInputLayout inputWrapper,
@NonNull Callback mainCallback)
{
return new PinVerifier.Callback() {
boolean hadWrongGuess = false;
@Override
public void onPinCorrect() {
dialog.dismiss();
mainCallback.onReminderCompleted(hadWrongGuess);
}
@Override
public void onPinWrong() {
hadWrongGuess = true;
inputWrapper.setError(context.getString(R.string.KbsReminderDialog__incorrect_pin_try_again));
}
@Override
public boolean hadWrongGuess() {
return hadWrongGuess;
}
};
}
private static final class V1PinVerifier implements PinVerifier {
private final String pinInPreferences;
private V1PinVerifier(@NonNull Context context) {
//noinspection deprecation Acceptable to check the old pin in a reminder on a non-migrated system.
this.pinInPreferences = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
}
@Override
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
if (pin != null && pin.replace(" ", "").equals(pinInPreferences)) {
callback.onPinCorrect();
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
} else {
callback.onPinWrong();
}
}
}
private static final class V2PinVerifier implements PinVerifier {
private final String localPinHash;
V2PinVerifier() {
localPinHash = SignalStore.kbsValues().getLocalPinHash();
if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
}
@Override
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
if (pin == null) return;
if (TextUtils.isEmpty(pin)) return;
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
callback.onPinCorrect();
} else {
callback.onPinWrong();
}
}
}
private interface PinVerifier {
void verifyPin(@Nullable String pin, @NonNull PinVerifier.Callback callback);
interface Callback {
void onPinCorrect();
void onPinWrong();
boolean hadWrongGuess();
}
}
public interface Launcher {
void launch(@NonNull Intent intent, int requestCode);
}
public interface Callback {
void onReminderDismissed(boolean includedFailure);
void onReminderCompleted(boolean includedFailure);
}
}

View File

@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.lock;
import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import java.util.HashMap;
import java.util.Map;
import java.util.NavigableSet;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
/**
* Reminder intervals for Signal PINs.
*/
public class SignalPinReminders {
private static final String TAG = Log.tag(SignalPinReminders.class);
private static final long ONE_DAY = TimeUnit.DAYS.toMillis(1);
private static final long THREE_DAYS = TimeUnit.DAYS.toMillis(3);
private static final long ONE_WEEK = TimeUnit.DAYS.toMillis(7);
private static final long TWO_WEEKS = TimeUnit.DAYS.toMillis(14);
private static final NavigableSet<Long> INTERVALS = new TreeSet<Long>() {{
add(ONE_DAY);
add(THREE_DAYS);
add(ONE_WEEK);
add(TWO_WEEKS);
}};
private static final Map<Long, Integer> STRINGS = new HashMap<Long, Integer>() {{
put(ONE_DAY, R.string.SignalPinReminders_well_remind_you_again_tomorrow);
put(THREE_DAYS, R.string.SignalPinReminders_well_remind_you_again_in_a_few_days);
put(ONE_WEEK, R.string.SignalPinReminders_well_remind_you_again_in_a_week);
put(TWO_WEEKS, R.string.SignalPinReminders_well_remind_you_again_in_a_couple_weeks);
}};
public static final long INITIAL_INTERVAL = INTERVALS.first();
public static long getNextInterval(long currentInterval) {
Long next = INTERVALS.higher(currentInterval);
return next != null ? next : INTERVALS.last();
}
public static long getPreviousInterval(long currentInterval) {
Long previous = INTERVALS.lower(currentInterval);
return previous != null ? previous : INTERVALS.first();
}
public static @StringRes int getReminderString(long interval) {
Integer stringRes = STRINGS.get(interval);
if (stringRes != null) {
return stringRes;
} else {
Log.w(TAG, "Couldn't find a string for interval " + interval);
return R.string.SignalPinReminders_well_remind_you_again_later;
}
}
}

View File

@ -108,6 +108,7 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewM
requireActivity().setResult(Activity.RESULT_OK); requireActivity().setResult(Activity.RESULT_OK);
closeNavGraphBranch(); closeNavGraphBranch();
SignalStore.registrationValues().setRegistrationComplete(); SignalStore.registrationValues().setRegistrationComplete();
SignalStore.pinValues().onPinChange();
} }
}); });
break; break;

View File

@ -20,8 +20,8 @@ public class BasicMegaphoneView extends FrameLayout {
private Button actionButton; private Button actionButton;
private Button snoozeButton; private Button snoozeButton;
private Megaphone megaphone; private Megaphone megaphone;
private MegaphoneListener megaphoneListener; private MegaphoneActionController megaphoneListener;
public BasicMegaphoneView(@NonNull Context context) { public BasicMegaphoneView(@NonNull Context context) {
super(context); super(context);
@ -52,7 +52,7 @@ public class BasicMegaphoneView extends FrameLayout {
} }
} }
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneListener megaphoneListener) { public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController megaphoneListener) {
this.megaphone = megaphone; this.megaphone = megaphone;
this.megaphoneListener = megaphoneListener; this.megaphoneListener = megaphoneListener;
@ -92,7 +92,7 @@ public class BasicMegaphoneView extends FrameLayout {
if (megaphone.canSnooze()) { if (megaphone.canSnooze()) {
snoozeButton.setVisibility(VISIBLE); snoozeButton.setVisibility(VISIBLE);
snoozeButton.setOnClickListener(v -> { snoozeButton.setOnClickListener(v -> {
megaphoneListener.onMegaphoneSnooze(megaphone); megaphoneListener.onMegaphoneSnooze(megaphone.getEvent());
if (megaphone.getSnoozeListener() != null) { if (megaphone.getSnoozeListener() != null) {
megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener); megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);

View File

@ -139,7 +139,7 @@ public class Megaphone {
return this; return this;
} }
public @NonNull Builder setButtonText(@StringRes int buttonTextRes, @NonNull EventListener listener) { public @NonNull Builder setActionButton(@StringRes int buttonTextRes, @NonNull EventListener listener) {
this.buttonTextRes = buttonTextRes; this.buttonTextRes = buttonTextRes;
this.buttonListener = listener; this.buttonListener = listener;
return this; return this;
@ -160,6 +160,6 @@ public class Megaphone {
} }
public interface EventListener { public interface EventListener {
void onEvent(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener); void onEvent(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController listener);
} }
} }

View File

@ -1,11 +1,12 @@
package org.thoughtcrime.securesms.megaphone; package org.thoughtcrime.securesms.megaphone;
import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
public interface MegaphoneListener { public interface MegaphoneActionController {
/** /**
* When a megaphone wants to navigate to a specific intent. * When a megaphone wants to navigate to a specific intent.
*/ */
@ -21,13 +22,18 @@ public interface MegaphoneListener {
*/ */
void onMegaphoneToastRequested(@NonNull String string); void onMegaphoneToastRequested(@NonNull String string);
/**
* When a megaphone needs a raw activity reference. Favor more specific methods when possible.
*/
@NonNull Activity getMegaphoneActivity();
/** /**
* When a megaphone has been snoozed via "remind me later" or a similar option. * When a megaphone has been snoozed via "remind me later" or a similar option.
*/ */
void onMegaphoneSnooze(@NonNull Megaphone megaphone); void onMegaphoneSnooze(@NonNull Megaphones.Event event);
/** /**
* Called when a megaphone completed its goal. * Called when a megaphone completed its goal.
*/ */
void onMegaphoneCompleted(@NonNull Megaphone megaphone); void onMegaphoneCompleted(@NonNull Megaphones.Event event);
} }

View File

@ -12,7 +12,7 @@ public class MegaphoneViewBuilder {
public static @Nullable View build(@NonNull Context context, public static @Nullable View build(@NonNull Context context,
@NonNull Megaphone megaphone, @NonNull Megaphone megaphone,
@NonNull MegaphoneListener listener) @NonNull MegaphoneActionController listener)
{ {
switch (megaphone.getStyle()) { switch (megaphone.getStyle()) {
case BASIC: case BASIC:
@ -28,7 +28,7 @@ public class MegaphoneViewBuilder {
private static @NonNull View buildBasicMegaphone(@NonNull Context context, private static @NonNull View buildBasicMegaphone(@NonNull Context context,
@NonNull Megaphone megaphone, @NonNull Megaphone megaphone,
@NonNull MegaphoneListener listener) @NonNull MegaphoneActionController listener)
{ {
BasicMegaphoneView view = new BasicMegaphoneView(context); BasicMegaphoneView view = new BasicMegaphoneView(context);
view.present(megaphone, listener); view.present(megaphone, listener);
@ -37,7 +37,7 @@ public class MegaphoneViewBuilder {
private static @NonNull View buildReactionsMegaphone(@NonNull Context context, private static @NonNull View buildReactionsMegaphone(@NonNull Context context,
@NonNull Megaphone megaphone, @NonNull Megaphone megaphone,
@NonNull MegaphoneListener listener) @NonNull MegaphoneActionController listener)
{ {
ReactionsMegaphoneView view = new ReactionsMegaphoneView(context); ReactionsMegaphoneView view = new ReactionsMegaphoneView(context);
view.present(megaphone, listener); view.present(megaphone, listener);

View File

@ -12,6 +12,11 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.MegaphoneRecord; import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.lock.SignalPinReminderDialog;
import org.thoughtcrime.securesms.lock.SignalPinReminders;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity; import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity;
import org.thoughtcrime.securesms.lock.v2.PinUtil; import org.thoughtcrime.securesms.lock.v2.PinUtil;
@ -76,6 +81,7 @@ public final class Megaphones {
return new LinkedHashMap<Event, MegaphoneSchedule>() {{ return new LinkedHashMap<Event, MegaphoneSchedule>() {{
put(Event.REACTIONS, new ForeverSchedule(true)); put(Event.REACTIONS, new ForeverSchedule(true));
put(Event.PINS_FOR_ALL, new PinsForAllSchedule()); put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
put(Event.PIN_REMINDER, new PinReminderSchedule());
}}; }};
} }
@ -84,7 +90,9 @@ public final class Megaphones {
case REACTIONS: case REACTIONS:
return buildReactionsMegaphone(); return buildReactionsMegaphone();
case PINS_FOR_ALL: case PINS_FOR_ALL:
return buildPinsForAllMegaphone(context, record); return buildPinsForAllMegaphone(record);
case PIN_REMINDER:
return buildPinReminderMegaphone(context);
default: default:
throw new IllegalArgumentException("Event not handled!"); throw new IllegalArgumentException("Event not handled!");
} }
@ -96,7 +104,7 @@ public final class Megaphones {
.build(); .build();
} }
private static @NonNull Megaphone buildPinsForAllMegaphone(@NonNull Context context, @NonNull MegaphoneRecord record) { private static @NonNull Megaphone buildPinsForAllMegaphone(@NonNull MegaphoneRecord record) {
if (PinsForAllSchedule.shouldDisplayFullScreen(record.getFirstVisible(), System.currentTimeMillis())) { if (PinsForAllSchedule.shouldDisplayFullScreen(record.getFirstVisible(), System.currentTimeMillis())) {
return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.FULLSCREEN) return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.FULLSCREEN)
.setMandatory(true) .setMandatory(true)
@ -125,7 +133,7 @@ public final class Megaphones {
private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithPin(@NonNull Megaphone.Builder builder) { private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithPin(@NonNull Megaphone.Builder builder) {
return builder.setTitle(R.string.KbsMegaphone__introducing_pins) return builder.setTitle(R.string.KbsMegaphone__introducing_pins)
.setBody(R.string.KbsMegaphone__your_registration_lock_is_now_called_a_pin) .setBody(R.string.KbsMegaphone__your_registration_lock_is_now_called_a_pin)
.setButtonText(R.string.KbsMegaphone__update_pin, (megaphone, listener) -> { .setActionButton(R.string.KbsMegaphone__update_pin, (megaphone, listener) -> {
Intent intent = CreateKbsPinActivity.getIntentForPinChangeFromSettings(ApplicationDependencies.getApplication()); Intent intent = CreateKbsPinActivity.getIntentForPinChangeFromSettings(ApplicationDependencies.getApplication());
listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN); listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN);
@ -136,7 +144,7 @@ public final class Megaphones {
private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithoutPin(@NonNull Megaphone.Builder builder) { private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithoutPin(@NonNull Megaphone.Builder builder) {
return builder.setTitle(R.string.KbsMegaphone__create_a_pin) return builder.setTitle(R.string.KbsMegaphone__create_a_pin)
.setBody(R.string.KbsMegaphone__pins_add_another_layer_of_security_to_your_signal_account) .setBody(R.string.KbsMegaphone__pins_add_another_layer_of_security_to_your_signal_account)
.setButtonText(R.string.KbsMegaphone__create_pin, (megaphone, listener) -> { .setActionButton(R.string.KbsMegaphone__create_pin, (megaphone, listener) -> {
Intent intent = CreateKbsPinActivity.getIntentForPinCreate(ApplicationDependencies.getApplication()); Intent intent = CreateKbsPinActivity.getIntentForPinCreate(ApplicationDependencies.getApplication());
listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN); listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN);
@ -144,9 +152,40 @@ public final class Megaphones {
.build(); .build();
} }
private static @NonNull Megaphone buildPinReminderMegaphone(@NonNull Context context) {
return new Megaphone.Builder(Event.PIN_REMINDER, Megaphone.Style.BASIC)
.setTitle(R.string.Megaphones_verify_your_signal_pin)
.setBody(R.string.Megaphones_well_occasionally_ask_you_to_verify_your_pin)
.setImage(R.drawable.kbs_pin_megaphone)
.setActionButton(R.string.Megaphones_verify_pin, (megaphone, controller) -> {
SignalPinReminderDialog.show(controller.getMegaphoneActivity(), controller::onMegaphoneNavigationRequested, new SignalPinReminderDialog.Callback() {
@Override
public void onReminderDismissed(boolean includedFailure) {
if (includedFailure) {
SignalStore.pinValues().onEntryFailure();
}
}
@Override
public void onReminderCompleted(boolean includedFailure) {
if (includedFailure) {
SignalStore.pinValues().onEntryFailure();
} else {
SignalStore.pinValues().onEntrySuccess();
}
controller.onMegaphoneSnooze(Event.PIN_REMINDER);
controller.onMegaphoneToastRequested(context.getString(SignalPinReminders.getReminderString(SignalStore.pinValues().getCurrentInterval())));
}
});
})
.build();
}
public enum Event { public enum Event {
REACTIONS("reactions"), REACTIONS("reactions"),
PINS_FOR_ALL("pins_for_all"); PINS_FOR_ALL("pins_for_all"),
PIN_REMINDER("pin_reminder");
private final String key; private final String key;

View File

@ -0,0 +1,23 @@
package org.thoughtcrime.securesms.megaphone;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.FeatureFlags;
final class PinReminderSchedule implements MegaphoneSchedule {
@Override
public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) {
if (!SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
return false;
}
if (!FeatureFlags.pinsForAll()) {
return false;
}
long lastSuccessTime = SignalStore.pinValues().getLastSuccessfulEntryTime();
long interval = SignalStore.pinValues().getCurrentInterval();
return currentTime - lastSuccessTime >= interval;
}
}

View File

@ -9,7 +9,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.megaphone.Megaphone; import org.thoughtcrime.securesms.megaphone.Megaphone;
import org.thoughtcrime.securesms.megaphone.MegaphoneListener; import org.thoughtcrime.securesms.megaphone.MegaphoneActionController;
public class ReactionsMegaphoneView extends FrameLayout { public class ReactionsMegaphoneView extends FrameLayout {
@ -31,7 +31,7 @@ public class ReactionsMegaphoneView extends FrameLayout {
this.closeButton = findViewById(R.id.reactions_megaphone_x); this.closeButton = findViewById(R.id.reactions_megaphone_x);
} }
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener) { public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController listener) {
this.closeButton.setOnClickListener(v -> listener.onMegaphoneCompleted(megaphone)); this.closeButton.setOnClickListener(v -> listener.onMegaphoneCompleted(megaphone.getEvent()));
} }
} }

View File

@ -12,7 +12,8 @@
android:paddingStart="8dp" android:paddingStart="8dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:background="?megaphone_background"> android:background="?megaphone_background"
android:clickable="true">
<ImageView <ImageView
android:id="@+id/basic_megaphone_image" android:id="@+id/basic_megaphone_image"

View File

@ -1,36 +1,34 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:padding="20dp">
<LinearLayout <TextView
android:id="@+id/header_container" android:id="@+id/pin_reminder_title"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/signal_primary"
android:gravity="center" android:gravity="center"
android:orientation="horizontal" android:text="@string/KbsReminderDialog__enter_your_signal_pin"
android:padding="40dp"> style="@style/Signal.Text.Body"
android:fontFamily="sans-serif-medium"
<TextView app:layout_constraintTop_toTopOf="parent"
android:layout_width="wrap_content" app:layout_constraintStart_toStartOf="parent"
android:layout_height="wrap_content" app:layout_constraintEnd_toEndOf="parent" />
android:gravity="center_horizontal"
android:text="@string/KbsReminderDialog__enter_your_signal_pin"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/pin_wrapper" android:id="@+id/pin_wrapper"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="80dp" android:paddingStart="80dp"
android:paddingTop="40dp" android:paddingTop="40dp"
android:paddingEnd="80dp"> android:paddingEnd="80dp"
app:layout_constraintTop_toBottomOf="@id/pin_reminder_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/pin" android:id="@+id/pin"
@ -43,37 +41,33 @@
<TextView <TextView
android:id="@+id/reminder" android:id="@+id/reminder"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.3" android:lineSpacingMultiplier="1.3"
android:paddingStart="20dp"
android:paddingTop="40dp" android:paddingTop="40dp"
android:paddingEnd="20dp"
android:paddingBottom="40dp" android:paddingBottom="40dp"
android:textSize="15sp" style="@style/Signal.Text.Preview"
app:layout_constraintTop_toBottomOf="@id/pin_wrapper"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="@string/KbsReminderDialog__to_help_you_memorize_your_pin" /> tools:text="@string/KbsReminderDialog__to_help_you_memorize_your_pin" />
<LinearLayout <Button
android:layout_width="match_parent" android:id="@+id/skip"
style="@style/Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="end" android:text="@string/KbsReminderDialog__skip"
android:orientation="horizontal" app:layout_constraintTop_toTopOf="@id/submit"
android:paddingEnd="20dp" app:layout_constraintEnd_toStartOf="@id/submit"/>
android:paddingBottom="20dp">
<Button <Button
android:id="@+id/skip" android:id="@+id/submit"
style="@style/Button.Borderless" style="@style/Button.Primary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/KbsReminderDialog__skip" /> android:text="@string/KbsReminderDialog__submit"
app:layout_constraintTop_toBottomOf="@id/reminder"
app:layout_constraintEnd_toEndOf="parent"/>
<Button </androidx.constraintlayout.widget.ConstraintLayout>
android:id="@+id/submit"
style="@style/Button.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/KbsReminderDialog__submit" />
</LinearLayout>
</LinearLayout>

View File

@ -545,6 +545,9 @@
<string name="Megaphones_introducing_reactions">Introducing Reactions</string> <string name="Megaphones_introducing_reactions">Introducing Reactions</string>
<string name="Megaphones_tap_and_hold_any_message_to_quicky_share_how_you_feel">Tap and hold any message to quickly share how you feel.</string> <string name="Megaphones_tap_and_hold_any_message_to_quicky_share_how_you_feel">Tap and hold any message to quickly share how you feel.</string>
<string name="Megaphones_remind_me_later">Remind me later</string> <string name="Megaphones_remind_me_later">Remind me later</string>
<string name="Megaphones_verify_your_signal_pin">Verify your Signal PIN</string>
<string name="Megaphones_well_occasionally_ask_you_to_verify_your_pin">We\'ll occasionally ask you to verify your PIN so that you remember it.</string>
<string name="Megaphones_verify_pin">Verify PIN</string>
<!-- NotificationBarManager --> <!-- NotificationBarManager -->
<string name="NotificationBarManager_signal_call_in_progress">Signal call in progress</string> <string name="NotificationBarManager_signal_call_in_progress">Signal call in progress</string>
@ -775,6 +778,13 @@
<string name="SharedContactView_invite_to_signal">Invite to Signal</string> <string name="SharedContactView_invite_to_signal">Invite to Signal</string>
<string name="SharedContactView_message">Signal Message</string> <string name="SharedContactView_message">Signal Message</string>
<!-- SignalPinReminders -->
<string name="SignalPinReminders_well_remind_you_again_later">We\'ll remind you again later.</string>
<string name="SignalPinReminders_well_remind_you_again_tomorrow">We\'ll remind you again tomorrow.</string>
<string name="SignalPinReminders_well_remind_you_again_in_a_few_days">We\'ll remind you again in a few days.</string>
<string name="SignalPinReminders_well_remind_you_again_in_a_week">We\'ll remind you again in a week.</string>
<string name="SignalPinReminders_well_remind_you_again_in_a_couple_weeks">We\'ll remind you again in a couple weeks.</string>
<!-- Slide --> <!-- Slide -->
<string name="Slide_image">Image</string> <string name="Slide_image">Image</string>
<string name="Slide_sticker">Sticker</string> <string name="Slide_sticker">Sticker</string>

View File

@ -321,7 +321,7 @@
<item name="media_keyboard_button_color">@color/core_grey_60</item> <item name="media_keyboard_button_color">@color/core_grey_60</item>
<item name="megaphone_background">@color/core_white</item> <item name="megaphone_background">@color/core_grey_05</item>
<item name="megaphone_background_shadow">@drawable/megaphone_background_shadow</item> <item name="megaphone_background_shadow">@drawable/megaphone_background_shadow</item>
<item name="megaphone_body_text_color">@color/core_grey_65</item> <item name="megaphone_body_text_color">@color/core_grey_65</item>
<item name="megaphone_reactions_shade">@color/core_grey_02</item> <item name="megaphone_reactions_shade">@color/core_grey_02</item>