Add support for an 'About' field on your profile.

This commit is contained in:
Greyson Parrelli
2021-01-21 12:35:00 -05:00
parent e80033c287
commit 7db16e6156
42 changed files with 709 additions and 119 deletions

View File

@@ -0,0 +1,132 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import org.signal.core.util.BreakIteratorCompat;
import org.signal.core.util.EditTextUtil;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
/**
* Let's you edit the 'About' section of your profile.
*/
public class EditAboutFragment extends Fragment implements ManageProfileActivity.EmojiController {
public static final int ABOUT_MAX_GLYPHS = 100;
public static final int ABOUT_LIMIT_DISPLAY_THRESHOLD = 75;
private static final String KEY_SELECTED_EMOJI = "selected_emoji";
private ImageView emojiView;
private EditText bodyView;
private TextView countView;
private String selectedEmoji;
@Override
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.edit_about_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
this.emojiView = view.findViewById(R.id.edit_about_emoji);
this.bodyView = view.findViewById(R.id.edit_about_body);
this.countView = view.findViewById(R.id.edit_about_count);
view.<Toolbar>findViewById(R.id.toolbar)
.setNavigationOnClickListener(v -> Navigation.findNavController(view)
.popBackStack());
EditTextUtil.addGraphemeClusterLimitFilter(bodyView, ABOUT_MAX_GLYPHS);
this.bodyView.addTextChangedListener(new AfterTextChanged(editable -> {
trimFieldToMaxByteLength(editable);
presentCount(editable.toString());
}));
this.emojiView.setOnClickListener(v -> {
ReactWithAnyEmojiBottomSheetDialogFragment.createForAboutSelection()
.show(requireFragmentManager(), "BOTTOM");
});
view.findViewById(R.id.edit_about_save).setOnClickListener(this::onSaveClicked);
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SELECTED_EMOJI)) {
onEmojiSelected(savedInstanceState.getString(KEY_SELECTED_EMOJI, ""));
} else {
this.bodyView.setText(Recipient.self().getAbout());
onEmojiSelected(Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""));
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putString(KEY_SELECTED_EMOJI, selectedEmoji);
}
@Override
public void onEmojiSelected(@NonNull String emoji) {
Drawable drawable = EmojiUtil.convertToDrawable(requireContext(), emoji);
if (drawable != null) {
this.emojiView.setImageDrawable(drawable);
this.selectedEmoji = emoji;
}
}
private void presentCount(@NonNull String aboutBody) {
BreakIteratorCompat breakIterator = BreakIteratorCompat.getInstance();
breakIterator.setText(aboutBody);
int glyphCount = breakIterator.countBreaks();
if (glyphCount >= ABOUT_LIMIT_DISPLAY_THRESHOLD) {
this.countView.setVisibility(View.VISIBLE);
this.countView.setText(getResources().getString(R.string.EditAboutFragment_count, glyphCount, ABOUT_MAX_GLYPHS));
} else {
this.countView.setVisibility(View.GONE);
}
}
private void onSaveClicked(View view) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
DatabaseFactory.getRecipientDatabase(requireContext()).setAbout(Recipient.self().getId(), bodyView.getText().toString(), selectedEmoji);
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
return null;
}, (nothing) -> {
Navigation.findNavController(view).popBackStack();
});
}
public static void trimFieldToMaxByteLength(Editable s) {
int trimmedLength = StringUtil.trimToFit(s.toString(), ProfileCipher.MAX_POSSIBLE_ABOUT_LENGTH).length();
if (s.length() > trimmedLength) {
s.delete(trimmedLength, s.length());
}
}
}

View File

