Skip to content

Commit 4bd73b6

Browse files
committed
Merge branch 'main' into ios-mentions
2 parents bfd451a + 5b0da81 commit 4bd73b6

13 files changed

Lines changed: 299 additions & 83 deletions

android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorView.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,11 +374,11 @@ class ReactNativeRichTextEditorView : AppCompatEditText {
374374
parametrizedStyles?.startMention(indicator)
375375
}
376376

377-
fun addMention(text: String, attributes: Map<String, String>) {
377+
fun addMention(indicator: String, text: String, attributes: Map<String, String>) {
378378
val isValid = verifyStyle(EditorSpans.MENTION)
379379
if (!isValid) return
380380

381-
parametrizedStyles?.setMentionSpan(text, attributes)
381+
parametrizedStyles?.setMentionSpan(text, indicator, attributes)
382382
}
383383

384384
// Update shadow node's state in order to recalculate layout

android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorViewManager.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.swmansion.reactnativerichtexteditor
22

33
import android.content.Context
4-
import com.facebook.react.bridge.ReactContext
54
import com.facebook.react.bridge.ReadableArray
65
import com.facebook.react.bridge.ReadableMap
76
import com.facebook.react.module.annotations.ReactModule
@@ -247,9 +246,9 @@ class ReactNativeRichTextEditorViewManager : SimpleViewManager<ReactNativeRichTe
247246
view?.startMention(indicator)
248247
}
249248

