From 7db16e615672e58ebeea8d423df25b54d97b6111 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 21 Jan 2021 12:35:00 -0500 Subject: [PATCH] Add support for an 'About' field on your profile. --- .../emoji/EmojiKeyboardProvider.java | 2 +- .../securesms/components/emoji/EmojiUtil.java | 20 ++- .../securesms/contacts/ContactRepository.java | 37 +++-- .../contacts/ContactSelectionListAdapter.java | 34 ++--- .../contacts/ContactSelectionListItem.java | 10 +- .../contacts/ContactsCursorLoader.java | 33 +++-- .../conversation/ConversationActivity.java | 10 +- .../conversation/ConversationBannerView.java | 7 + .../conversation/ConversationFragment.java | 3 +- .../securesms/database/RecipientDatabase.java | 42 +++++- .../database/helpers/SQLCipherOpenHelper.java | 8 +- .../groups/ui/GroupMemberListAdapter.java | 15 +- .../securesms/jobs/ProfileUploadJob.java | 5 +- .../securesms/jobs/RefreshOwnProfileJob.java | 13 ++ .../securesms/jobs/RetrieveProfileJob.java | 15 ++ .../profiles/manage/EditAboutFragment.java | 132 ++++++++++++++++++ .../manage/ManageProfileActivity.java | 27 +++- .../manage/ManageProfileFragment.java | 33 +++-- .../manage/ManageProfileViewModel.java | 6 +- ...WithAnyEmojiBottomSheetDialogFragment.java | 48 +++++-- .../any/ReactWithAnyEmojiRepository.java | 10 +- .../any/ReactWithAnyEmojiViewModel.java | 6 +- .../securesms/recipients/Recipient.java | 32 ++++- .../recipients/RecipientDetails.java | 6 + .../RecipientBottomSheetDialogFragment.java | 11 ++ .../ManageRecipientFragment.java | 7 + .../securesms/util/FeatureFlags.java | 8 -- app/src/main/res/drawable/ic_add_emoji.xml | 9 ++ .../layout/contact_selection_list_item.xml | 3 +- .../res/layout/conversation_banner_view.xml | 13 +- .../main/res/layout/edit_about_fragment.xml | 70 ++++++++++ .../res/layout/group_recipient_list_item.xml | 27 +++- .../res/layout/manage_profile_fragment.xml | 8 +- .../res/layout/recipient_bottom_sheet.xml | 12 +- .../res/layout/recipient_manage_fragment.xml | 11 ++ .../main/res/navigation/manage_profile.xml | 30 ++-- app/src/main/res/values/strings.xml | 5 + app/src/main/res/values/themes.xml | 10 +- .../api/SignalServiceAccountManager.java | 7 +- .../api/crypto/ProfileCipher.java | 19 ++- .../api/profiles/SignalServiceProfile.java | 14 ++ .../profiles/SignalServiceProfileWrite.java | 10 +- 42 files changed, 709 insertions(+), 119 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java create mode 100644 app/src/main/res/drawable/ic_add_emoji.xml create mode 100644 app/src/main/res/layout/edit_about_fragment.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiKeyboardProvider.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiKeyboardProvider.java index 5c11d5f18d..e1c22d2ce2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiKeyboardProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiKeyboardProvider.java @@ -30,7 +30,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider, { private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL); - private static final String RECENT_STORAGE_KEY = "pref_recent_emoji2"; + public static final String RECENT_STORAGE_KEY = "pref_recent_emoji2"; private final Context context; private final List models; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java index 97a583a8ab..ec8e7d2edf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java @@ -1,7 +1,14 @@ package org.thoughtcrime.securesms.components.emoji; -import androidx.annotation.NonNull; +import android.content.Context; +import android.graphics.drawable.Drawable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser; +import org.thoughtcrime.securesms.util.StringUtil; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.Pair; import java.util.HashMap; @@ -72,4 +79,15 @@ public final class EmojiUtil { String canonical = VARIATION_MAP.get(emoji); return canonical != null ? canonical : emoji; } + + /** + * Converts the provided emoji string into a single drawable, if possible. + */ + public static @Nullable Drawable convertToDrawable(@NonNull Context context, @Nullable String emoji) { + if (Util.isEmpty(emoji)) { + return null; + } else { + return EmojiProvider.getInstance(context).getEmojiDrawable(emoji); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java index 038b47a690..5510504ddf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java @@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.Pair; @@ -42,6 +43,7 @@ public class ContactRepository { static final String NUMBER_TYPE_COLUMN = "number_type"; static final String LABEL_COLUMN = "label"; static final String CONTACT_TYPE_COLUMN = "contact_type"; + static final String ABOUT_COLUMN = "about"; static final int NORMAL_TYPE = 0; static final int PUSH_TYPE = 1; @@ -52,18 +54,18 @@ public class ContactRepository { /** Maps the recipient results to the legacy contact column names */ private static final List> SEARCH_CURSOR_MAPPERS = new ArrayList>() {{ - add(new Pair<>(ID_COLUMN, cursor -> cursor.getLong(cursor.getColumnIndexOrThrow(RecipientDatabase.ID)))); + add(new Pair<>(ID_COLUMN, cursor -> CursorUtil.requireLong(cursor, RecipientDatabase.ID))); add(new Pair<>(NAME_COLUMN, cursor -> { - String system = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_DISPLAY_NAME)); - String profile = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SEARCH_PROFILE_NAME)); + String system = CursorUtil.requireString(cursor, RecipientDatabase.SYSTEM_DISPLAY_NAME); + String profile = CursorUtil.requireString(cursor, RecipientDatabase.SEARCH_PROFILE_NAME); return Util.getFirstNonEmpty(system, profile); })); add(new Pair<>(NUMBER_COLUMN, cursor -> { - String phone = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.PHONE)); - String email = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.EMAIL)); + String phone = CursorUtil.requireString(cursor, RecipientDatabase.PHONE); + String email = CursorUtil.requireString(cursor, RecipientDatabase.EMAIL); if (phone != null) { phone = PhoneNumberFormatter.prettyPrint(phone); @@ -72,14 +74,31 @@ public class ContactRepository { return Util.getFirstNonEmpty(phone, email); })); - add(new Pair<>(NUMBER_TYPE_COLUMN, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_PHONE_TYPE)))); + add(new Pair<>(NUMBER_TYPE_COLUMN, cursor -> CursorUtil.requireInt(cursor, RecipientDatabase.SYSTEM_PHONE_TYPE))); - add(new Pair<>(LABEL_COLUMN, cursor -> cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_PHONE_LABEL)))); + add(new Pair<>(LABEL_COLUMN, cursor -> CursorUtil.requireString(cursor, RecipientDatabase.SYSTEM_PHONE_LABEL))); add(new Pair<>(CONTACT_TYPE_COLUMN, cursor -> { - int registered = cursor.getInt(cursor.getColumnIndexOrThrow(RecipientDatabase.REGISTERED)); + int registered = CursorUtil.requireInt(cursor, RecipientDatabase.REGISTERED); return registered == RecipientDatabase.RegisteredState.REGISTERED.getId() ? PUSH_TYPE : NORMAL_TYPE; })); + + add(new Pair<>(ABOUT_COLUMN, cursor -> { + String aboutEmoji = CursorUtil.requireString(cursor, RecipientDatabase.ABOUT_EMOJI); + String about = CursorUtil.requireString(cursor, RecipientDatabase.ABOUT); + + if (aboutEmoji != null) { + if (about != null) { + return aboutEmoji + " " + about; + } else { + return aboutEmoji; + } + } else if (about != null) { + return about; + } else { + return ""; + } + })); }}; public ContactRepository(@NonNull Context context) { @@ -106,7 +125,7 @@ public class ContactRepository { if (shouldAdd) { MatrixCursor selfCursor = new MatrixCursor(RecipientDatabase.SEARCH_PROJECTION_NAMES); - selfCursor.addRow(new Object[]{ self.getId().serialize(), noteToSelfTitle, null, self.getE164().or(""), self.getEmail().orNull(), null, -1, RecipientDatabase.RegisteredState.REGISTERED.getId(), noteToSelfTitle }); + selfCursor.addRow(new Object[]{ self.getId().serialize(), noteToSelfTitle, self.getE164().or(""), self.getEmail().orNull(), null, -1, RecipientDatabase.RegisteredState.REGISTERED.getId(), self.getAbout(), self.getAboutEmoji(), noteToSelfTitle, noteToSelfTitle }); cursor = cursor == null ? selfCursor : new MergeCursor(new Cursor[]{ cursor, selfCursor }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java index dd31237b5a..1971b5ee2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java @@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolde import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter; import org.thoughtcrime.securesms.util.Util; @@ -97,7 +98,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter " + (System.currentTimeMillis() - startMillis) + "ms"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index f200108db9..b814d8991d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -395,8 +395,8 @@ public class ConversationActivity extends PassphraseRequiredActivity private LiveRecipient recipient; private long threadId; private int distributionType; - private int reactWithAnyEmojiStartPage; private boolean isSecureText; + private int reactWithAnyEmojiStartPage = -1; private boolean isDefaultSms = true; private boolean isMmsEnabled = true; private boolean isSecurityInitialized = false; @@ -488,7 +488,7 @@ public class ConversationActivity extends PassphraseRequiredActivity return; } - reactWithAnyEmojiStartPage = 0; + reactWithAnyEmojiStartPage = -1; if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent() || inputPanel.getQuote().isPresent()) { saveDraft(); attachmentManager.clear(glideRequests, false); @@ -745,7 +745,7 @@ public class ConversationActivity extends PassphraseRequiredActivity protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); - reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, 0); + reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, -1); } private void setVisibleThread(long threadId) { @@ -2262,6 +2262,10 @@ public class ConversationActivity extends PassphraseRequiredActivity reactWithAnyEmojiStartPage = page; } + @Override + public void onReactWithAnyEmojiSelected(@NonNull String emoji) { + } + @Override public void onSearchMoveUpPressed() { searchViewModel.onMoveUp(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java index 96132bf7c9..d0f5433469 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java @@ -21,6 +21,7 @@ public class ConversationBannerView extends ConstraintLayout { private AvatarImageView contactAvatar; private TextView contactTitle; + private TextView contactAbout; private TextView contactSubtitle; private TextView contactDescription; @@ -39,6 +40,7 @@ public class ConversationBannerView extends ConstraintLayout { contactAvatar = findViewById(R.id.message_request_avatar); contactTitle = findViewById(R.id.message_request_title); + contactAbout = findViewById(R.id.message_request_about); contactSubtitle = findViewById(R.id.message_request_subtitle); contactDescription = findViewById(R.id.message_request_description); @@ -53,6 +55,11 @@ public class ConversationBannerView extends ConstraintLayout { contactTitle.setText(title); } + public void setAbout(@Nullable String about) { + contactAbout.setText(about); + contactAbout.setVisibility(TextUtils.isEmpty(about) ? GONE : VISIBLE); + } + public void setSubtitle(@Nullable CharSequence subtitle) { contactSubtitle.setText(subtitle); contactSubtitle.setVisibility(TextUtils.isEmpty(subtitle) ? GONE : VISIBLE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index d257ad76ea..679673d08e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -126,6 +126,7 @@ import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity; import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.CommunicationActions; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.HtmlUtil; import org.thoughtcrime.securesms.util.RemoteDeleteUtil; import org.thoughtcrime.securesms.util.SaveAttachmentTask; @@ -417,7 +418,6 @@ public class ConversationFragment extends LoggingFragment { } private static void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationBannerView conversationBanner) { - if (conversationBanner == null) { return; } @@ -434,6 +434,7 @@ public class ConversationFragment extends LoggingFragment { String title = isSelf ? context.getString(R.string.note_to_self) : recipient.getDisplayNameOrUsername(context); conversationBanner.setTitle(title); + conversationBanner.setAbout(recipient.getCombinedAboutAndEmoji()); if (recipient.isGroup()) { if (pendingMemberCount > 0) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 69c5b8ad24..cc27219df7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -138,6 +138,8 @@ public class RecipientDatabase extends Database { private static final String LAST_SESSION_RESET = "last_session_reset"; private static final String WALLPAPER = "wallpaper"; private static final String WALLPAPER_URI = "wallpaper_file"; + public static final String ABOUT = "about"; + public static final String ABOUT_EMOJI = "about_emoji"; public static final String SEARCH_PROFILE_NAME = "search_signal_profile"; private static final String SORT_NAME = "sort_name"; @@ -162,12 +164,14 @@ public class RecipientDatabase extends Database { FORCE_SMS_SELECTION, CAPABILITIES, STORAGE_SERVICE_ID, DIRTY, - MENTION_SETTING, WALLPAPER, WALLPAPER_URI + MENTION_SETTING, WALLPAPER, WALLPAPER_URI, + MENTION_SETTING, + ABOUT, ABOUT_EMOJI }; private static final String[] ID_PROJECTION = new String[]{ID}; - private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME}; - public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME}; + private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME}; + public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, SEARCH_PROFILE_NAME, SORT_NAME}; private static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) .map(columnName -> TABLE_NAME + "." + columnName) .toList().toArray(new String[0]); @@ -359,7 +363,9 @@ public class RecipientDatabase extends Database { LAST_GV1_MIGRATE_REMINDER + " INTEGER DEFAULT 0, " + LAST_SESSION_RESET + " BLOB DEFAULT NULL, " + WALLPAPER + " BLOB DEFAULT NULL, " + - WALLPAPER_URI + " TEXT DEFAULT NULL);"; + WALLPAPER_URI + " TEXT DEFAULT NULL, " + + ABOUT + " TEXT DEFAULT NULL, " + + ABOUT_EMOJI + " TEXT DEFAULT NULL);"; private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID + " FROM " + TABLE_NAME + @@ -1274,6 +1280,8 @@ public class RecipientDatabase extends Database { String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID); int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING); byte[] wallpaper = CursorUtil.requireBlob(cursor, WALLPAPER); + String about = CursorUtil.requireString(cursor, ABOUT); + String aboutEmoji = CursorUtil.requireString(cursor, ABOUT_EMOJI); MaterialColor color; byte[] profileKey = null; @@ -1359,6 +1367,8 @@ public class RecipientDatabase extends Database { storageKey, MentionSetting.fromId(mentionSettingId), chatWallpaper, + about, + aboutEmoji, getSyncExtras(cursor)); } @@ -1777,6 +1787,16 @@ public class RecipientDatabase extends Database { } } + public void setAbout(@NonNull RecipientId id, @Nullable String about, @Nullable String emoji) { + ContentValues contentValues = new ContentValues(); + contentValues.put(ABOUT, about); + contentValues.put(ABOUT_EMOJI, emoji); + + if (update(id, contentValues)) { + Recipient.live(id).refresh(); + } + } + public void setProfileSharing(@NonNull RecipientId id, @SuppressWarnings("SameParameterValue") boolean enabled) { ContentValues contentValues = new ContentValues(1); contentValues.put(PROFILE_SHARING, enabled ? 1 : 0); @@ -2938,6 +2958,8 @@ public class RecipientDatabase extends Database { private final byte[] storageId; private final MentionSetting mentionSetting; private final ChatWallpaper wallpaper; + private final String about; + private final String aboutEmoji; private final SyncExtras syncExtras; RecipientSettings(@NonNull RecipientId id, @@ -2976,6 +2998,8 @@ public class RecipientDatabase extends Database { @Nullable byte[] storageId, @NonNull MentionSetting mentionSetting, @Nullable ChatWallpaper wallpaper, + @Nullable String about, + @Nullable String aboutEmoji, @NonNull SyncExtras syncExtras) { this.id = id; @@ -3016,6 +3040,8 @@ public class RecipientDatabase extends Database { this.storageId = storageId; this.mentionSetting = mentionSetting; this.wallpaper = wallpaper; + this.about = about; + this.aboutEmoji = aboutEmoji; this.syncExtras = syncExtras; } @@ -3167,6 +3193,14 @@ public class RecipientDatabase extends Database { return wallpaper; } + public @Nullable String getAbout() { + return about; + } + + public @Nullable String getAboutEmoji() { + return aboutEmoji; + } + public @NonNull SyncExtras getSyncExtras() { return syncExtras; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 8304fb12df..6bc8189450 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -170,8 +170,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int CLEAR_PROFILE_KEY_CREDENTIALS = 86; private static final int LAST_RESET_SESSION_TIME = 87; private static final int WALLPAPER = 88; + private static final int ABOUT = 89; - private static final int DATABASE_VERSION = 88; + private static final int DATABASE_VERSION = 89; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -1252,6 +1253,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab db.execSQL("ALTER TABLE recipient ADD COLUMN wallpaper_file TEXT DEFAULT NULL"); } + if (oldVersion < ABOUT) { + db.execSQL("ALTER TABLE recipient ADD COLUMN about TEXT DEFAULT NULL"); + db.execSQL("ALTER TABLE recipient ADD COLUMN about_emoji TEXT DEFAULT NULL"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java index 2a0be29deb..ca517b5b1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java @@ -15,9 +15,12 @@ import androidx.recyclerview.widget.RecyclerView; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.AvatarImageView; +import org.thoughtcrime.securesms.components.emoji.EmojiTextView; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter; import org.thoughtcrime.securesms.util.LifecycleViewHolder; +import org.thoughtcrime.securesms.util.Util; import java.util.ArrayList; import java.util.HashSet; @@ -166,6 +169,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter profileKeyCredential = profileAndCredential.getProfileKeyCredential(); @@ -117,6 +118,18 @@ public class RefreshOwnProfileJob extends BaseJob { } } + private void setProfileAbout(@Nullable String encryptedAbout, @Nullable String encryptedEmoji) { + try { + ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey(); + String plaintextAbout = ProfileUtil.decryptName(profileKey, encryptedAbout); + String plaintextEmoji = ProfileUtil.decryptName(profileKey, encryptedEmoji); + + DatabaseFactory.getRecipientDatabase(context).setAbout(Recipient.self().getId(), plaintextAbout, plaintextEmoji); + } catch (InvalidCiphertextException | IOException e) { + Log.w(TAG, e); + } + } + private static void setProfileAvatar(@Nullable String avatar) { ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), avatar)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index 85f146850f..b0aa1d0f5a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -328,6 +328,7 @@ public class RetrieveProfileJob extends BaseJob { ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); setProfileName(recipient, profile.getName()); + setProfileAbout(recipient, profile.getAbout(), profile.getAboutEmoji()); setProfileAvatar(recipient, profile.getAvatar()); clearUsername(recipient); setProfileCapabilities(recipient, profile.getCapabilities()); @@ -454,6 +455,20 @@ public class RetrieveProfileJob extends BaseJob { } } + private void setProfileAbout(@NonNull Recipient recipient, @Nullable String encryptedAbout, @Nullable String encryptedEmoji) { + try { + ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); + if (profileKey == null) return; + + String plaintextAbout = ProfileUtil.decryptName(profileKey, encryptedAbout); + String plaintextEmoji = ProfileUtil.decryptName(profileKey, encryptedEmoji); + + DatabaseFactory.getRecipientDatabase(context).setAbout(recipient.getId(), plaintextAbout, plaintextEmoji); + } catch (InvalidCiphertextException | IOException e) { + Log.w(TAG, e); + } + } + private static void setProfileAvatar(Recipient recipient, String profileAvatar) { if (recipient.getProfileKey() == null) return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java new file mode 100644 index 0000000000..db40c8bd87 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java @@ -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.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()); + } + } +} 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 b2d3e4d0fa..73f1fd9862 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 @@ -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); + } } 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 d061b4c65b..4d78880515 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 @@ -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) { 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 f3cf6e0455..3cb79aaddd 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 @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java index 545fac3773..518a580f8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java @@ -45,9 +45,14 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee EmojiPageViewGridAdapter.VariationSelectorListener { + private static final String REACTION_STORAGE_KEY = "reactions_recent_emoji"; + private static final String ABOUT_STORAGE_KEY = EmojiKeyboardProvider.RECENT_STORAGE_KEY; + private static final String ARG_MESSAGE_ID = "arg_message_id"; private static final String ARG_IS_MMS = "arg_is_mms"; private static final String ARG_START_PAGE = "arg_start_page"; + private static final String ARG_SHADOWS = "arg_shadows"; + private static final String ARG_RECENT_KEY = "arg_recent_key"; private ReactWithAnyEmojiViewModel viewModel; private TextSwitcher categoryLabel; @@ -65,6 +70,22 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee args.putLong(ARG_MESSAGE_ID, messageRecord.getId()); args.putBoolean(ARG_IS_MMS, messageRecord.isMms()); args.putInt(ARG_START_PAGE, startingPage); + args.putBoolean(ARG_SHADOWS, false); + args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY); + fragment.setArguments(args); + + return fragment; + } + + public static DialogFragment createForAboutSelection() { + DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment(); + Bundle args = new Bundle(); + + args.putLong(ARG_MESSAGE_ID, -1); + args.putBoolean(ARG_IS_MMS, false); + args.putInt(ARG_START_PAGE, -1); + args.putBoolean(ARG_SHADOWS, true); + args.putString(ARG_RECENT_KEY, ABOUT_STORAGE_KEY); fragment.setArguments(args); return fragment; @@ -79,10 +100,13 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee @Override public void onCreate(@Nullable Bundle savedInstanceState) { + boolean shadows = requireArguments().getBoolean(ARG_SHADOWS); if (ThemeUtil.isDarkTheme(requireContext())) { - setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny); + setStyle(DialogFragment.STYLE_NORMAL, shadows ? R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny + : R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny_Shadowless); } else { - setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny); + setStyle(DialogFragment.STYLE_NORMAL, shadows ? R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny + : R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny_Shadowless); } super.onCreate(savedInstanceState); @@ -168,15 +192,18 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee if (savedInstanceState == null) { FrameLayout container = requireDialog().findViewById(R.id.container); LayoutInflater layoutInflater = LayoutInflater.from(requireContext()); - View statusBarShader = layoutInflater.inflate(R.layout.react_with_any_emoji_status_fade, container, false); TabLayout categoryTabs = (TabLayout) layoutInflater.inflate(R.layout.react_with_any_emoji_tabs, container, false); - ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtil.getStatusBarHeight(container)); - statusBarShader.setLayoutParams(params); - container.addView(statusBarShader, 0); + if (!requireArguments().getBoolean(ARG_SHADOWS)) { + View statusBarShader = layoutInflater.inflate(R.layout.react_with_any_emoji_status_fade, container, false); + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtil.getStatusBarHeight(container)); + + statusBarShader.setLayoutParams(params); + container.addView(statusBarShader, 0); + } + container.addView(categoryTabs); - ViewCompat.setOnApplyWindowInsetsListener(container, (v, insets) -> insets.consumeSystemWindowInsets()); new TabLayoutMediator(categoryTabs, categoryPager, (tab, position) -> { @@ -203,7 +230,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee private void initializeViewModel() { Bundle args = requireArguments(); - ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext()); + ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY)); ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(reactionsLoader, repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS)); viewModel = ViewModelProviders.of(this, factory).get(ReactWithAnyEmojiViewModel.class); @@ -212,6 +239,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee @Override public void onEmojiSelected(String emoji) { viewModel.onEmojiSelected(emoji); + callback.onReactWithAnyEmojiSelected(emoji); dismiss(); } @@ -239,7 +267,8 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee } private int getStartingPage(boolean firstPageHasContent) { - return requireArguments().getInt(ARG_START_PAGE, firstPageHasContent ? 0 : 1); + int startPage = requireArguments().getInt(ARG_START_PAGE); + return startPage >= 0 ? startPage : (firstPageHasContent ? 0 : 1); } private class OnPageChanged extends ViewPager2.OnPageChangeCallback { @@ -253,5 +282,6 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee public interface Callback { void onReactWithAnyEmojiDialogDismissed(); void onReactWithAnyEmojiPageChanged(int page); + void onReactWithAnyEmojiSelected(@NonNull String emoji); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java index 7325a1a8b1..5d9b0e4db8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java @@ -32,15 +32,13 @@ final class ReactWithAnyEmojiRepository { private static final String TAG = Log.tag(ReactWithAnyEmojiRepository.class); - private static final String RECENT_STORAGE_KEY = "reactions_recent_emoji"; - - private final Context context; - private final RecentEmojiPageModel recentEmojiPageModel; + private final Context context; + private final RecentEmojiPageModel recentEmojiPageModel; private final List emojiPages; - ReactWithAnyEmojiRepository(@NonNull Context context) { + ReactWithAnyEmojiRepository(@NonNull Context context, @NonNull String storageKey) { this.context = context; - this.recentEmojiPageModel = new RecentEmojiPageModel(context, RECENT_STORAGE_KEY); + this.recentEmojiPageModel = new RecentEmojiPageModel(context, storageKey); this.emojiPages = new LinkedList<>(); emojiPages.addAll(Stream.of(EmojiUtil.getDisplayPages()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java index a075db47d6..013500eec1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java @@ -36,8 +36,10 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel { } void onEmojiSelected(@NonNull String emoji) { - SignalStore.emojiValues().setPreferredVariation(emoji); - repository.addEmojiToMessage(emoji, messageId, isMms); + if (messageId > 0) { + SignalStore.emojiValues().setPreferredVariation(emoji); + repository.addEmojiToMessage(emoji, messageId, isMms); + } } static class Factory implements ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 14a5ad74fb..7e30ee63af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -108,6 +108,8 @@ public class Recipient { private final byte[] storageId; private final MentionSetting mentionSetting; private final ChatWallpaper wallpaper; + private final String about; + private final String aboutEmoji; /** @@ -342,6 +344,8 @@ public class Recipient { this.storageId = null; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; this.wallpaper = null; + this.about = null; + this.aboutEmoji = null; } public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) { @@ -385,6 +389,8 @@ public class Recipient { this.storageId = details.storageId; this.mentionSetting = details.mentionSetting; this.wallpaper = details.wallpaper; + this.about = details.about; + this.aboutEmoji = details.aboutEmoji; } public @NonNull RecipientId getId() { @@ -870,6 +876,28 @@ public class Recipient { return contactUri != null; } + public @Nullable String getAbout() { + return about; + } + + public @Nullable String getAboutEmoji() { + return aboutEmoji; + } + + public @Nullable String getCombinedAboutAndEmoji() { + if (aboutEmoji != null) { + if (about != null) { + return aboutEmoji + " " + about; + } else { + return aboutEmoji; + } + } else if (about != null) { + return about; + } else { + return null; + } + } + /** * If this recipient is missing crucial data, this will return a populated copy. Otherwise it * returns itself. @@ -985,7 +1013,9 @@ public class Recipient { insightsBannerTier == other.insightsBannerTier && Arrays.equals(storageId, other.storageId) && mentionSetting == other.mentionSetting && - Objects.equals(wallpaper, other.wallpaper); + Objects.equals(wallpaper, other.wallpaper) && + Objects.equals(about, other.about) && + Objects.equals(aboutEmoji, other.aboutEmoji); } private static boolean allContentsAreTheSame(@NonNull List a, @NonNull List b) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index c46b547eff..d8af3b51af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -67,6 +67,8 @@ public class RecipientDetails { final byte[] storageId; final MentionSetting mentionSetting; final ChatWallpaper wallpaper; + final String about; + final String aboutEmoji; public RecipientDetails(@Nullable String name, @NonNull Optional groupAvatarId, @@ -113,6 +115,8 @@ public class RecipientDetails { this.storageId = settings.getStorageId(); this.mentionSetting = settings.getMentionSetting(); this.wallpaper = settings.getWallpaper(); + this.about = settings.getAbout(); + this.aboutEmoji = settings.getAboutEmoji(); if (name == null) this.name = settings.getSystemDisplayName(); else this.name = name; @@ -161,6 +165,8 @@ public class RecipientDetails { this.storageId = null; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; this.wallpaper = null; + this.about = null; + this.aboutEmoji = null; } public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientSettings settings) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java index e133e94ec0..957acfe7a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java @@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.util.BottomSheetUtil; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; @@ -55,6 +56,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF private RecipientDialogViewModel viewModel; private AvatarImageView avatar; private TextView fullName; + private TextView about; private TextView usernameNumber; private Button messageButton; private Button secureCallButton; @@ -102,6 +104,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF avatar = view.findViewById(R.id.rbs_recipient_avatar); fullName = view.findViewById(R.id.rbs_full_name); + about = view.findViewById(R.id.rbs_about); usernameNumber = view.findViewById(R.id.rbs_username_number); messageButton = view.findViewById(R.id.rbs_message_button); secureCallButton = view.findViewById(R.id.rbs_secure_call_button); @@ -158,6 +161,14 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF TextViewCompat.setCompoundDrawableTintList(fullName, ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_text_primary))); } + String aboutText = recipient.getCombinedAboutAndEmoji(); + if (!Util.isEmpty(aboutText)) { + about.setText(aboutText); + about.setVisibility(View.VISIBLE); + } else { + about.setVisibility(View.GONE); + } + String usernameNumberString = recipient.hasAUserSetDisplayName(requireContext()) && !recipient.isSelf() ? recipient.getSmsAddress().transform(PhoneNumberFormatter::prettyPrint).or("").trim() : ""; diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java index 001f219f09..d4ec162ed2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java @@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment; import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.LifecycleCursorWrapper; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.Util; @@ -71,6 +72,7 @@ public class ManageRecipientFragment extends LoggingFragment { private GroupMemberListView sharedGroupList; private Toolbar toolbar; private TextView title; + private TextView about; private TextView subtitle; private ViewGroup internalDetails; private TextView internalDetailsText; @@ -132,6 +134,7 @@ public class ManageRecipientFragment extends LoggingFragment { contactText = view.findViewById(R.id.recipient_contact_text); contactIcon = view.findViewById(R.id.recipient_contact_icon); title = view.findViewById(R.id.name); + about = view.findViewById(R.id.about); subtitle = view.findViewById(R.id.username_number); internalDetails = view.findViewById(R.id.recipient_internal_details); internalDetailsText = view.findViewById(R.id.recipient_internal_details_text); @@ -303,6 +306,10 @@ public class ManageRecipientFragment extends LoggingFragment { }); } + String aboutText = recipient.getCombinedAboutAndEmoji(); + about.setText(aboutText); + about.setVisibility(Util.isEmpty(aboutText) ? View.GONE : View.VISIBLE); + disappearingMessagesCard.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE); addToAGroup.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index ec1909c2ea..a96d1c6fa6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -71,7 +71,6 @@ public final class FeatureFlags { private static final String AUTOMATIC_SESSION_INTERVAL = "android.automaticSessionResetInterval"; private static final String DEFAULT_MAX_BACKOFF = "android.defaultMaxBackoff"; private static final String OKHTTP_AUTOMATIC_RETRY = "android.okhttpAutomaticRetry"; - private static final String ABOUT = "android.about"; private static final String SHARE_SELECTION_LIMIT = "android.share.limit"; /** @@ -101,7 +100,6 @@ public final class FeatureFlags { AUTOMATIC_SESSION_INTERVAL, DEFAULT_MAX_BACKOFF, OKHTTP_AUTOMATIC_RETRY, - ABOUT, SHARE_SELECTION_LIMIT ); @@ -141,7 +139,6 @@ public final class FeatureFlags { AUTOMATIC_SESSION_INTERVAL, DEFAULT_MAX_BACKOFF, OKHTTP_AUTOMATIC_RETRY, - ABOUT, SHARE_SELECTION_LIMIT ); @@ -327,11 +324,6 @@ public final class FeatureFlags { return getBoolean(OKHTTP_AUTOMATIC_RETRY, false); } - /** Whether or not the 'About' section of the profile is enabled. */ - public static boolean about() { - return getBoolean(ABOUT, false); - } - /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/res/drawable/ic_add_emoji.xml b/app/src/main/res/drawable/ic_add_emoji.xml new file mode 100644 index 0000000000..626e613cc0 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_emoji.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/contact_selection_list_item.xml b/app/src/main/res/layout/contact_selection_list_item.xml index 8b5432a186..741e9db935 100644 --- a/app/src/main/res/layout/contact_selection_list_item.xml +++ b/app/src/main/res/layout/contact_selection_list_item.xml @@ -59,7 +59,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"> - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/group_recipient_list_item.xml b/app/src/main/res/layout/group_recipient_list_item.xml index c11a66affa..9196c7b725 100644 --- a/app/src/main/res/layout/group_recipient_list_item.xml +++ b/app/src/main/res/layout/group_recipient_list_item.xml @@ -39,12 +39,31 @@ android:gravity="start|center_vertical" android:textAlignment="viewStart" android:textAppearance="@style/Signal.Text.Body" - app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar" + app:layout_constraintBottom_toTopOf="@+id/recipient_about" app:layout_constraintEnd_toStartOf="@+id/admin" app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toEndOf="@+id/recipient_avatar" app:layout_constraintTop_toTopOf="@+id/recipient_avatar" - tools:text="@tools:sample/full_names" /> + tools:text="Miles Morales" /> + + - - - + tools:text="Photographer for the Daily Bugle"/> + + + + + app:enterAnim="@anim/slide_from_end" + app:exitAnim="@anim/slide_to_start" + app:popEnterAnim="@anim/slide_from_start" + app:popExitAnim="@anim/slide_to_end" /> + app:enterAnim="@anim/slide_from_end" + app:exitAnim="@anim/slide_to_start" + app:popEnterAnim="@anim/slide_from_start" + app:popExitAnim="@anim/slide_to_end" /> + + @@ -41,4 +49,10 @@ android:label="fragment_manage_profile_name" tools:layout="@layout/edit_profile_name_fragment" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c257c9a173..d6e5c791eb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1999,6 +1999,11 @@ Create a username Custom MMS group names and photos will only be visible to you. + + About + Write a few words about yourself… + %d/%d + Edit group name and photo Group name diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index dc9f50916b..9813363b72 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -321,15 +321,23 @@ + + + + diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 6ce1229ccb..ad26c3e2c5 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -21,6 +21,7 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; +import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream; import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; @@ -634,12 +635,14 @@ public class SignalServiceAccountManager { /** * @return The avatar URL path, if one was written. */ - public Optional setVersionedProfile(UUID uuid, ProfileKey profileKey, String name, StreamDetails avatar) + public Optional setVersionedProfile(UUID uuid, ProfileKey profileKey, String name, String about, String aboutEmoji, StreamDetails avatar) throws IOException { if (name == null) name = ""; byte[] ciphertextName = new ProfileCipher(profileKey).encryptName(name.getBytes(StandardCharsets.UTF_8), ProfileCipher.getTargetNameLength(name)); + byte[] ciphertextAbout = new ProfileCipher(profileKey).encryptName(about.getBytes(StandardCharsets.UTF_8), ProfileCipher.getTargetAboutLength(about)); + byte[] ciphertextEmoji = new ProfileCipher(profileKey).encryptName(aboutEmoji.getBytes(StandardCharsets.UTF_8), ProfileCipher.EMOJI_PADDED_LENGTH); boolean hasAvatar = avatar != null; ProfileAvatarData profileAvatarData = null; @@ -652,6 +655,8 @@ public class SignalServiceAccountManager { return this.pushServiceSocket.writeProfile(new SignalServiceProfileWrite(profileKey.getProfileKeyVersion(uuid).serialize(), ciphertextName, + ciphertextAbout, + ciphertextEmoji, hasAvatar, profileKey.getCommitment(uuid).serialize()), profileAvatarData); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java index 446c6dd832..8bd75d0c6e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java @@ -23,8 +23,13 @@ public class ProfileCipher { private static final int NAME_PADDED_LENGTH_1 = 53; private static final int NAME_PADDED_LENGTH_2 = 257; + private static final int ABOUT_PADDED_LENGTH_1 = 128; + private static final int ABOUT_PADDED_LENGTH_2 = 254; + private static final int ABOUT_PADDED_LENGTH_3 = 512; - public static final int MAX_POSSIBLE_NAME_LENGTH = NAME_PADDED_LENGTH_2; + public static final int MAX_POSSIBLE_NAME_LENGTH = NAME_PADDED_LENGTH_2; + public static final int MAX_POSSIBLE_ABOUT_LENGTH = ABOUT_PADDED_LENGTH_3; + public static final int EMOJI_PADDED_LENGTH = 32; private final ProfileKey key; @@ -112,4 +117,16 @@ public class ProfileCipher { return NAME_PADDED_LENGTH_2; } } + + public static int getTargetAboutLength(String about) { + int aboutLength = about.getBytes(StandardCharsets.UTF_8).length; + + if (aboutLength <= ABOUT_PADDED_LENGTH_1) { + return ABOUT_PADDED_LENGTH_1; + } else if (aboutLength < ABOUT_PADDED_LENGTH_2){ + return ABOUT_PADDED_LENGTH_2; + } else { + return ABOUT_PADDED_LENGTH_3; + } + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index 9f614f8e52..e58d75d162 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -29,6 +29,12 @@ public class SignalServiceProfile { @JsonProperty private String name; + @JsonProperty + private String about; + + @JsonProperty + private String aboutEmoji; + @JsonProperty private String avatar; @@ -62,6 +68,14 @@ public class SignalServiceProfile { return name; } + public String getAbout() { + return about; + } + + public String getAboutEmoji() { + return aboutEmoji; + } + public String getAvatar() { return avatar; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java index 57ded06a5a..243e7c668b 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java @@ -11,6 +11,12 @@ public class SignalServiceProfileWrite { @JsonProperty private byte[] name; + @JsonProperty + private byte[] about; + + @JsonProperty + private byte[] aboutEmoji; + @JsonProperty private boolean avatar; @@ -21,9 +27,11 @@ public class SignalServiceProfileWrite { public SignalServiceProfileWrite(){ } - public SignalServiceProfileWrite(String version, byte[] name, boolean avatar, byte[] commitment) { + public SignalServiceProfileWrite(String version, byte[] name, byte[] about, byte[] aboutEmoji, boolean avatar, byte[] commitment) { this.version = version; this.name = name; + this.about = about; + this.aboutEmoji = aboutEmoji; this.avatar = avatar; this.commitment = commitment; }