Allow styling text in composer when selecting it with native actions (#14249)
This commit is contained in:
parent
a9295ac17e
commit
32d85d5059
|
@ -0,0 +1,78 @@
|
||||||
|
package im.status.ethereum.module;
|
||||||
|
|
||||||
|
import android.view.ActionMode;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
import com.facebook.react.uimanager.NativeViewHierarchyManager;
|
||||||
|
import com.facebook.react.uimanager.UIBlock;
|
||||||
|
import com.facebook.react.uimanager.UIManagerModule;
|
||||||
|
import com.facebook.react.views.textinput.ReactEditText;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
class RNSelectableTextInputModule extends ReactContextBaseJavaModule {
|
||||||
|
|
||||||
|
private ActionMode lastActionMode;
|
||||||
|
|
||||||
|
public RNSelectableTextInputModule(ReactApplicationContext reactContext) {
|
||||||
|
super(reactContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "RNSelectableTextInputManager";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void setupMenuItems(final Integer selectableTextViewReactTag, final Integer textInputReactTag) {
|
||||||
|
ReactApplicationContext reactContext = this.getReactApplicationContext();
|
||||||
|
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
|
||||||
|
uiManager.addUIBlock(new UIBlock() {
|
||||||
|
public void execute (NativeViewHierarchyManager nvhm) {
|
||||||
|
RNSelectableTextInputViewManager rnSelectableTextManager = (RNSelectableTextInputViewManager) nvhm.resolveViewManager(selectableTextViewReactTag);
|
||||||
|
ReactEditText reactTextView = (ReactEditText) nvhm.resolveView(textInputReactTag);
|
||||||
|
rnSelectableTextManager.registerSelectionListener(reactTextView);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void startActionMode(final Integer textInputReactTag) {
|
||||||
|
ReactApplicationContext reactContext = this.getReactApplicationContext();
|
||||||
|
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
|
||||||
|
uiManager.addUIBlock(new UIBlock() {
|
||||||
|
public void execute (NativeViewHierarchyManager nvhm) {
|
||||||
|
ReactEditText reactTextView = (ReactEditText) nvhm.resolveView(textInputReactTag);
|
||||||
|
lastActionMode = reactTextView.startActionMode(reactTextView.getCustomSelectionActionModeCallback(), ActionMode.TYPE_FLOATING);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void hideLastActionMode(){
|
||||||
|
ReactApplicationContext reactContext = this.getReactApplicationContext();
|
||||||
|
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
|
||||||
|
uiManager.addUIBlock(new UIBlock() {
|
||||||
|
public void execute (NativeViewHierarchyManager nvhm) {
|
||||||
|
if(lastActionMode!=null){
|
||||||
|
lastActionMode.finish();
|
||||||
|
lastActionMode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void setSelection(final Integer textInputReactTag, final Integer start, final Integer end){
|
||||||
|
ReactApplicationContext reactContext = this.getReactApplicationContext();
|
||||||
|
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
|
||||||
|
uiManager.addUIBlock(new UIBlock() {
|
||||||
|
public void execute (NativeViewHierarchyManager nvhm) {
|
||||||
|
ReactEditText reactTextView = (ReactEditText) nvhm.resolveView(textInputReactTag);
|
||||||
|
reactTextView.setSelection(start, end);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
package im.status.ethereum.module;
|
||||||
|
|
||||||
|
import android.view.ActionMode;
|
||||||
|
import android.view.ActionMode.Callback;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.ReactContext;
|
||||||
|
import com.facebook.react.bridge.ReadableArray;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.facebook.react.common.MapBuilder;
|
||||||
|
import com.facebook.react.uimanager.ThemedReactContext;
|
||||||
|
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||||
|
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||||
|
import com.facebook.react.views.textinput.ReactEditText;
|
||||||
|
import com.facebook.react.views.view.ReactViewGroup;
|
||||||
|
import com.facebook.react.views.view.ReactViewManager;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class RNSelectableTextInputViewManager extends ReactViewManager {
|
||||||
|
public static final String REACT_CLASS = "RNSelectableTextInput";
|
||||||
|
private String[] _menuItems = new String[0];
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return REACT_CLASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReactViewGroup createViewInstance(ThemedReactContext context) {
|
||||||
|
return new ReactViewGroup(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "menuItems")
|
||||||
|
public void setMenuItems(ReactViewGroup reactViewGroup, ReadableArray items) {
|
||||||
|
if(items != null) {
|
||||||
|
List<String> result = new ArrayList<String>(items.size());
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
result.add(items.getString(i));
|
||||||
|
}
|
||||||
|
this._menuItems = result.toArray(new String[items.size()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerSelectionListener(final ReactEditText view) {
|
||||||
|
view.setCustomSelectionActionModeCallback(new Callback() {
|
||||||
|
@Override
|
||||||
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||||
|
menu.clear();
|
||||||
|
for (int i = 0; i < _menuItems.length; i++) {
|
||||||
|
menu.add(0, i, 0, _menuItems[i]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyActionMode(ActionMode mode) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||||
|
int selectionStart = view.getSelectionStart();
|
||||||
|
int selectionEnd = view.getSelectionEnd();
|
||||||
|
String selectedText = view.getText().toString().substring(selectionStart, selectionEnd);
|
||||||
|
|
||||||
|
// Dispatch event
|
||||||
|
onSelectNativeEvent(view, item.getItemId(), selectedText, selectionStart, selectionEnd);
|
||||||
|
|
||||||
|
mode.finish();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSelectNativeEvent(ReactEditText view, int eventType, String content, int selectionStart, int selectionEnd) {
|
||||||
|
WritableMap event = Arguments.createMap();
|
||||||
|
event.putInt("eventType", eventType);
|
||||||
|
event.putString("content", content);
|
||||||
|
event.putInt("selectionStart", selectionStart);
|
||||||
|
event.putInt("selectionEnd", selectionEnd);
|
||||||
|
|
||||||
|
// Dispatch
|
||||||
|
ReactContext reactContext = (ReactContext) view.getContext();
|
||||||
|
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(view.getId(), "topSelection", event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map getExportedCustomDirectEventTypeConstants() {
|
||||||
|
return MapBuilder.builder()
|
||||||
|
.put("topSelection", MapBuilder.of("registrationName","onSelection"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
import com.facebook.react.uimanager.ViewManager;
|
import com.facebook.react.uimanager.ViewManager;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -29,12 +30,15 @@ public class StatusPackage implements ReactPackage {
|
||||||
List<NativeModule> modules = new ArrayList<>();
|
List<NativeModule> modules = new ArrayList<>();
|
||||||
|
|
||||||
modules.add(new StatusModule(reactContext, this.rootedDevice));
|
modules.add(new StatusModule(reactContext, this.rootedDevice));
|
||||||
|
modules.add(new RNSelectableTextInputModule(reactContext));
|
||||||
|
|
||||||
return modules;
|
return modules;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||||
return Collections.emptyList();
|
return Arrays.<ViewManager>asList(
|
||||||
|
new RNSelectableTextInputViewManager()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,18 @@
|
||||||
[re-frame.core :as re-frame]
|
[re-frame.core :as re-frame]
|
||||||
[status-im.chat.models.mentions :as mentions]
|
[status-im.chat.models.mentions :as mentions]
|
||||||
[quo2.foundations.colors :as colors]
|
[quo2.foundations.colors :as colors]
|
||||||
[quo.react]))
|
[quo.react]
|
||||||
|
["react-native" :as react-native]
|
||||||
|
[status-im.ui.components.react :as react]
|
||||||
|
[status-im.utils.types :as types]
|
||||||
|
[oops.core :as oops]))
|
||||||
|
|
||||||
(defonce input-texts (atom {}))
|
(defonce input-texts (atom {}))
|
||||||
(defonce mentions-enabled (reagent/atom {}))
|
(defonce mentions-enabled (reagent/atom {}))
|
||||||
(defonce chat-input-key (reagent/atom 1))
|
(defonce chat-input-key (reagent/atom 1))
|
||||||
|
|
||||||
|
(declare selectable-text-input)
|
||||||
|
|
||||||
(re-frame/reg-fx
|
(re-frame/reg-fx
|
||||||
:chat.ui/clear-inputs
|
:chat.ui/clear-inputs
|
||||||
(fn []
|
(fn []
|
||||||
|
@ -64,8 +70,8 @@
|
||||||
|
|
||||||
(defn on-selection-change [timeout-id last-text-change mentionable-users args]
|
(defn on-selection-change [timeout-id last-text-change mentionable-users args]
|
||||||
(let [selection (.-selection ^js (.-nativeEvent ^js args))
|
(let [selection (.-selection ^js (.-nativeEvent ^js args))
|
||||||
start (.-start selection)
|
start (.-start selection)
|
||||||
end (.-end selection)]
|
end (.-end selection)]
|
||||||
;; NOTE(rasom): on iOS we do not dispatch this event immediately
|
;; NOTE(rasom): on iOS we do not dispatch this event immediately
|
||||||
;; because it is needed only in case if selection is changed without
|
;; because it is needed only in case if selection is changed without
|
||||||
;; typing. Timeout might be canceled on `on-change`.
|
;; typing. Timeout might be canceled on `on-change`.
|
||||||
|
@ -91,7 +97,7 @@
|
||||||
mentionable-users]))))
|
mentionable-users]))))
|
||||||
|
|
||||||
(defn on-change [last-text-change timeout-id mentionable-users refs chat-id sending-image args]
|
(defn on-change [last-text-change timeout-id mentionable-users refs chat-id sending-image args]
|
||||||
(let [text (.-text ^js (.-nativeEvent ^js args))
|
(let [text (.-text ^js (.-nativeEvent ^js args))
|
||||||
prev-text (get @input-texts chat-id)]
|
prev-text (get @input-texts chat-id)]
|
||||||
(when (and (seq prev-text) (empty? text) (not sending-image))
|
(when (and (seq prev-text) (empty? text) (not sending-image))
|
||||||
(hide-send refs))
|
(hide-send refs))
|
||||||
|
@ -116,12 +122,12 @@
|
||||||
(>evt [::mentions/calculate-suggestions mentionable-users]))))
|
(>evt [::mentions/calculate-suggestions mentionable-users]))))
|
||||||
|
|
||||||
(defn on-text-input [mentionable-users chat-id args]
|
(defn on-text-input [mentionable-users chat-id args]
|
||||||
(let [native-event (.-nativeEvent ^js args)
|
(let [native-event (.-nativeEvent ^js args)
|
||||||
text (.-text ^js native-event)
|
text (.-text ^js native-event)
|
||||||
previous-text (.-previousText ^js native-event)
|
previous-text (.-previousText ^js native-event)
|
||||||
range (.-range ^js native-event)
|
range (.-range ^js native-event)
|
||||||
start (.-start ^js range)
|
start (.-start ^js range)
|
||||||
end (.-end ^js range)]
|
end (.-end ^js range)]
|
||||||
(when (and (not (get @mentions-enabled chat-id)) (string/index-of text "@"))
|
(when (and (not (get @mentions-enabled chat-id)) (string/index-of text "@"))
|
||||||
(swap! mentions-enabled assoc chat-id true))
|
(swap! mentions-enabled assoc chat-id true))
|
||||||
|
|
||||||
|
@ -140,40 +146,183 @@
|
||||||
(defn text-input [{:keys [set-active-panel refs chat-id sending-image on-content-size-change]}]
|
(defn text-input [{:keys [set-active-panel refs chat-id sending-image on-content-size-change]}]
|
||||||
(let [cooldown-enabled? (<sub [:chats/current-chat-cooldown-enabled?])
|
(let [cooldown-enabled? (<sub [:chats/current-chat-cooldown-enabled?])
|
||||||
mentionable-users (<sub [:chats/mentionable-users])
|
mentionable-users (<sub [:chats/mentionable-users])
|
||||||
timeout-id (atom nil)
|
timeout-id (atom nil)
|
||||||
last-text-change (atom nil)
|
last-text-change (atom nil)
|
||||||
mentions-enabled (get @mentions-enabled chat-id)]
|
mentions-enabled (get @mentions-enabled chat-id)
|
||||||
|
props {:style (style/text-input)
|
||||||
|
:ref (:text-input-ref refs)
|
||||||
|
:max-font-size-multiplier 1
|
||||||
|
:accessibility-label :chat-message-input
|
||||||
|
:text-align-vertical :center
|
||||||
|
:multiline true
|
||||||
|
:editable (not cooldown-enabled?)
|
||||||
|
:blur-on-submit false
|
||||||
|
:auto-focus false
|
||||||
|
:on-focus #(set-active-panel nil)
|
||||||
|
:max-length chat.constants/max-text-size
|
||||||
|
:placeholder-text-color (:text-02 @quo.colors/theme)
|
||||||
|
:placeholder (if cooldown-enabled?
|
||||||
|
(i18n/label :cooldown/text-input-disabled)
|
||||||
|
(i18n/label :t/type-a-message))
|
||||||
|
:underline-color-android :transparent
|
||||||
|
:auto-capitalize :sentences
|
||||||
|
:auto-correct false
|
||||||
|
:spell-check false
|
||||||
|
:on-content-size-change on-content-size-change
|
||||||
|
:on-selection-change (partial on-selection-change timeout-id last-text-change mentionable-users)
|
||||||
|
:on-change (partial on-change last-text-change timeout-id mentionable-users refs chat-id sending-image)
|
||||||
|
:on-text-input (partial on-text-input mentionable-users chat-id)}
|
||||||
|
children (fn []
|
||||||
|
(if mentions-enabled
|
||||||
|
(for [[index [type text]] (map-indexed
|
||||||
|
(fn [idx item]
|
||||||
|
[idx item])
|
||||||
|
(<sub [:chat/input-with-mentions]))]
|
||||||
|
^{:key (str index "_" type "_" text)}
|
||||||
|
[rn/text (when (= type :mention) {:style {:color colors/primary-50}})
|
||||||
|
text])
|
||||||
|
(get @input-texts chat-id)))]
|
||||||
|
;when ios implementation for selectable-text-input is ready, we need remove this condition and use selectable-text-input directly.
|
||||||
|
(if platform/android?
|
||||||
|
[selectable-text-input chat-id props children]
|
||||||
|
[rn/text-input props
|
||||||
|
[children]])))
|
||||||
|
|
||||||
[rn/text-input
|
(defn selectable-text-input-manager []
|
||||||
{:style (style/text-input)
|
(when (exists? (.-NativeModules react-native))
|
||||||
:ref (:text-input-ref refs)
|
(.-RNSelectableTextInputManager ^js (.-NativeModules react-native))))
|
||||||
:max-font-size-multiplier 1
|
|
||||||
:accessibility-label :chat-message-input
|
(defonce rn-selectable-text-input (reagent/adapt-react-class (.requireNativeComponent react-native "RNSelectableTextInput")))
|
||||||
:text-align-vertical :center
|
|
||||||
:multiline true
|
(declare first-level-menu-items second-level-menu-items)
|
||||||
:editable (not cooldown-enabled?)
|
|
||||||
:blur-on-submit false
|
(defn update-input-text [{:keys [text-input chat-id]} text]
|
||||||
:auto-focus false
|
(on-text-change text chat-id)
|
||||||
:on-focus #(set-active-panel nil)
|
(.setNativeProps ^js text-input (clj->js {:text text})))
|
||||||
:max-length chat.constants/max-text-size
|
|
||||||
:placeholder-text-color (:text-02 @quo.colors/theme)
|
(defn calculate-input-text [{:keys [full-text selection-start selection-end]} content]
|
||||||
:placeholder (if cooldown-enabled?
|
(let [head (subs full-text 0 selection-start)
|
||||||
(i18n/label :cooldown/text-input-disabled)
|
tail (subs full-text selection-end)]
|
||||||
(i18n/label :t/type-a-message))
|
(str head content tail)))
|
||||||
:underline-color-android :transparent
|
|
||||||
:auto-capitalize :sentences
|
(defn update-selection [text-input-handle selection-start selection-end]
|
||||||
:auto-correct false
|
;to avoid something disgusting like this https://lightrun.com/answers/facebook-react-native-textinput-controlled-selection-broken-on-both-ios-and-android
|
||||||
:spell-check false
|
;use native invoke instead! do not use setNativeProps! e.g. (.setNativeProps ^js text-input (clj->js {:selection {:start selection-start :end selection-end}}))
|
||||||
:on-content-size-change on-content-size-change
|
(let [manager (selectable-text-input-manager)]
|
||||||
:on-selection-change (partial on-selection-change timeout-id last-text-change mentionable-users)
|
(oops/ocall manager :setSelection text-input-handle selection-start selection-end)))
|
||||||
:on-change (partial on-change last-text-change timeout-id mentionable-users refs chat-id sending-image)
|
|
||||||
:on-text-input (partial on-text-input mentionable-users chat-id)}
|
(def first-level-menus {:cut (fn [{:keys [content] :as params}]
|
||||||
(if mentions-enabled
|
(let [new-text (calculate-input-text params "")]
|
||||||
(for [[idx [type text]] (map-indexed
|
(react/copy-to-clipboard content)
|
||||||
(fn [idx item]
|
(update-input-text params new-text)))
|
||||||
[idx item])
|
|
||||||
(<sub [:chat/input-with-mentions]))]
|
:copy-to-clipboard (fn [{:keys [content]}]
|
||||||
^{:key (str idx "_" type "_" text)}
|
(react/copy-to-clipboard content))
|
||||||
[rn/text (when (= type :mention) {:style {:color colors/primary-50}})
|
|
||||||
text])
|
:paste (fn [params]
|
||||||
(get @input-texts chat-id))]))
|
(let [callback (fn [paste-content]
|
||||||
|
(let [content (string/trim paste-content)
|
||||||
|
new-text (calculate-input-text params content)]
|
||||||
|
(update-input-text params new-text)))]
|
||||||
|
(react/get-from-clipboard callback)))
|
||||||
|
|
||||||
|
:biu (fn [{:keys [first-level text-input-handle menu-items selection-start selection-end]}]
|
||||||
|
(reset! first-level false)
|
||||||
|
(reset! menu-items second-level-menu-items)
|
||||||
|
(update-selection text-input-handle selection-start selection-end))})
|
||||||
|
|
||||||
|
(def first-level-menu-items (map i18n/label (keys first-level-menus)))
|
||||||
|
|
||||||
|
(defn reset-to-first-level-menu [first-level menu-items]
|
||||||
|
(reset! first-level true)
|
||||||
|
(reset! menu-items first-level-menu-items))
|
||||||
|
|
||||||
|
(defn append-markdown-char [{:keys [first-level menu-items content selection-start selection-end text-input-handle selection-event] :as params} wrap-chars]
|
||||||
|
(let [content (str wrap-chars content wrap-chars)
|
||||||
|
new-text (calculate-input-text params content)
|
||||||
|
len-wrap-chars (count wrap-chars)
|
||||||
|
selection-start (+ selection-start len-wrap-chars)
|
||||||
|
selection-end (+ selection-end len-wrap-chars)]
|
||||||
|
;don't update selection directly here, process it within on-selection-change instead
|
||||||
|
;so that we can avoid java.lang.IndexOutOfBoundsException: setSpan..
|
||||||
|
(reset! selection-event {:start selection-start
|
||||||
|
:end selection-end
|
||||||
|
:text-input-handle text-input-handle})
|
||||||
|
(update-input-text params new-text)
|
||||||
|
(reset-to-first-level-menu first-level menu-items)))
|
||||||
|
|
||||||
|
(def second-level-menus {:bold #(append-markdown-char % "**")
|
||||||
|
|
||||||
|
:italic #(append-markdown-char % "*")
|
||||||
|
|
||||||
|
:strikethrough #(append-markdown-char % "~~")})
|
||||||
|
|
||||||
|
(def second-level-menu-items (map i18n/label (keys second-level-menus)))
|
||||||
|
|
||||||
|
(defn on-menu-item-touched [{:keys [first-level event-type] :as params}]
|
||||||
|
(let [menus (if @first-level first-level-menus second-level-menus)
|
||||||
|
menu-item-key (nth (keys menus) event-type)
|
||||||
|
action (get menus menu-item-key)]
|
||||||
|
(action params)))
|
||||||
|
|
||||||
|
(defn selectable-text-input [chat-id {:keys [style ref on-selection-change] :as props} children]
|
||||||
|
(let [text-input-ref (reagent/atom nil)
|
||||||
|
menu-items (reagent/atom first-level-menu-items)
|
||||||
|
first-level (reagent/atom true)
|
||||||
|
selection-event (atom nil)
|
||||||
|
manager (selectable-text-input-manager)]
|
||||||
|
(reagent/create-class
|
||||||
|
{:component-did-mount
|
||||||
|
(fn [this]
|
||||||
|
(when @text-input-ref
|
||||||
|
(let [selectable-text-input-handle (rn/find-node-handle this)
|
||||||
|
text-input-handle (rn/find-node-handle @text-input-ref)]
|
||||||
|
(oops/ocall manager :setupMenuItems selectable-text-input-handle text-input-handle))))
|
||||||
|
|
||||||
|
:component-did-update (fn [_ _ _ _]
|
||||||
|
(when (not @first-level)
|
||||||
|
(let [text-input-handle (rn/find-node-handle @text-input-ref)]
|
||||||
|
(oops/ocall manager :startActionMode text-input-handle))))
|
||||||
|
|
||||||
|
:render
|
||||||
|
(fn [_]
|
||||||
|
(let [ref #(do (reset! text-input-ref %)
|
||||||
|
(when ref
|
||||||
|
(quo.react/set-ref-val! ref %)))
|
||||||
|
on-selection-change (fn [args]
|
||||||
|
(let [selection (.-selection ^js (.-nativeEvent ^js args))
|
||||||
|
start (.-start selection)
|
||||||
|
end (.-end selection)
|
||||||
|
no-selection (<= (- end start) 0)]
|
||||||
|
(when (and no-selection (not @first-level))
|
||||||
|
(oops/ocall manager :hideLastActionMode)
|
||||||
|
(reset-to-first-level-menu first-level menu-items)))
|
||||||
|
(when on-selection-change
|
||||||
|
(on-selection-change args))
|
||||||
|
(when @selection-event
|
||||||
|
(let [{:keys [start end text-input-handle]} @selection-event]
|
||||||
|
(update-selection text-input-handle start end)
|
||||||
|
(reset! selection-event nil))))
|
||||||
|
on-selection (fn [event]
|
||||||
|
(let [native-event (.-nativeEvent event)
|
||||||
|
native-event (types/js->clj native-event)
|
||||||
|
{:keys [eventType content selectionStart selectionEnd]} native-event
|
||||||
|
full-text (:input-text (<sub [:chats/current-chat-inputs]))]
|
||||||
|
(on-menu-item-touched {:first-level first-level
|
||||||
|
:event-type eventType
|
||||||
|
:content content
|
||||||
|
:selection-start selectionStart
|
||||||
|
:selection-end selectionEnd
|
||||||
|
:text-input @text-input-ref
|
||||||
|
:text-input-handle (rn/find-node-handle @text-input-ref)
|
||||||
|
:full-text full-text
|
||||||
|
:menu-items menu-items
|
||||||
|
:chat-id chat-id
|
||||||
|
:selection-event selection-event})))
|
||||||
|
props (merge props {:ref ref
|
||||||
|
:style nil
|
||||||
|
:on-selection-change on-selection-change
|
||||||
|
:on-selection on-selection})]
|
||||||
|
[rn-selectable-text-input {:menuItems @menu-items :style style}
|
||||||
|
[rn/text-input props
|
||||||
|
[children]]]))})))
|
||||||
|
|
|
@ -1862,5 +1862,10 @@
|
||||||
"mark-user-untrustworthy": "Mark {{username}} as untrustworthy",
|
"mark-user-untrustworthy": "Mark {{username}} as untrustworthy",
|
||||||
"leave-group?": "Leave Group?",
|
"leave-group?": "Leave Group?",
|
||||||
"block-user?": "Block User?",
|
"block-user?": "Block User?",
|
||||||
"clear-history?": "Clear History?"
|
"clear-history?": "Clear History?",
|
||||||
|
"cut": "Cut",
|
||||||
|
"biu": "BIU",
|
||||||
|
"bold": "Bold",
|
||||||
|
"italic": "Italic",
|
||||||
|
"strikethrough": "Strikethrough"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue