Add username education screen.

This commit is contained in:
Alex Hart
2023-02-01 13:39:53 -04:00
committed by Greyson Parrelli
parent dae69744c2
commit 4f387cf8d9
16 changed files with 774 additions and 386 deletions

View File

@@ -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"))

View File

@@ -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<CertificateType> REGULAR_CERTIFICATE = Collections.singletonList(CertificateType.UUID_AND_E164);
private static final Collection<CertificateType> PRIVACY_CERTIFICATE = Collections.singletonList(CertificateType.UUID_ONLY);
@@ -32,7 +33,7 @@ public final class PhoneNumberPrivacyValues extends SignalStoreValues {
@Override
@NonNull List<String> 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);
}
/**

View File

@@ -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<String> 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);
}

View File

@@ -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)}
*
* <p>
* 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<Megaphone> 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.
*
* <p>
* This is also when you would hide certain megaphones based on things like {@link FeatureFlags}.
*/
private static Map<Event, MegaphoneSchedule> buildDisplayOrder(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> 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<Event, MegaphoneRecord> 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<Event, MegaphoneRecord> 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<Event, MegaphoneRecord> 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;

View File

@@ -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)) {

View File

@@ -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?
}
}

View File

@@ -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> avatarState;
private final SingleLiveEvent<Event> events;
private final RecipientForeverObserver observer;
private final ManageProfileRepository repository;
private final MutableLiveData<Optional<Badge>> badge;
private final ManageProfileRepository repository;
private final UsernameEditRepository usernameEditRepository;
private final MutableLiveData<Optional<Badge>> 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<UsernameEditRepository.UsernameDeleteResult> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return Objects.requireNonNull(modelClass.cast(new ManageProfileViewModel()));
}
}

View File

@@ -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;

View File

@@ -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())
}
}
}