@@ -5,21 +5,24 @@ import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.navigation.NavDirections;
import androidx.navigation.NavGraph;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import org.thoughtcrime.securesms.BaseActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.profiles.edit.EditProfileFragmentDirections;
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
/**
* Activity that manages the local user's profile, as accessed via the settings.
*/
public class ManageProfileActivity extends BaseActivity {
public class ManageProfileActivity extends BaseActivity implements ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
@@ -61,4 +64,26 @@ public class ManageProfileActivity extends BaseActivity {
super.onResume();
dynamicTheme.onResume(this);
}
@Override
public void onReactWithAnyEmojiDialogDismissed() {
}
@Override
public void onReactWithAnyEmojiPageChanged(int page) {
}
@Override
public void onReactWithAnyEmojiSelected(@NonNull String emoji) {
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().getPrimaryNavigationFragment();
Fragment activeFragment = navHostFragment.getChildFragmentManager().getPrimaryNavigationFragment();
if (activeFragment instanceof EmojiController) {
((EmojiController) activeFragment).onEmojiSelected(emoji);
}
}
interface EmojiController {
void onEmojiSelected(@NonNull String emoji);
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -13,6 +14,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.res.ResourcesCompat;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
@@ -21,6 +23,7 @@ import com.bumptech.glide.Glide;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
import org.thoughtcrime.securesms.mediasend.Media;
@@ -44,7 +47,7 @@ public class ManageProfileFragment extends LoggingFragment {
private View usernameContainer;
private TextView aboutView;
private View aboutContainer;
private TextView aboutEmojiView;
private ImageView aboutEmojiView;
private AlertDialog avatarProgress;
private ManageProfileViewModel viewModel;
@@ -65,6 +68,7 @@ public class ManageProfileFragment extends LoggingFragment {
this.usernameContainer = view.findViewById(R.id.manage_profile_username_container);
this.aboutView = view.findViewById(R.id.manage_profile_about);
this.aboutContainer = view.findViewById(R.id.manage_profile_about_container);
this.aboutEmojiView = view.findViewById(R.id.manage_profile_about_icon);
initializeViewModel();
@@ -78,6 +82,10 @@ public class ManageProfileFragment extends LoggingFragment {
this.usernameContainer.setOnClickListener(v -> {
Navigation.findNavController(v).navigate(ManageProfileFragmentDirections.actionManageUsername());
});
this.aboutContainer.setOnClickListener(v -> {
Navigation.findNavController(v).navigate(ManageProfileFragmentDirections.actionManageAbout());
});
}
@Override
@@ -102,13 +110,8 @@ public class ManageProfileFragment extends LoggingFragment {
viewModel.getAvatar().observe(getViewLifecycleOwner(), this::presentAvatar);
viewModel.getProfileName().observe(getViewLifecycleOwner(), this::presentProfileName);
viewModel.getEvents().observe(getViewLifecycleOwner(), this::presentEvent);
if (viewModel.shouldShowAbout()) {
viewModel.getAbout().observe(getViewLifecycleOwner(), this::presentAbout);
viewModel.getAboutEmoji().observe(getViewLifecycleOwner(), this::presentAboutEmoji);
} else {
aboutContainer.setVisibility(View.GONE);
}
viewModel.getAbout().observe(getViewLifecycleOwner(), this::presentAbout);
viewModel.getAboutEmoji().observe(getViewLifecycleOwner(), this::presentAboutEmoji);
if (viewModel.shouldShowUsername()) {
viewModel.getUsername().observe(getViewLifecycleOwner(), this::presentUsername);
@@ -156,14 +159,26 @@ public class ManageProfileFragment extends LoggingFragment {
private void presentAbout(@Nullable String about) {
if (about == null || about.isEmpty()) {
aboutView.setHint(R.string.ManageProfileFragment_about);
aboutView.setText(R.string.ManageProfileFragment_about);
aboutView.setTextColor(requireContext().getResources().getColor(R.color.signal_text_secondary));
} else {
aboutView.setText(about);
aboutView.setTextColor(requireContext().getResources().getColor(R.color.signal_text_primary));
}
}
private void presentAboutEmoji(@NonNull String aboutEmoji) {
if (aboutEmoji == null || aboutEmoji.isEmpty()) {
aboutEmojiView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_compose_24, null));
} else {
Drawable emoji = EmojiUtil.convertToDrawable(requireContext(), aboutEmoji);
if (emoji != null) {
aboutEmojiView.setImageDrawable(emoji);
} else {
aboutEmojiView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_compose_24, null));
}
}
}
private void presentEvent(@NonNull ManageProfileViewModel.Event event) {

View File

@@ -100,10 +100,6 @@ class ManageProfileViewModel extends ViewModel {
return FeatureFlags.usernames();
}
public boolean shouldShowAbout() {
return FeatureFlags.about();
}
public void onAvatarSelected(@NonNull Context context, @Nullable Media media) {
if (media == null) {
SignalExecutors.BOUNDED.execute(() -> {
@@ -136,6 +132,8 @@ class ManageProfileViewModel extends ViewModel {
private void onRecipientChanged(@NonNull Recipient recipient) {
profileName.postValue(recipient.getProfileName());
username.postValue(recipient.getUsername().orNull());
about.postValue(recipient.getAbout());
aboutEmoji.postValue(recipient.getAboutEmoji());
}
@Override