mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
Add support for an 'About' field on your profile.
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user