diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java b/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java index 229dada167..0b1d6f4653 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java @@ -8,6 +8,7 @@ import android.os.Bundle; import android.text.Annotation; import android.text.Editable; import android.text.InputType; +import android.text.Selection; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; @@ -22,6 +23,7 @@ import android.view.MenuItem; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; +import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; @@ -338,48 +340,11 @@ public class ComposeText extends EmojiEditText { @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - Editable text = getText(); - - if (text == null) { - return false; + boolean handled = handleFormatText(item.getItemId()); + if (handled) { + mode.finish(); } - - if (item.getItemId() != R.id.edittext_bold && - item.getItemId() != R.id.edittext_italic && - item.getItemId() != R.id.edittext_strikethrough && - item.getItemId() != R.id.edittext_monospace && - item.getItemId() != R.id.edittext_spoiler && - item.getItemId() != R.id.edittext_clear_formatting) - { - return false; - } - - int start = getSelectionStart(); - int end = getSelectionEnd(); - BodyRangeList.BodyRange.Style style = null; - - if (item.getItemId() == R.id.edittext_bold) { - style = BodyRangeList.BodyRange.Style.BOLD; - } else if (item.getItemId() == R.id.edittext_italic) { - style = BodyRangeList.BodyRange.Style.ITALIC; - } else if (item.getItemId() == R.id.edittext_strikethrough) { - style = BodyRangeList.BodyRange.Style.STRIKETHROUGH; - } else if (item.getItemId() == R.id.edittext_monospace) { - style = BodyRangeList.BodyRange.Style.MONOSPACE; - } else if (item.getItemId() == R.id.edittext_spoiler) { - style = BodyRangeList.BodyRange.Style.SPOILER; - } - - clearComposingText(); - - if (style != null) { - MessageStyler.toggleStyle(style, text, start, end); - } else if (item.getItemId() == R.id.edittext_clear_formatting) { - MessageStyler.clearStyling(text, start, end); - } - - mode.finish(); - return true; + return handled; } @Override @@ -567,6 +532,56 @@ public class ComposeText extends EmojiEditText { return TIME_PATTERN.matcher(text.subSequence(startOfToken, endOfToken)).find(); } + public boolean isTextHighlighted() { + return getText() != null && getSelectionStart() < getSelectionEnd(); + } + + public boolean handleFormatText(@IdRes int id) { + Editable text = getText(); + + if (text == null) { + return false; + } + + if (id != R.id.edittext_bold && + id != R.id.edittext_italic && + id != R.id.edittext_strikethrough && + id != R.id.edittext_monospace && + id != R.id.edittext_spoiler && + id != R.id.edittext_clear_formatting) + { + return false; + } + + int start = getSelectionStart(); + int end = getSelectionEnd(); + BodyRangeList.BodyRange.Style style = null; + + if (id == R.id.edittext_bold) { + style = BodyRangeList.BodyRange.Style.BOLD; + } else if (id == R.id.edittext_italic) { + style = BodyRangeList.BodyRange.Style.ITALIC; + } else if (id == R.id.edittext_strikethrough) { + style = BodyRangeList.BodyRange.Style.STRIKETHROUGH; + } else if (id == R.id.edittext_monospace) { + style = BodyRangeList.BodyRange.Style.MONOSPACE; + } else if (id == R.id.edittext_spoiler) { + style = BodyRangeList.BodyRange.Style.SPOILER; + } + + clearComposingText(); + + if (style != null) { + MessageStyler.toggleStyle(style, text, start, end); + } else { + MessageStyler.clearStyling(text, start, end); + } + + Selection.setSelection(getText(), end); + + return true; + } + private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener { private static final String TAG = Log.tag(CommitContentListener.class); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationOptionsMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationOptionsMenu.kt index 317956cffe..70f5b80709 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationOptionsMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationOptionsMenu.kt @@ -5,6 +5,7 @@ package org.thoughtcrime.securesms.conversation +import android.text.SpannableString import android.view.Menu import android.view.MenuInflater import android.view.MenuItem @@ -165,9 +166,23 @@ internal object ConversationOptionsMenu { hideMenuItem(menu, R.id.menu_view_media) } + menu.findItem(R.id.menu_format_text_submenu).subMenu?.clearHeader() + menu.findItem(R.id.edittext_bold).applyTitleSpan(MessageStyler.boldStyle()) + menu.findItem(R.id.edittext_italic).applyTitleSpan(MessageStyler.italicStyle()) + menu.findItem(R.id.edittext_strikethrough).applyTitleSpan(MessageStyler.strikethroughStyle()) + menu.findItem(R.id.edittext_monospace).applyTitleSpan(MessageStyler.monoStyle()) + callback.onOptionsMenuCreated(menu) } + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + val formatText = menu.findItem(R.id.menu_format_text_submenu) + if (formatText != null) { + formatText.isVisible = callback.isTextHighlighted() + } + } + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { when (menuItem.itemId) { R.id.menu_call_secure -> callback.handleDial(true) @@ -189,6 +204,12 @@ internal object ConversationOptionsMenu { R.id.menu_expiring_messages_off, R.id.menu_expiring_messages -> callback.handleSelectMessageExpiration() R.id.menu_create_bubble -> callback.handleCreateBubble() R.id.home -> callback.handleGoHome() + R.id.edittext_bold, + R.id.edittext_italic, + R.id.edittext_strikethrough, + R.id.edittext_monospace, + R.id.edittext_spoiler, + R.id.edittext_clear_formatting -> callback.handleFormatText(menuItem.itemId) else -> return false } @@ -200,6 +221,10 @@ internal object ConversationOptionsMenu { menu.findItem(menuItem).isVisible = false } } + + private fun MenuItem.applyTitleSpan(span: Any) { + title = SpannableString(title).apply { setSpan(span, 0, length, MessageStyler.SPAN_FLAGS) } + } } /** @@ -224,6 +249,7 @@ internal object ConversationOptionsMenu { */ interface Callback { fun getSnapshot(): Snapshot + fun isTextHighlighted(): Boolean fun onOptionsMenuCreated(menu: Menu) @@ -248,5 +274,6 @@ internal object ConversationOptionsMenu { fun showExpiring(recipient: Recipient) fun clearExpiring() fun showGroupCallingTooltip() + fun handleFormatText(@IdRes id: Int) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java index 1f910764b1..cf4808670c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -472,8 +472,6 @@ public class ConversationParentFragment extends Fragment private Callback callback; private RecentEmojiPageModel recentEmojis; - private ConversationOptionsMenu.Provider menuProvider; - private Set previousPages; public static ConversationParentFragment create(Intent intent) { @@ -494,7 +492,6 @@ public class ConversationParentFragment extends Fragment @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { disposables.bindTo(getViewLifecycleOwner()); - menuProvider = new ConversationOptionsMenu.Provider(this, disposables); SpoilerAnnotation.resetRevealedSpoilers(); if (requireActivity() instanceof Callback) { @@ -575,10 +572,6 @@ public class ConversationParentFragment extends Fragment }; requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback); - if (isSearchRequested && savedInstanceState == null) { - menuProvider.onCreateMenu(toolbar.getMenu(), requireActivity().getMenuInflater()); - } - sendButton.post(() -> sendButton.triggerSelectedChangedEvent()); } @@ -990,7 +983,7 @@ public class ConversationParentFragment extends Fragment if (!isSearchRequested && getActivity() != null) { optionsMenuDebouncer.publish(() -> { if (getActivity() != null) { - menuProvider.onCreateMenu(toolbar.getMenu(), requireActivity().getMenuInflater()); + toolbar.invalidateMenu(); } }); } @@ -2108,8 +2101,8 @@ public class ConversationParentFragment extends Fragment } protected void initializeActionBar() { + toolbar.addMenuProvider(new ConversationOptionsMenu.Provider(this, disposables)); invalidateOptionsMenu(); - toolbar.setOnMenuItemClickListener(menuProvider::onMenuItemSelected); toolbar.setNavigationContentDescription(R.string.ConversationFragment__content_description_back_button); if (isInBubble()) { toolbar.setNavigationIcon(DrawableUtil.tint(ContextUtil.requireDrawable(requireContext(), R.drawable.ic_notification), @@ -2370,6 +2363,11 @@ public class ConversationParentFragment extends Fragment .show(TooltipPopup.POSITION_BELOW); } + @Override + public void handleFormatText(@IdRes int id) { + composeText.handleFormatText(id); + } + private void showStickerIntroductionTooltip() { TextSecurePreferences.setMediaKeyboardMode(requireContext(), MediaKeyboardMode.STICKER); inputPanel.setMediaKeyboardToggleMode(KeyboardPage.STICKER); @@ -3679,6 +3677,11 @@ public class ConversationParentFragment extends Fragment ); } + @Override + public boolean isTextHighlighted() { + return composeText.isTextHighlighted(); + } + @Override public void showExpiring(@NonNull Recipient recipient) { titleView.showExpiring(recipient); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 7a768d971a..be36bb5d80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -330,7 +330,6 @@ class ConversationFragment : private val colorizer = Colorizer() private val textDraftSaveDebouncer = Debouncer(500) - private lateinit var conversationOptionsMenuProvider: ConversationOptionsMenu.Provider private lateinit var layoutManager: LinearLayoutManager private lateinit var markReadHelper: MarkReadHelper private lateinit var giphyMp4ProjectionRecycler: GiphyMp4ProjectionRecycler @@ -396,7 +395,6 @@ class ConversationFragment : disposables.bindTo(viewLifecycleOwner) FullscreenHelper(requireActivity()).showSystemUI() - conversationOptionsMenuProvider = ConversationOptionsMenu.Provider(ConversationOptionsMenuCallback(), disposables) markReadHelper = MarkReadHelper(ConversationId.forConversation(args.threadId), requireContext(), viewLifecycleOwner) initializeConversationThreadUi() @@ -777,12 +775,13 @@ class ConversationFragment : } private fun invalidateOptionsMenu() { - if (!isSearchRequested && activity != null) { - conversationOptionsMenuProvider.onCreateMenu(binding.toolbar.menu, requireActivity().menuInflater) + if (!isSearchRequested) { + binding.toolbar.invalidateMenu() } } private fun presentActionBarMenu() { + binding.toolbar.addMenuProvider(ConversationOptionsMenu.Provider(ConversationOptionsMenuCallback(), disposables)) invalidateOptionsMenu() when (args.conversationScreenType) { @@ -790,8 +789,6 @@ class ConversationFragment : ConversationScreenType.BUBBLE -> presentNavigationIconForBubble() ConversationScreenType.POPUP -> Unit } - - binding.toolbar.setOnMenuItemClickListener(conversationOptionsMenuProvider::onMenuItemSelected) } private fun presentNavigationIconForNormal() { @@ -2109,6 +2106,10 @@ class ConversationFragment : ) } + override fun isTextHighlighted(): Boolean { + return composeText.isTextHighlighted + } + override fun onOptionsMenuCreated(menu: Menu) { searchMenuItem = menu.findItem(R.id.menu_search) @@ -2337,6 +2338,10 @@ class ConversationFragment : override fun showGroupCallingTooltip() { conversationTooltips.displayGroupCallingTooltip(requireView().findViewById(R.id.menu_video_secure)) } + + override fun handleFormatText(id: Int) { + composeText.handleFormatText(id) + } } private inner class OnReactionsSelectedListener : ConversationReactionOverlay.OnReactionSelectedListener { diff --git a/app/src/main/res/menu/conversation.xml b/app/src/main/res/menu/conversation.xml index e7e97bbefc..51510c1c34 100644 --- a/app/src/main/res/menu/conversation.xml +++ b/app/src/main/res/menu/conversation.xml @@ -1,23 +1,62 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> - + - + - + - + - + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48e59d9ef3..0552654f81 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3346,6 +3346,8 @@ Chat settings Add to home screen Create bubble + + Format text Expand popup