250-
override fun addMention(view: ReactNativeRichTextEditorView?, text: String, payload: String) {
249+
override fun addMention(view: ReactNativeRichTextEditorView?, indicator: String, text: String, payload: String) {
251250
val attributes = jsonStringToStringMap(payload)
252-
view?.addMention(text, attributes)
251+
view?.addMention(text, indicator, attributes)
253252
}
254253

255254
override fun measure(

android/src/main/java/com/swmansion/reactnativerichtexteditor/spans/EditorMentionSpan.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import android.view.View
66
import com.swmansion.reactnativerichtexteditor.spans.interfaces.EditorInlineSpan
77
import com.swmansion.reactnativerichtexteditor.styles.RichTextStyle
88

9-
class EditorMentionSpan(private val text: String, private val attributes: Map<String, String>, private val richTextStyle: RichTextStyle) :
9+
class EditorMentionSpan(private val text: String, private val indicator: String, private val attributes: Map<String, String>, private val richTextStyle: RichTextStyle) :
1010
ClickableSpan(), EditorInlineSpan {
1111
override fun onClick(view: View) {
1212
// Do nothing. Mentions inside the editor are not clickable.
@@ -16,9 +16,10 @@ class EditorMentionSpan(private val text: String, private val attributes: Map<St
1616
override fun updateDrawState(textPaint: TextPaint) {
1717
super.updateDrawState(textPaint)
1818

19-
textPaint.color = richTextStyle.mentionColor
20-
textPaint.bgColor = richTextStyle.mentionBackgroundColor
21-
textPaint.isUnderlineText = richTextStyle.mentionUnderline
19+
val mentionsStyle = richTextStyle.mentionsStyle[indicator] ?: return
20+
textPaint.color = mentionsStyle.color
21+
textPaint.bgColor = mentionsStyle.backgroundColor
22+
textPaint.isUnderlineText = mentionsStyle.underline
2223
}
2324

2425
fun getAttributes(): Map<String, String> {
@@ -28,4 +29,8 @@ class EditorMentionSpan(private val text: String, private val attributes: Map<St
2829
fun getText(): String {
2930
return text
3031
}
32+
33+
fun getIndicator(): String {
34+
return indicator
35+
}
3136
}

android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParametrizedStyles.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ class ParametrizedStyles(private val editorView: ReactNativeRichTextEditorView)
146146
}
147147
}
148148

149-
fun setMentionSpan(text: String, attributes: Map<String, String>) {
149+
fun setMentionSpan(indicator: String, text: String, attributes: Map<String, String>) {
150150
val selection = editorView.selection ?: return
151151

152152
val spannable = editorView.text as SpannableStringBuilder
@@ -160,7 +160,7 @@ class ParametrizedStyles(private val editorView: ReactNativeRichTextEditorView)
160160
var start = mentionStart ?: return
161161
spannable.replace(start, selectionEnd, text)
162162

163-
val span = EditorMentionSpan(text, attributes, editorView.richTextStyle)
163+
val span = EditorMentionSpan(text, indicator, attributes, editorView.richTextStyle)
164164
val spanEnd = start + text.length
165165
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd)
166166
spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/RichTextStyle.kt

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ class RichTextStyle {
4747
var inlineCodeColor: Int = Color.BLACK
4848
var inlineCodeBackgroundColor: Int = Color.BLACK
4949

50-
var mentionColor: Int = Color.BLACK
51-
var mentionBackgroundColor: Int = Color.BLACK
52-
var mentionUnderline: Boolean = true
50+
var mentionsStyle: MutableMap<String, MentionStyle> = mutableMapOf()
5351

5452
constructor(editorView: ReactNativeRichTextEditorView, style: ReadableMap?) {
5553
this.editorView = editorView
@@ -105,9 +103,7 @@ class RichTextStyle {
105103
inlineCodeBackgroundColor = parseColorWithOpacity(inlineCodeStyle, "backgroundColor", 80)
106104

107105
val mentionStyle = style.getMap("mention")
108-
mentionUnderline = parseIsUnderline(mentionStyle)
109-
mentionColor = parseColor(mentionStyle, "color")
110-
mentionBackgroundColor = parseColorWithOpacity(mentionStyle, "backgroundColor", 80)
106+
mentionsStyle = parseMentionsStyle(mentionStyle)
111107
}
112108

113109
private fun parseFloat(map: ReadableMap?, key: String): Float {
@@ -163,4 +159,34 @@ class RichTextStyle {
163159

164160
return map
165161
}
162+
163+
private fun parseMentionsStyle(mentionsStyle: ReadableMap?): MutableMap<String, MentionStyle> {
164+
if (mentionsStyle == null) throw Error("Mentions style cannot be null")
165+
166+
val parsedMentionsStyle: MutableMap<String, MentionStyle> = mutableMapOf()
167+
168+
val iterator = mentionsStyle.keySetIterator()
169+
while (iterator.hasNextKey()) {
170+
val key = iterator.nextKey()
171+
val value = mentionsStyle.getMap(key)
172+
173+
if (value == null) throw Error("Mention style for key '$key' cannot be null")
174+
175+
val color = parseColor(value, "color")
176+
val backgroundColor = parseColorWithOpacity(value, "backgroundColor", 80)
177+
val isUnderline = parseIsUnderline(value)
178+
val parsedStyle = MentionStyle(color, backgroundColor, isUnderline)
179+
parsedMentionsStyle.put(key, parsedStyle)
180+
}
181+
182+
return parsedMentionsStyle
183+
}
184+
185+
companion object {
186+
data class MentionStyle(
187+
val color: Int,
188+
val backgroundColor: Int,
189+
val underline: Boolean
190+
)
191+
}
166192
}

android/src/main/java/com/swmansion/reactnativerichtexteditor/utils/EditorParser.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,10 @@ private static void withinParagraph(StringBuilder out, Spanned text, int start,
460460
out.append(((EditorMentionSpan) style[j]).getText());
461461
out.append("\"");
462462

463+
out.append(" indicator=\"");
464+
out.append(((EditorMentionSpan) style[j]).getIndicator());
465+
out.append("\"");
466+
463467
Map<String, String> attributes = ((EditorMentionSpan) style[j]).getAttributes();
464468
for (Map.Entry<String, String> entry : attributes.entrySet()) {
465469
out.append(" ");
@@ -1064,15 +1068,18 @@ private void endA(Editable text, RichTextStyle style) {
10641068

10651069
private static void startMention(Editable mention, Attributes attributes) {
10661070
String text = attributes.getValue("", "text");
1071+
String indicator = attributes.getValue("", "indicator");
10671072

10681073
Map<String, String> attributesMap = new HashMap<>();
10691074
for (int i = 0; i < attributes.getLength(); i++) {
1070-
if (!"text".equals(attributes.getLocalName(i))) {
1071-
attributesMap.put(attributes.getLocalName(i), attributes.getValue(i));
1075+
String localName = attributes.getLocalName(i);
1076+
1077+
if (!"text".equals(localName) && !"indicator".equals(localName)) {
1078+
attributesMap.put(localName, attributes.getValue(i));
10721079
}
10731080
}
10741081

1075-
start(mention, new Mention(text, attributesMap));
1082+
start(mention, new Mention(indicator, text, attributesMap));
10761083
}
10771084

10781085
private void endMention(Editable text, RichTextStyle style) {
@@ -1081,7 +1088,7 @@ private void endMention(Editable text, RichTextStyle style) {
10811088
if (m == null) return;
10821089
if (m.mText == null) return;
10831090

1084-
setSpanFromMark(text, m, new EditorMentionSpan(m.mText, m.mAttributes, style));
1091+
setSpanFromMark(text, m, new EditorMentionSpan(m.mText, m.mIndicator, m.mAttributes, style));
10851092
}
10861093

10871094
private int getHtmlColor(String color) {
@@ -1204,9 +1211,11 @@ public List(String type, int index) {
12041211

12051212
private static class Mention {
12061213
public Map<String, String> mAttributes;
1214+
public String mIndicator;
12071215
public String mText;
12081216

1209-
public Mention(String text, Map<String, String> attributes) {
1217+
public Mention(String indicator, String text, Map<String, String> attributes) {
1218+
mIndicator = indicator;
12101219
mAttributes = attributes;
12111220
mText = text;
12121221
}

example/src/App.tsx

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import { Button } from './components/Button';
2222
import { Toolbar } from './components/Toolbar';
2323
import { LinkModal } from './components/LinkModal';
2424
import { launchImageLibrary } from 'react-native-image-picker';
25-
import { MentionPopup } from './components/MentionPopup';
26-
import { type MentionItem, useMention } from './useMention';
25+
import { type MentionItem, MentionPopup } from './components/MentionPopup';
26+
import { useUserMention } from './useUserMention';
27+
import { useChannelMention } from './useChannelMention';
2728

2829
type StylesState = OnChangeStateEvent;
2930

@@ -61,7 +62,8 @@ const DEFAULT_LINK_STATE = {
6162
const DEBUG_SCROLLABLE = false;
6263

6364
export default function App() {
64-
const [isMentionPopupOpen, setIsMentionPopupOpen] = useState(false);
65+
const [isChannelPopupOpen, setIsChannelPopupOpen] = useState(false);
66+
const [isUserPopupOpen, setIsUserPopupOpen] = useState(false);
6567
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
6668

6769
const [selection, setSelection] = useState<Selection>();
@@ -72,7 +74,8 @@ export default function App() {
7274

7375
const ref = useRef<RichTextInputInstance>(null);
7476

75-
const { mentionData, onMentionChange } = useMention();
77+
const userMention = useUserMention();
78+
const channelMention = useChannelMention();
7679

7780
const handleChangeText = (e: NativeSyntheticEvent<OnChangeTextEvent>) => {
7881
console.log('Text changed:', e?.nativeEvent.value);
@@ -106,13 +109,39 @@ export default function App() {
106109
setIsLinkModalOpen(false);
107110
};
108111

109-
const openMentionPopup = () => {
110-
setIsMentionPopupOpen(true);
112+
const openUserMentionPopup = () => {
113+
setIsUserPopupOpen(true);
111114
};
112115

113-
const closeMentionPopup = () => {
114-
setIsMentionPopupOpen(false);
115-
onMentionChange('');
116+
const closeUserMentionPopup = () => {
117+
setIsUserPopupOpen(false);
118+
userMention.onMentionChange('');
119+
};
120+
121+
const openChannelMentionPopup = () => {
122+
setIsChannelPopupOpen(true);
123+
};
124+
125+
const closeChannelMentionPopup = () => {
126+
setIsChannelPopupOpen(false);
127+
channelMention.onMentionChange('');
128+
};
129+
130+
const handleStartMention = (indicator: string) => {
131+
indicator === '@' ? openUserMentionPopup() : openChannelMentionPopup();
132+
};
133+
134+
const handleEndMention = (indicator: string) => {
135+
const isUserMention = indicator === '@';
136+
137+
if (isUserMention) {
138+
closeUserMentionPopup();
139+
userMention.onMentionChange('');
140+
return;
141+
}
142+
143+
closeChannelMentionPopup();
144+
channelMention.onMentionChange('');
116145
};
117146

118147
const submitLink = (text: string, url: string) => {
@@ -134,20 +163,26 @@ export default function App() {
134163
ref.current?.setImage(imageUri);
135164
};
136165

137-
const handleChangeMention = ({ text }: OnChangeMentionEvent) => {
138-
if (!isMentionPopupOpen) {
139-
openMentionPopup();
140-
}
141-
142-
onMentionChange(text);
166+
const handleChangeMention = ({ indicator, text }: OnChangeMentionEvent) => {
167+
indicator === '@'
168+
? userMention.onMentionChange(text)
169+
: channelMention.onMentionChange(text);
143170
};
144171

145-
const handleMentionSelected = (item: MentionItem) => {
146-
ref.current?.setMention(`@${item.name}`, {
172+
const handleUserMentionSelected = (item: MentionItem) => {
173+
ref.current?.setMention('@', `@${item.name}`, {
147174
id: item.id,
148175
type: 'user',
149176
});
150-
closeMentionPopup();
177+
closeUserMentionPopup();
178+
};
179+
180+
const handleChannelMentionSelected = (item: MentionItem) => {
181+
ref.current?.setMention('#', `#${item.name}`, {
182+
id: item.id,
183+
type: 'channel',
184+
});
185+
closeChannelMentionPopup();
151186
};
152187

153188
const handleFocusEvent = () => {
@@ -188,9 +223,9 @@ export default function App() {
188223
onChangeState={handleChangeState}
189224
onLinkDetected={setCurrentLink}
190225
onMentionDetected={console.log}
191-
onStartMention={openMentionPopup}
226+
onStartMention={handleStartMention}
192227
onChangeMention={handleChangeMention}
193-
onEndMention={closeMentionPopup}
228+
onEndMention={handleEndMention}
194229
onFocus={handleFocusEvent}
195230
onBlur={handleBlurEvent}
196231
onChangeSelection={handleSelectionChangeEvent}
@@ -219,9 +254,16 @@ export default function App() {
219254
onClose={closeLinkModal}
220255
/>
221256
<MentionPopup
222-
data={mentionData}
223-
isOpen={isMentionPopupOpen}
224-
onItemPress={handleMentionSelected}
257+
variant="user"
258+
data={userMention.data}
259+
isOpen={isUserPopupOpen}
260+
onItemPress={handleUserMentionSelected}
261+
/>
262+
<MentionPopup
263+
variant="channel"
264+
data={channelMention.data}
265+
isOpen={isChannelPopupOpen}
266+
onItemPress={handleChannelMentionSelected}
225267
/>
226268
</>
227269
);
@@ -256,9 +298,16 @@ const richTextStyles: RichTextStyle = {
256298
textDecorationLine: 'underline',
257299
},
258300
mention: {
259-
color: 'red',
260-
backgroundColor: 'lightyellow',
261-
textDecorationLine: 'underline',
301+
'#': {
302+
color: 'blue',
303+
backgroundColor: 'lightblue',
304+
textDecorationLine: 'underline',
305+
},
306+
'@': {
307+
color: 'green',
308+
backgroundColor: 'lightgreen',
309+
textDecorationLine: 'none',
310+
},
262311
},
263312
img: {
264313
width: 50,

0 commit comments

Comments
 (0)