diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index 9ddaf1e27d..502c46dc71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -578,6 +578,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter } ) + clickPref( + title = DSLSettingsText.from("Clear Username education ui hint"), + onClick = { + SignalStore.uiHints().clearHasSeenUsernameEducation() + } + ) + if (FeatureFlags.chatFilters()) { dividerPref() sectionHeaderPref(DSLSettingsText.from("Chat Filters")) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java index 881126f012..7837158a1a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java @@ -12,7 +12,8 @@ import java.util.List; public final class PhoneNumberPrivacyValues extends SignalStoreValues { public static final String SHARING_MODE = "phoneNumberPrivacy.sharingMode"; - public static final String LISTING_MODE = "phoneNumberPrivacy.listingMode"; + public static final String LISTING_MODE = "phoneNumberPrivacy.listingMode"; + public static final String LISTING_TIMESTAMP = "phoneNumberPrivacy.listingMode.timestamp"; private static final Collection REGULAR_CERTIFICATE = Collections.singletonList(CertificateType.UUID_AND_E164); private static final Collection PRIVACY_CERTIFICATE = Collections.singletonList(CertificateType.UUID_ONLY); @@ -32,7 +33,7 @@ public final class PhoneNumberPrivacyValues extends SignalStoreValues { @Override @NonNull List getKeysToIncludeInBackup() { - return Arrays.asList(SHARING_MODE, LISTING_MODE); + return Arrays.asList(SHARING_MODE, LISTING_MODE, LISTING_TIMESTAMP); } public @NonNull PhoneNumberSharingMode getPhoneNumberSharingMode() { @@ -56,7 +57,15 @@ public final class PhoneNumberPrivacyValues extends SignalStoreValues { } public void setPhoneNumberListingMode(@NonNull PhoneNumberListingMode phoneNumberListingMode) { - putInteger(LISTING_MODE, phoneNumberListingMode.serialize()); + getStore() + .beginWrite() + .putInteger(LISTING_MODE, phoneNumberListingMode.serialize()) + .putLong(LISTING_TIMESTAMP, System.currentTimeMillis()) + .apply(); + } + + public long getPhoneNumberListingModeTimestamp() { + return getLong(LISTING_TIMESTAMP, 0); } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java index b73fa6e38c..7471cab92c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.keyvalue; import androidx.annotation.NonNull; -import java.util.Collections; +import java.util.Arrays; import java.util.List; public class UiHints extends SignalStoreValues { @@ -14,7 +14,7 @@ public class UiHints extends SignalStoreValues { private static final String HAS_SET_OR_SKIPPED_USERNAME_CREATION = "uihints.has_set_or_skipped_username_creation"; private static final String NEVER_DISPLAY_PULL_TO_FILTER_TIP = "uihints.never_display_pull_to_filter_tip"; private static final String HAS_SEEN_SCHEDULED_MESSAGES_INFO_ONCE = "uihints.has_seen_scheduled_messages_info_once"; - + private static final String HAS_SEEN_USERNAME_EDUCATION = "uihints.has_seen_username_education"; UiHints(@NonNull KeyValueStore store) { super(store); } @@ -26,7 +26,7 @@ public class UiHints extends SignalStoreValues { @Override @NonNull List getKeysToIncludeInBackup() { - return Collections.singletonList(NEVER_DISPLAY_PULL_TO_FILTER_TIP); + return Arrays.asList(NEVER_DISPLAY_PULL_TO_FILTER_TIP, HAS_SEEN_USERNAME_EDUCATION); } public void markHasSeenGroupSettingsMenuToast() { @@ -61,6 +61,19 @@ public class UiHints extends SignalStoreValues { putBoolean(HAS_SET_OR_SKIPPED_USERNAME_CREATION, true); } + public void markHasSeenUsernameEducation() { + putBoolean(HAS_SEEN_USERNAME_EDUCATION, true); + } + + public boolean hasSeenUsernameEducation() { + return getBoolean(HAS_SEEN_USERNAME_EDUCATION, false); + } + + public void clearHasSeenUsernameEducation() { + putBoolean(HAS_SEEN_USERNAME_EDUCATION, false); + } + + public void resetNeverDisplayPullToRefreshCount() { putInteger(NEVER_DISPLAY_PULL_TO_FILTER_TIP, 0); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index f52faf0149..b7d479fda6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -12,6 +12,7 @@ import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; +import org.signal.core.util.MapUtil; import org.signal.core.util.SetUtil; import org.signal.core.util.TranslationDetection; import org.signal.core.util.logging.Log; @@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SmsExportPhase; import org.thoughtcrime.securesms.lock.SignalPinReminderDialog; @@ -54,13 +56,13 @@ import java.util.concurrent.TimeUnit; * - Add an enum to {@link Event} * - Return a megaphone in {@link #forRecord(Context, MegaphoneRecord)} * - Include the event in {@link #buildDisplayOrder(Context, Map)} - * + *

* Common patterns: * - For events that have a snooze-able recurring display schedule, use a {@link RecurringSchedule}. * - For events guarded by feature flags, set a {@link ForeverSchedule} with false in - * {@link #buildDisplayOrder(Context, Map)}. + * {@link #buildDisplayOrder(Context, Map)}. * - For events that change, return different megaphones in {@link #forRecord(Context, MegaphoneRecord)} - * based on whatever properties you're interested in. + * based on whatever properties you're interested in. */ public final class Megaphones { @@ -80,7 +82,7 @@ public final class Megaphones { List megaphones = Stream.of(buildDisplayOrder(context, records)) .filter(e -> { - MegaphoneRecord record = Objects.requireNonNull(records.get(e.getKey())); + MegaphoneRecord record = Objects.requireNonNull(records.get(e.getKey())); MegaphoneSchedule schedule = e.getValue(); return !record.isFinished() && schedule.shouldDisplay(record.getSeenCount(), record.getLastSeen(), record.getFirstVisible(), currentTime); @@ -100,7 +102,7 @@ public final class Megaphones { /** * The megaphones we want to display *in priority order*. This is a {@link LinkedHashMap}, so order is preserved. * We will render the first applicable megaphone in this collection. - * + *

* This is also when you would hide certain megaphones based on things like {@link FeatureFlags}. */ private static Map buildDisplayOrder(@NonNull Context context, @NonNull Map records) { @@ -115,6 +117,7 @@ public final class Megaphones { put(Event.DONATE_Q2_2022, shouldShowDonateMegaphone(context, Event.DONATE_Q2_2022, records) ? ShowForDurationSchedule.showForDays(7) : NEVER); put(Event.REMOTE_MEGAPHONE, shouldShowRemoteMegaphone(records) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(1)) : NEVER); put(Event.PIN_REMINDER, new SignalPinReminderSchedule()); + put(Event.SET_UP_YOUR_USERNAME, shouldShowSetUpYourUsernameMegaphone(records) ? ALWAYS : NEVER); // Feature-introduction megaphones should *probably* be added below this divider put(Event.ADD_A_PROFILE_PHOTO, shouldShowAddAProfilePhotoMegaphone(context) ? ALWAYS : NEVER); @@ -147,6 +150,9 @@ public final class Megaphones { return buildBackupPermissionMegaphone(context); case SMS_EXPORT: return buildSmsExportMegaphone(context); + case SET_UP_YOUR_USERNAME: + return buildSetUpYourUsernameMegaphone(context); + default: throw new IllegalArgumentException("Event not handled!"); } @@ -155,110 +161,110 @@ public final class Megaphones { private static @NonNull Megaphone buildPinsForAllMegaphone(@NonNull MegaphoneRecord record) { if (PinsForAllSchedule.shouldDisplayFullScreen(record.getFirstVisible(), System.currentTimeMillis())) { return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.FULLSCREEN) - .enableSnooze(null) - .setOnVisibleListener((megaphone, listener) -> { - if (new NetworkConstraint.Factory(ApplicationDependencies.getApplication()).create().isMet()) { - listener.onMegaphoneNavigationRequested(KbsMigrationActivity.createIntent(), KbsMigrationActivity.REQUEST_NEW_PIN); - } - }) - .build(); + .enableSnooze(null) + .setOnVisibleListener((megaphone, listener) -> { + if (new NetworkConstraint.Factory(ApplicationDependencies.getApplication()).create().isMet()) { + listener.onMegaphoneNavigationRequested(KbsMigrationActivity.createIntent(), KbsMigrationActivity.REQUEST_NEW_PIN); + } + }) + .build(); } else { return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.BASIC) - .setImage(R.drawable.kbs_pin_megaphone) - .setTitle(R.string.KbsMegaphone__create_a_pin) - .setBody(R.string.KbsMegaphone__pins_keep_information_thats_stored_with_signal_encrytped) - .setActionButton(R.string.KbsMegaphone__create_pin, (megaphone, listener) -> { - Intent intent = CreateKbsPinActivity.getIntentForPinCreate(ApplicationDependencies.getApplication()); + .setImage(R.drawable.kbs_pin_megaphone) + .setTitle(R.string.KbsMegaphone__create_a_pin) + .setBody(R.string.KbsMegaphone__pins_keep_information_thats_stored_with_signal_encrytped) + .setActionButton(R.string.KbsMegaphone__create_pin, (megaphone, listener) -> { + Intent intent = CreateKbsPinActivity.getIntentForPinCreate(ApplicationDependencies.getApplication()); - listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN); - }) - .build(); + listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN); + }) + .build(); } } @SuppressWarnings("CodeBlock2Expr") 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) { - Log.i(TAG, "[PinReminder] onReminderDismissed(" + includedFailure + ")"); - if (includedFailure) { - SignalStore.pinValues().onEntrySkipWithWrongGuess(); - } - } + .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) { + Log.i(TAG, "[PinReminder] onReminderDismissed(" + includedFailure + ")"); + if (includedFailure) { + SignalStore.pinValues().onEntrySkipWithWrongGuess(); + } + } - @Override - public void onReminderCompleted(@NonNull String pin, boolean includedFailure) { - Log.i(TAG, "[PinReminder] onReminderCompleted(" + includedFailure + ")"); - if (includedFailure) { - SignalStore.pinValues().onEntrySuccessWithWrongGuess(pin); - } else { - SignalStore.pinValues().onEntrySuccess(pin); - } + @Override + public void onReminderCompleted(@NonNull String pin, boolean includedFailure) { + Log.i(TAG, "[PinReminder] onReminderCompleted(" + includedFailure + ")"); + if (includedFailure) { + SignalStore.pinValues().onEntrySuccessWithWrongGuess(pin); + } else { + SignalStore.pinValues().onEntrySuccess(pin); + } - controller.onMegaphoneSnooze(Event.PIN_REMINDER); - controller.onMegaphoneToastRequested(controller.getMegaphoneActivity().getString(SignalPinReminders.getReminderString(SignalStore.pinValues().getCurrentInterval()))); - } - }); - }) - .build(); + controller.onMegaphoneSnooze(Event.PIN_REMINDER); + controller.onMegaphoneToastRequested(controller.getMegaphoneActivity().getString(SignalPinReminders.getReminderString(SignalStore.pinValues().getCurrentInterval()))); + } + }); + }) + .build(); } private static @NonNull Megaphone buildClientDeprecatedMegaphone(@NonNull Context context) { return new Megaphone.Builder(Event.CLIENT_DEPRECATED, Megaphone.Style.FULLSCREEN) - .disableSnooze() - .setOnVisibleListener((megaphone, listener) -> listener.onMegaphoneNavigationRequested(new Intent(context, ClientDeprecatedActivity.class))) - .build(); + .disableSnooze() + .setOnVisibleListener((megaphone, listener) -> listener.onMegaphoneNavigationRequested(new Intent(context, ClientDeprecatedActivity.class))) + .build(); } private static @NonNull Megaphone buildOnboardingMegaphone() { return new Megaphone.Builder(Event.ONBOARDING, Megaphone.Style.ONBOARDING) - .build(); + .build(); } private static @NonNull Megaphone buildNotificationsMegaphone(@NonNull Context context) { return new Megaphone.Builder(Event.NOTIFICATIONS, Megaphone.Style.BASIC) - .setTitle(R.string.NotificationsMegaphone_turn_on_notifications) - .setBody(R.string.NotificationsMegaphone_never_miss_a_message) - .setImage(R.drawable.megaphone_notifications_64) - .setActionButton(R.string.NotificationsMegaphone_turn_on, (megaphone, controller) -> { - if (Build.VERSION.SDK_INT >= 26 && !NotificationChannels.getInstance().isMessageChannelEnabled()) { - Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); - intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationChannels.getInstance().getMessagesChannel()); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); - controller.onMegaphoneNavigationRequested(intent); - } else if (Build.VERSION.SDK_INT >= 26 && - (!NotificationChannels.getInstance().areNotificationsEnabled() || !NotificationChannels.getInstance().isMessagesChannelGroupEnabled())) - { - Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); - controller.onMegaphoneNavigationRequested(intent); - } else { - controller.onMegaphoneNavigationRequested(AppSettingsActivity.notifications(context)); - } - }) - .setSecondaryButton(R.string.NotificationsMegaphone_not_now, (megaphone, controller) -> controller.onMegaphoneSnooze(Event.NOTIFICATIONS)) - .build(); + .setTitle(R.string.NotificationsMegaphone_turn_on_notifications) + .setBody(R.string.NotificationsMegaphone_never_miss_a_message) + .setImage(R.drawable.megaphone_notifications_64) + .setActionButton(R.string.NotificationsMegaphone_turn_on, (megaphone, controller) -> { + if (Build.VERSION.SDK_INT >= 26 && !NotificationChannels.getInstance().isMessageChannelEnabled()) { + Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationChannels.getInstance().getMessagesChannel()); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); + controller.onMegaphoneNavigationRequested(intent); + } else if (Build.VERSION.SDK_INT >= 26 && + (!NotificationChannels.getInstance().areNotificationsEnabled() || !NotificationChannels.getInstance().isMessagesChannelGroupEnabled())) + { + Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); + controller.onMegaphoneNavigationRequested(intent); + } else { + controller.onMegaphoneNavigationRequested(AppSettingsActivity.notifications(context)); + } + }) + .setSecondaryButton(R.string.NotificationsMegaphone_not_now, (megaphone, controller) -> controller.onMegaphoneSnooze(Event.NOTIFICATIONS)) + .build(); } private static @NonNull Megaphone buildAddAProfilePhotoMegaphone(@NonNull Context context) { return new Megaphone.Builder(Event.ADD_A_PROFILE_PHOTO, Megaphone.Style.BASIC) - .setTitle(R.string.AddAProfilePhotoMegaphone__add_a_profile_photo) - .setImage(R.drawable.ic_add_a_profile_megaphone_image) - .setBody(R.string.AddAProfilePhotoMegaphone__choose_a_look_and_color) - .setActionButton(R.string.AddAProfilePhotoMegaphone__add_photo, (megaphone, listener) -> { - listener.onMegaphoneNavigationRequested(ManageProfileActivity.getIntentForAvatarEdit(context)); - listener.onMegaphoneCompleted(Event.ADD_A_PROFILE_PHOTO); - }) - .setSecondaryButton(R.string.AddAProfilePhotoMegaphone__not_now, (megaphone, listener) -> { - listener.onMegaphoneCompleted(Event.ADD_A_PROFILE_PHOTO); - }) - .build(); + .setTitle(R.string.AddAProfilePhotoMegaphone__add_a_profile_photo) + .setImage(R.drawable.ic_add_a_profile_megaphone_image) + .setBody(R.string.AddAProfilePhotoMegaphone__choose_a_look_and_color) + .setActionButton(R.string.AddAProfilePhotoMegaphone__add_photo, (megaphone, listener) -> { + listener.onMegaphoneNavigationRequested(ManageProfileActivity.getIntentForAvatarEdit(context)); + listener.onMegaphoneCompleted(Event.ADD_A_PROFILE_PHOTO); + }) + .setSecondaryButton(R.string.AddAProfilePhotoMegaphone__not_now, (megaphone, listener) -> { + listener.onMegaphoneCompleted(Event.ADD_A_PROFILE_PHOTO); + }) + .build(); } private static @NonNull Megaphone buildBecomeASustainerMegaphone(@NonNull Context context) { @@ -391,6 +397,19 @@ public final class Megaphones { } } + public static @NonNull Megaphone buildSetUpYourUsernameMegaphone(@NonNull Context context) { + return new Megaphone.Builder(Event.SET_UP_YOUR_USERNAME, Megaphone.Style.BASIC) + .setTitle(R.string.SetUpYourUsername__set_up_your_signal_username) + .setBody(R.string.SetUpYourUsername__usernames_let_others) + .setActionButton(R.string.SetUpYourUsername__continue, (megaphone, controller) -> { + controller.onMegaphoneNavigationRequested(ManageProfileActivity.getIntentForUsernameEdit(context)); + }) + .setSecondaryButton(R.string.SetUpYourUsername__not_now, (megaphone, controller) -> { + controller.onMegaphoneCompleted(Event.SET_UP_YOUR_USERNAME); + }) + .build(); + } + private static boolean shouldShowDonateMegaphone(@NonNull Context context, @NonNull Event event, @NonNull Map records) { long timeSinceLastDonatePrompt = timeSinceLastDonatePrompt(event, records); @@ -422,10 +441,10 @@ public final class Megaphones { if (shouldShow) { Locale locale = DynamicLanguageContextWrapper.getUsersSelectedLocale(context); if (!new TranslationDetection(context, locale) - .textExistsInUsersLanguage(R.string.NotificationsMegaphone_turn_on_notifications, - R.string.NotificationsMegaphone_never_miss_a_message, - R.string.NotificationsMegaphone_turn_on, - R.string.NotificationsMegaphone_not_now)) + .textExistsInUsersLanguage(R.string.NotificationsMegaphone_turn_on_notifications, + R.string.NotificationsMegaphone_never_miss_a_message, + R.string.NotificationsMegaphone_turn_on, + R.string.NotificationsMegaphone_not_now)) { Log.i(TAG, "Would show NotificationsMegaphone but is not yet translated in " + locale); return false; @@ -448,6 +467,23 @@ public final class Megaphones { return true; } + /** + * Prompt megaphone 3 days after turning off phone number discovery when no username is set. + */ + private static boolean shouldShowSetUpYourUsernameMegaphone(@NonNull Map records) { + boolean hasUsername = SignalStore.account().isRegistered() && Recipient.self().getUsername().isPresent(); + boolean hasCompleted = MapUtil.mapOrDefault(records, Event.SET_UP_YOUR_USERNAME, MegaphoneRecord::isFinished, false); + long phoneNumberDiscoveryDisabledAt = SignalStore.phoneNumberPrivacy().getPhoneNumberListingModeTimestamp(); + PhoneNumberPrivacyValues.PhoneNumberListingMode listingMode = SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode(); + + return FeatureFlags.usernames() && + !hasUsername && + listingMode.isUnlisted() && + !hasCompleted && + phoneNumberDiscoveryDisabledAt > 0 && + (System.currentTimeMillis() - phoneNumberDiscoveryDisabledAt) >= TimeUnit.DAYS.toMillis(3); + } + @WorkerThread private static boolean shouldShowRemoteMegaphone(@NonNull Map records) { boolean canShowLocalDonate = timeSinceLastDonatePrompt(Event.REMOTE_MEGAPHONE, records) > MIN_TIME_BETWEEN_DONATE_MEGAPHONES; @@ -488,7 +524,8 @@ public final class Megaphones { TURN_OFF_CENSORSHIP_CIRCUMVENTION("turn_off_censorship_circumvention"), REMOTE_MEGAPHONE("remote_megaphone"), BACKUP_SCHEDULE_PERMISSION("backup_schedule_permission"), - SMS_EXPORT("sms_export"); + SMS_EXPORT("sms_export"), + SET_UP_YOUR_USERNAME("set_up_your_username"); private final String key; diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java index f96a978236..23317c29bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java @@ -13,6 +13,7 @@ import androidx.navigation.fragment.NavHostFragment; import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicTheme; @@ -63,8 +64,13 @@ public class ManageProfileActivity extends PassphraseRequiredActivity implements navController.setGraph(graph, extras != null ? extras : new Bundle()); if (extras != null && extras.getBoolean(START_AT_USERNAME, false)) { - NavDirections action = ManageProfileFragmentDirections.actionManageUsername(); - SafeNavigation.safeNavigate(navController, action); + if (SignalStore.uiHints().hasSeenUsernameEducation()) { + NavDirections action = ManageProfileFragmentDirections.actionManageUsername(); + SafeNavigation.safeNavigate(navController, action); + } else { + NavDirections action = ManageProfileFragmentDirections.actionManageProfileFragmentToUsernameEducationFragment(); + SafeNavigation.safeNavigate(navController, action); + } } if (extras != null && extras.getBoolean(START_AT_AVATAR, false)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java index c462be7cdb..08b0a25517 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java @@ -20,6 +20,7 @@ import androidx.navigation.Navigation; import com.airbnb.lottie.SimpleColorFilter; import com.bumptech.glide.Glide; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.signal.core.util.logging.Log; @@ -33,11 +34,12 @@ import org.thoughtcrime.securesms.badges.self.none.BecomeASustainerFragment; import org.thoughtcrime.securesms.components.emoji.EmojiTextView; import org.thoughtcrime.securesms.components.emoji.EmojiUtil; import org.thoughtcrime.securesms.databinding.ManageProfileFragmentBinding; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarState; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.NameUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.util.navigation.SafeNavigation; @@ -49,6 +51,8 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Optional; +import io.reactivex.rxjava3.disposables.Disposable; + public class ManageProfileFragment extends LoggingFragment { private static final String TAG = Log.tag(ManageProfileFragment.class); @@ -56,6 +60,7 @@ public class ManageProfileFragment extends LoggingFragment { private AlertDialog avatarProgress; private ManageProfileViewModel viewModel; private ManageProfileFragmentBinding binding; + private LifecycleDisposable disposables; @Override public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -66,6 +71,9 @@ public class ManageProfileFragment extends LoggingFragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + disposables = new LifecycleDisposable(); + disposables.bindTo(getViewLifecycleOwner()); + new UsernameEditFragment.ResultContract().registerForResult(getParentFragmentManager(), getViewLifecycleOwner(), isUsernameCreated -> { Snackbar.make(view, R.string.ManageProfileFragment__username_created, Snackbar.LENGTH_SHORT).show(); }); @@ -85,7 +93,28 @@ public class ManageProfileFragment extends LoggingFragment { }); binding.manageProfileUsernameContainer.setOnClickListener(v -> { - SafeNavigation.safeNavigate(Navigation.findNavController(v), ManageProfileFragmentDirections.actionManageUsername()); + if (SignalStore.uiHints().hasSeenUsernameEducation()) { + if (Recipient.self().getUsername().isPresent()) { + new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Signal_MaterialAlertDialog_List) + .setItems(R.array.username_edit_entries, (d, w) -> { + switch (w) { + case 0: + SafeNavigation.safeNavigate(Navigation.findNavController(v), ManageProfileFragmentDirections.actionManageUsername()); + break; + case 1: + displayConfirmUsernameDeletionDialog(); + break; + default: + throw new IllegalStateException(); + } + }) + .show(); + } else { + SafeNavigation.safeNavigate(Navigation.findNavController(v), ManageProfileFragmentDirections.actionManageUsername()); + } + } else { + SafeNavigation.safeNavigate(Navigation.findNavController(v), ManageProfileFragmentDirections.actionManageProfileFragmentToUsernameEducationFragment()); + } }); binding.manageProfileAboutContainer.setOnClickListener(v -> { @@ -272,4 +301,29 @@ public class ManageProfileFragment extends LoggingFragment { private void onEditAvatarClicked() { SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), ManageProfileFragmentDirections.actionManageProfileFragmentToAvatarPicker(null, null)); } + + private void displayConfirmUsernameDeletionDialog() { + new MaterialAlertDialogBuilder(requireContext()) + .setTitle("Delete Username?") // TODO [alex] -- Final copy + .setMessage("This will remove your username, allowing other users to claim it. Are you sure?") // TODO [alex] -- Final copy + .setPositiveButton(R.string.delete, (d, w) -> { + onUserConfirmedUsernameDeletion(); + }) + .setNegativeButton(android.R.string.cancel, (d, w) -> {}) + .show(); + } + + private void onUserConfirmedUsernameDeletion() { + binding.progressCard.setVisibility(View.VISIBLE); + Disposable disposable = viewModel.deleteUsername() + .subscribe(result -> { + binding.progressCard.setVisibility(View.GONE); + handleUsernameDeletionResult(result); + }); + disposables.add(disposable); + } + + private void handleUsernameDeletionResult(@NonNull UsernameEditRepository.UsernameDeleteResult usernameDeleteResult) { + // TODO [alex] -- Snackbar? + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java index 241dd35e3a..2152b08149 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java @@ -34,6 +34,9 @@ import java.util.Arrays; import java.util.Objects; import java.util.Optional; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; + class ManageProfileViewModel extends ViewModel { private static final String TAG = Log.tag(ManageProfileViewModel.class); @@ -46,22 +49,24 @@ class ManageProfileViewModel extends ViewModel { private final LiveData avatarState; private final SingleLiveEvent events; private final RecipientForeverObserver observer; - private final ManageProfileRepository repository; - private final MutableLiveData> badge; + private final ManageProfileRepository repository; + private final UsernameEditRepository usernameEditRepository; + private final MutableLiveData> badge; private byte[] previousAvatar; public ManageProfileViewModel() { - this.internalAvatarState = new MutableLiveData<>(); - this.profileName = new MutableLiveData<>(); - this.username = new MutableLiveData<>(); - this.about = new MutableLiveData<>(); - this.aboutEmoji = new MutableLiveData<>(); - this.events = new SingleLiveEvent<>(); - this.repository = new ManageProfileRepository(); - this.badge = new DefaultValueLiveData<>(Optional.empty()); - this.observer = this::onRecipientChanged; - this.avatarState = LiveDataUtil.combineLatest(Recipient.self().live().getLiveData(), internalAvatarState, (self, state) -> new AvatarState(state, self)); + this.internalAvatarState = new MutableLiveData<>(); + this.profileName = new MutableLiveData<>(); + this.username = new MutableLiveData<>(); + this.about = new MutableLiveData<>(); + this.aboutEmoji = new MutableLiveData<>(); + this.events = new SingleLiveEvent<>(); + this.repository = new ManageProfileRepository(); + this.usernameEditRepository = new UsernameEditRepository(); + this.badge = new DefaultValueLiveData<>(Optional.empty()); + this.observer = this::onRecipientChanged; + this.avatarState = LiveDataUtil.combineLatest(Recipient.self().live().getLiveData(), internalAvatarState, (self, state) -> new AvatarState(state, self)); SignalExecutors.BOUNDED.execute(() -> { onRecipientChanged(Recipient.self().fresh()); @@ -99,6 +104,10 @@ class ManageProfileViewModel extends ViewModel { return events; } + public Single deleteUsername() { + return usernameEditRepository.deleteUsername().observeOn(AndroidSchedulers.mainThread()); + } + public boolean shouldShowUsername() { return FeatureFlags.usernames(); } @@ -271,7 +280,7 @@ class ManageProfileViewModel extends ViewModel { static class Factory extends ViewModelProvider.NewInstanceFactory { @Override - public @NonNull T create(@NonNull Class modelClass) { + public @NonNull T create(@NonNull Class modelClass) { return Objects.requireNonNull(modelClass.cast(new ManageProfileViewModel())); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java index 2a80804363..c567e6622f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import org.signal.core.util.Result; -import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -17,7 +16,6 @@ import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenExcepti import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse; import java.io.IOException; -import java.util.concurrent.Executor; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEducationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEducationFragment.kt new file mode 100644 index 0000000000..8532f69e33 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEducationFragment.kt @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.profiles.manage + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.databinding.UsernameEducationFragmentBinding +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.navigation.safeNavigate + +/** + * Displays a Username education screen which displays some basic information + * about usernames and provides a learn-more link. + */ +class UsernameEducationFragment : Fragment(R.layout.username_education_fragment) { + private val binding by ViewBinderDelegate(UsernameEducationFragmentBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.toolbar.setNavigationOnClickListener { + findNavController().popBackStack() + } + + binding.usernameEducationLearnMore.setOnClickListener { + // TODO [alex] -- Launch "Learn More" page. + } + + binding.continueButton.setOnClickListener { + SignalStore.uiHints().markHasSeenUsernameEducation() + findNavController().safeNavigate(UsernameEducationFragmentDirections.actionUsernameEducationFragmentToUsernameManageFragment()) + } + } +} diff --git a/app/src/main/res/layout/manage_profile_fragment.xml b/app/src/main/res/layout/manage_profile_fragment.xml index 4a67249225..22489f7f1d 100644 --- a/app/src/main/res/layout/manage_profile_fragment.xml +++ b/app/src/main/res/layout/manage_profile_fragment.xml @@ -1,310 +1,324 @@ - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:fillViewport="true"> + android:layout_height="wrap_content"> - + app:layout_constraintTop_toTopOf="parent" + app:navigationIcon="@drawable/ic_arrow_left_24" + app:title="@string/CreateProfileActivity__profile" /> + + + + - - - - - - + android:layout_height="0dp" + android:fontFamily="sans-serif-medium" + android:gravity="center" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/manage_profile_avatar_background" + app:layout_constraintEnd_toEndOf="@id/manage_profile_avatar_background" + app:layout_constraintStart_toStartOf="@id/manage_profile_avatar_background" + app:layout_constraintTop_toTopOf="@id/manage_profile_avatar_background" + tools:ignore="SpUsage" + tools:text="AF" + tools:visibility="visible" /> + + + + + app:layout_constraintTop_toBottomOf="@id/manage_profile_avatar_background" /> - - - - - + android:paddingStart="@dimen/dsl_settings_gutter" + android:paddingTop="16dp" + android:paddingEnd="@dimen/dsl_settings_gutter" + android:paddingBottom="16dp" + app:layout_constraintTop_toBottomOf="@id/manage_profile_edit_photo"> - + - + - + - + + + android:background="?selectableItemBackground" + android:minHeight="72dp" + android:paddingStart="@dimen/dsl_settings_gutter" + android:paddingEnd="@dimen/safety_number_recipient_row_item_gutter" + app:layout_constraintTop_toBottomOf="@id/manage_profile_name_container"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + app:layout_constraintTop_toBottomOf="@+id/manage_profile_badges_container" + app:layout_constraintVertical_bias="1.0" /> - + - + - + diff --git a/app/src/main/res/layout/username_education_fragment.xml b/app/src/main/res/layout/username_education_fragment.xml new file mode 100644 index 0000000000..8496863bbe --- /dev/null +++ b/app/src/main/res/layout/username_education_fragment.xml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/manage_profile.xml b/app/src/main/res/navigation/manage_profile.xml index 39d2390a11..ecf348b6a5 100644 --- a/app/src/main/res/navigation/manage_profile.xml +++ b/app/src/main/res/navigation/manage_profile.xml @@ -65,6 +65,9 @@ + @@ -74,6 +77,17 @@ android:label="fragment_manage_username" tools:layout="@layout/username_edit_fragment" /> + + + + + + @string/UsernameEditDialog__edit_username + @string/UsernameEditDialog__delete_username + @string/preferences__system_default diff --git a/app/src/main/res/values/signal_styles.xml b/app/src/main/res/values/signal_styles.xml index ae36222ac3..5795fdb12f 100644 --- a/app/src/main/res/values/signal_styles.xml +++ b/app/src/main/res/values/signal_styles.xml @@ -234,6 +234,11 @@ @style/Signal.MaterialAlertDialog.Wide + +