@droppii-org/chat-mobile 0.2.80 → 0.2.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/lib/module/components/ThreadCard/MentionBadge.js +24 -0
  2. package/lib/module/components/ThreadCard/MentionBadge.js.map +1 -0
  3. package/lib/module/components/ThreadCard/ThreadCard.js +5 -1
  4. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  5. package/lib/module/components/messages/linkMessage/LinkPreviewCard.js +67 -0
  6. package/lib/module/components/messages/linkMessage/LinkPreviewCard.js.map +1 -0
  7. package/lib/module/components/messages/linkMessage/index.js +21 -68
  8. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  9. package/lib/module/components/messages/quotedMessage/QuotedMessagePreviewStrip.js +152 -0
  10. package/lib/module/components/messages/quotedMessage/QuotedMessagePreviewStrip.js.map +1 -0
  11. package/lib/module/components/messages/textMessage/index.js +102 -8
  12. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  13. package/lib/module/config/api-endpoints.js +2 -1
  14. package/lib/module/config/api-endpoints.js.map +1 -1
  15. package/lib/module/core/useChatListener.js +1 -1
  16. package/lib/module/core/useChatListener.js.map +1 -1
  17. package/lib/module/hooks/contacts/useSearchContacts.js +50 -0
  18. package/lib/module/hooks/contacts/useSearchContacts.js.map +1 -0
  19. package/lib/module/hooks/message/useSendMessage.js +64 -3
  20. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  21. package/lib/module/hooks/query-keys.js +8 -0
  22. package/lib/module/hooks/query-keys.js.map +1 -1
  23. package/lib/module/hooks/useChatCompose.js +3 -2
  24. package/lib/module/hooks/useChatCompose.js.map +1 -1
  25. package/lib/module/screens/SearchConversation/ContactResultItem.js +119 -0
  26. package/lib/module/screens/SearchConversation/ContactResultItem.js.map +1 -0
  27. package/lib/module/screens/SearchConversation/SearchAllResult.js +26 -20
  28. package/lib/module/screens/SearchConversation/SearchAllResult.js.map +1 -1
  29. package/lib/module/screens/SearchConversation/SearchCommunityResult.js +79 -0
  30. package/lib/module/screens/SearchConversation/SearchCommunityResult.js.map +1 -0
  31. package/lib/module/screens/SearchConversation/SearchContactsResult.js +117 -0
  32. package/lib/module/screens/SearchConversation/SearchContactsResult.js.map +1 -0
  33. package/lib/module/screens/SearchConversation/SearchEmpty.js +2 -0
  34. package/lib/module/screens/SearchConversation/SearchEmpty.js.map +1 -1
  35. package/lib/module/screens/SearchConversation/SearchMessagesResult.js +1 -8
  36. package/lib/module/screens/SearchConversation/SearchMessagesResult.js.map +1 -1
  37. package/lib/module/screens/SearchConversation/SearchResultItem.js +2 -0
  38. package/lib/module/screens/SearchConversation/SearchResultItem.js.map +1 -1
  39. package/lib/module/screens/SearchConversation/TabResult.js +24 -7
  40. package/lib/module/screens/SearchConversation/TabResult.js.map +1 -1
  41. package/lib/module/screens/SearchConversation/index.js +6 -2
  42. package/lib/module/screens/SearchConversation/index.js.map +1 -1
  43. package/lib/module/screens/SearchConversation/types.js +1 -0
  44. package/lib/module/screens/SearchConversation/types.js.map +1 -1
  45. package/lib/module/screens/chat-detail/ChatComposer.js +170 -48
  46. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  47. package/lib/module/screens/chat-detail/ChatListLegend.js +1 -0
  48. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  49. package/lib/module/screens/chat-detail/components/ChatMessageInput.js +23 -2
  50. package/lib/module/screens/chat-detail/components/ChatMessageInput.js.map +1 -1
  51. package/lib/module/screens/chat-detail/components/MentionHighlightAdapter.js +51 -0
  52. package/lib/module/screens/chat-detail/components/MentionHighlightAdapter.js.map +1 -0
  53. package/lib/module/screens/chat-detail/components/MentionSuggestionItem.js +49 -0
  54. package/lib/module/screens/chat-detail/components/MentionSuggestionItem.js.map +1 -0
  55. package/lib/module/screens/chat-detail/components/MentionSuggestionList.js +114 -0
  56. package/lib/module/screens/chat-detail/components/MentionSuggestionList.js.map +1 -0
  57. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +8 -0
  58. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
  59. package/lib/module/screens/chat-detail/hooks/useMentionComposer.js +251 -0
  60. package/lib/module/screens/chat-detail/hooks/useMentionComposer.js.map +1 -0
  61. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +5 -1
  62. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  63. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +11 -1
  64. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -1
  65. package/lib/module/services/endpoints.js +2 -1
  66. package/lib/module/services/endpoints.js.map +1 -1
  67. package/lib/module/services/mentionSearch.js +49 -0
  68. package/lib/module/services/mentionSearch.js.map +1 -0
  69. package/lib/module/services/message.js +6 -1
  70. package/lib/module/services/message.js.map +1 -1
  71. package/lib/module/translation/resources/i18n.js +11 -0
  72. package/lib/module/translation/resources/i18n.js.map +1 -1
  73. package/lib/module/types/chat.js +8 -0
  74. package/lib/module/types/chat.js.map +1 -1
  75. package/lib/module/types/mention.js +2 -0
  76. package/lib/module/types/mention.js.map +1 -0
  77. package/lib/module/utils/dateTimeUtils.js +10 -0
  78. package/lib/module/utils/dateTimeUtils.js.map +1 -1
  79. package/lib/module/utils/highlightSearch.js +6 -2
  80. package/lib/module/utils/highlightSearch.js.map +1 -1
  81. package/lib/module/utils/mentionSerializer.js +113 -0
  82. package/lib/module/utils/mentionSerializer.js.map +1 -0
  83. package/lib/module/utils/mentions/index.js +7 -0
  84. package/lib/module/utils/mentions/index.js.map +1 -0
  85. package/lib/module/utils/mentions/mentionDetector.js +49 -0
  86. package/lib/module/utils/mentions/mentionDetector.js.map +1 -0
  87. package/lib/module/utils/mentions/mentionDiff.js +66 -0
  88. package/lib/module/utils/mentions/mentionDiff.js.map +1 -0
  89. package/lib/module/utils/mentions/mentionRange.js +188 -0
  90. package/lib/module/utils/mentions/mentionRange.js.map +1 -0
  91. package/lib/module/utils/mentions/mentionTokenize.js +50 -0
  92. package/lib/module/utils/mentions/mentionTokenize.js.map +1 -0
  93. package/lib/module/utils/messageUtils.js +16 -3
  94. package/lib/module/utils/messageUtils.js.map +1 -1
  95. package/lib/module/utils/url.js +33 -0
  96. package/lib/module/utils/url.js.map +1 -1
  97. package/lib/typescript/src/components/ThreadCard/MentionBadge.d.ts +7 -0
  98. package/lib/typescript/src/components/ThreadCard/MentionBadge.d.ts.map +1 -0
  99. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  100. package/lib/typescript/src/components/messages/linkMessage/LinkPreviewCard.d.ts +16 -0
  101. package/lib/typescript/src/components/messages/linkMessage/LinkPreviewCard.d.ts.map +1 -0
  102. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  103. package/lib/typescript/src/components/messages/quotedMessage/QuotedMessagePreviewStrip.d.ts +13 -0
  104. package/lib/typescript/src/components/messages/quotedMessage/QuotedMessagePreviewStrip.d.ts.map +1 -0
  105. package/lib/typescript/src/components/messages/textMessage/index.d.ts +3 -1
  106. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  107. package/lib/typescript/src/config/api-endpoints.d.ts +1 -0
  108. package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
  109. package/lib/typescript/src/hooks/contacts/useSearchContacts.d.ts +221 -0
  110. package/lib/typescript/src/hooks/contacts/useSearchContacts.d.ts.map +1 -0
  111. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +5 -2
  112. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  113. package/lib/typescript/src/hooks/query-keys.d.ts +8 -0
  114. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  115. package/lib/typescript/src/hooks/useChatCompose.d.ts +2 -1
  116. package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -1
  117. package/lib/typescript/src/screens/SearchConversation/ContactResultItem.d.ts +9 -0
  118. package/lib/typescript/src/screens/SearchConversation/ContactResultItem.d.ts.map +1 -0
  119. package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts +3 -2
  120. package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts.map +1 -1
  121. package/lib/typescript/src/screens/SearchConversation/SearchCommunityResult.d.ts +7 -0
  122. package/lib/typescript/src/screens/SearchConversation/SearchCommunityResult.d.ts.map +1 -0
  123. package/lib/typescript/src/screens/SearchConversation/SearchContactsResult.d.ts +8 -0
  124. package/lib/typescript/src/screens/SearchConversation/SearchContactsResult.d.ts.map +1 -0
  125. package/lib/typescript/src/screens/SearchConversation/SearchEmpty.d.ts.map +1 -1
  126. package/lib/typescript/src/screens/SearchConversation/SearchMessagesResult.d.ts.map +1 -1
  127. package/lib/typescript/src/screens/SearchConversation/SearchResultItem.d.ts.map +1 -1
  128. package/lib/typescript/src/screens/SearchConversation/TabResult.d.ts +4 -2
  129. package/lib/typescript/src/screens/SearchConversation/TabResult.d.ts.map +1 -1
  130. package/lib/typescript/src/screens/SearchConversation/index.d.ts +4 -2
  131. package/lib/typescript/src/screens/SearchConversation/index.d.ts.map +1 -1
  132. package/lib/typescript/src/screens/SearchConversation/types.d.ts +2 -1
  133. package/lib/typescript/src/screens/SearchConversation/types.d.ts.map +1 -1
  134. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  135. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  136. package/lib/typescript/src/screens/chat-detail/components/ChatMessageInput.d.ts +10 -2
  137. package/lib/typescript/src/screens/chat-detail/components/ChatMessageInput.d.ts.map +1 -1
  138. package/lib/typescript/src/screens/chat-detail/components/MentionHighlightAdapter.d.ts +29 -0
  139. package/lib/typescript/src/screens/chat-detail/components/MentionHighlightAdapter.d.ts.map +1 -0
  140. package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionItem.d.ts +8 -0
  141. package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionItem.d.ts.map +1 -0
  142. package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionList.d.ts +12 -0
  143. package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionList.d.ts.map +1 -0
  144. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +1 -1
  145. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
  146. package/lib/typescript/src/screens/chat-detail/hooks/useMentionComposer.d.ts +48 -0
  147. package/lib/typescript/src/screens/chat-detail/hooks/useMentionComposer.d.ts.map +1 -0
  148. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  149. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -1
  150. package/lib/typescript/src/services/endpoints.d.ts +1 -0
  151. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  152. package/lib/typescript/src/services/mentionSearch.d.ts +15 -0
  153. package/lib/typescript/src/services/mentionSearch.d.ts.map +1 -0
  154. package/lib/typescript/src/services/message.d.ts +1 -1
  155. package/lib/typescript/src/services/message.d.ts.map +1 -1
  156. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  157. package/lib/typescript/src/types/chat.d.ts +50 -0
  158. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  159. package/lib/typescript/src/types/mention.d.ts +65 -0
  160. package/lib/typescript/src/types/mention.d.ts.map +1 -0
  161. package/lib/typescript/src/utils/dateTimeUtils.d.ts +2 -0
  162. package/lib/typescript/src/utils/dateTimeUtils.d.ts.map +1 -1
  163. package/lib/typescript/src/utils/highlightSearch.d.ts.map +1 -1
  164. package/lib/typescript/src/utils/mentionSerializer.d.ts +78 -0
  165. package/lib/typescript/src/utils/mentionSerializer.d.ts.map +1 -0
  166. package/lib/typescript/src/utils/mentions/index.d.ts +5 -0
  167. package/lib/typescript/src/utils/mentions/index.d.ts.map +1 -0
  168. package/lib/typescript/src/utils/mentions/mentionDetector.d.ts +18 -0
  169. package/lib/typescript/src/utils/mentions/mentionDetector.d.ts.map +1 -0
  170. package/lib/typescript/src/utils/mentions/mentionDiff.d.ts +20 -0
  171. package/lib/typescript/src/utils/mentions/mentionDiff.d.ts.map +1 -0
  172. package/lib/typescript/src/utils/mentions/mentionRange.d.ts +86 -0
  173. package/lib/typescript/src/utils/mentions/mentionRange.d.ts.map +1 -0
  174. package/lib/typescript/src/utils/mentions/mentionTokenize.d.ts +29 -0
  175. package/lib/typescript/src/utils/mentions/mentionTokenize.d.ts.map +1 -0
  176. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  177. package/lib/typescript/src/utils/url.d.ts +12 -0
  178. package/lib/typescript/src/utils/url.d.ts.map +1 -1
  179. package/package.json +3 -1
  180. package/src/build-ignore.d.ts +2 -0
  181. package/src/components/ThreadCard/MentionBadge.tsx +28 -0
  182. package/src/components/ThreadCard/ThreadCard.tsx +3 -0
  183. package/src/components/messages/linkMessage/LinkPreviewCard.tsx +92 -0
  184. package/src/components/messages/linkMessage/index.tsx +25 -89
  185. package/src/components/messages/quotedMessage/QuotedMessagePreviewStrip.tsx +168 -0
  186. package/src/components/messages/textMessage/index.tsx +124 -4
  187. package/src/config/api-endpoints.ts +2 -0
  188. package/src/core/useChatListener.ts +1 -1
  189. package/src/hooks/contacts/useSearchContacts.ts +66 -0
  190. package/src/hooks/message/useSendMessage.ts +60 -1
  191. package/src/hooks/query-keys.ts +12 -0
  192. package/src/hooks/useChatCompose.ts +115 -107
  193. package/src/screens/SearchConversation/ContactResultItem.tsx +128 -0
  194. package/src/screens/SearchConversation/SearchAllResult.tsx +38 -24
  195. package/src/screens/SearchConversation/SearchCommunityResult.tsx +96 -0
  196. package/src/screens/SearchConversation/SearchContactsResult.tsx +155 -0
  197. package/src/screens/SearchConversation/SearchEmpty.tsx +2 -0
  198. package/src/screens/SearchConversation/SearchMessagesResult.tsx +1 -6
  199. package/src/screens/SearchConversation/SearchResultItem.tsx +6 -1
  200. package/src/screens/SearchConversation/TabResult.tsx +40 -5
  201. package/src/screens/SearchConversation/index.tsx +10 -2
  202. package/src/screens/SearchConversation/types.tsx +1 -0
  203. package/src/screens/chat-detail/ChatComposer.tsx +201 -67
  204. package/src/screens/chat-detail/ChatListLegend.tsx +1 -0
  205. package/src/screens/chat-detail/components/ChatMessageInput.tsx +35 -2
  206. package/src/screens/chat-detail/components/MentionHighlightAdapter.tsx +57 -0
  207. package/src/screens/chat-detail/components/MentionSuggestionItem.tsx +53 -0
  208. package/src/screens/chat-detail/components/MentionSuggestionList.tsx +133 -0
  209. package/src/screens/chat-detail/hooks/useChatComposerState.ts +7 -1
  210. package/src/screens/chat-detail/hooks/useMentionComposer.ts +344 -0
  211. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +7 -1
  212. package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +22 -4
  213. package/src/services/endpoints.ts +1 -0
  214. package/src/services/mentionSearch.ts +57 -0
  215. package/src/services/message.ts +9 -1
  216. package/src/translation/resources/i18n.ts +12 -0
  217. package/src/types/chat.ts +52 -0
  218. package/src/types/mention.ts +70 -0
  219. package/src/utils/dateTimeUtils.ts +15 -0
  220. package/src/utils/highlightSearch.ts +12 -2
  221. package/src/utils/mentionSerializer.ts +155 -0
  222. package/src/utils/mentions/index.ts +16 -0
  223. package/src/utils/mentions/mentionDetector.ts +58 -0
  224. package/src/utils/mentions/mentionDiff.ts +95 -0
  225. package/src/utils/mentions/mentionRange.ts +207 -0
  226. package/src/utils/mentions/mentionTokenize.ts +53 -0
  227. package/src/utils/messageUtils.ts +21 -10
  228. package/src/utils/url.ts +30 -0
@@ -0,0 +1,207 @@
1
+ import type { ActiveMention, Mention } from '../../types/mention';
2
+ import { diffTextChange } from './mentionDiff';
3
+
4
+ export interface InsertMentionResult {
5
+ text: string;
6
+ mentions: Mention[];
7
+ cursor: number;
8
+ }
9
+
10
+ let mentionIdCounter = 0;
11
+ const nextMentionId = () => `mention-${Date.now()}-${mentionIdCounter++}`;
12
+
13
+ /**
14
+ * Replaces the active "@query" range with "@DisplayName " and commits a new
15
+ * Mention. The trailing space is deliberately excluded from the mention's
16
+ * [start,end) range — it's a plain separator, not part of the token. This
17
+ * keeps the data model simple (no "our space" vs "user-typed space"
18
+ * special-casing) at the cost of removing a mention immediately after
19
+ * insertion taking two backspaces (first eats the space, second — now
20
+ * touching the mention's actual range — triggers atomic removal). Matches
21
+ * the spec's own fixture: "Hello @Thinh" with NO trailing space, cursor at
22
+ * the end, backspace removes the whole mention in one step, since there's
23
+ * no space to consume first.
24
+ */
25
+ export function insertMention(
26
+ text: string,
27
+ mentions: Mention[],
28
+ activeMention: ActiveMention,
29
+ selected: { userId: string; displayName: string }
30
+ ): InsertMentionResult {
31
+ const { start, end } = activeMention;
32
+ const inserted = `@${selected.displayName} `;
33
+ const newText = text.slice(0, start) + inserted + text.slice(end);
34
+ const delta = inserted.length - (end - start);
35
+
36
+ const newMention: Mention = {
37
+ id: nextMentionId(),
38
+ userId: selected.userId,
39
+ displayName: selected.displayName,
40
+ start,
41
+ end: start + 1 + selected.displayName.length,
42
+ };
43
+
44
+ // m.start >= end is safe: an existing committed mention can never overlap
45
+ // an activeMention's range — findActiveMention's '@'/boundary bail-out
46
+ // guarantees the active query never reaches back into a prior mention.
47
+ const shifted = mentions.map((m) =>
48
+ m.start >= end ? { ...m, start: m.start + delta, end: m.end + delta } : m
49
+ );
50
+
51
+ const nextMentions = [...shifted, newMention].sort(
52
+ (a, b) => a.start - b.start
53
+ );
54
+ const cursor = newMention.end + 1; // after the trailing space
55
+
56
+ return { text: newText, mentions: nextMentions, cursor };
57
+ }
58
+
59
+ export interface ReconcileMentionsResult {
60
+ mentions: Mention[];
61
+ invalidated: Mention[];
62
+ }
63
+
64
+ /**
65
+ * Re-derives mention ranges after an edit, given only the before/after full
66
+ * strings (RN's onChangeText carries no delta). Diffs the two strings to
67
+ * localize the changed region, then per mention: fully before the change ->
68
+ * kept as-is; fully after -> shifted by the length delta; any overlap at
69
+ * all -> invalidated (dropped, never left partially stale).
70
+ *
71
+ * Boundary semantics matter here: `m.end <= removedStart` (touching-but-
72
+ * before) is "kept", and `m.start >= removedEnd` (touching-but-after) is
73
+ * "shift" — so typing immediately after a mention, or deleting text
74
+ * strictly before one, never invalidates it. Anything else, even a
75
+ * 1-character overlap, does.
76
+ */
77
+ export function reconcileMentions({
78
+ previousText,
79
+ nextText,
80
+ mentions,
81
+ }: {
82
+ previousText: string;
83
+ nextText: string;
84
+ mentions: Mention[];
85
+ }): ReconcileMentionsResult {
86
+ if (nextText === previousText || mentions.length === 0) {
87
+ return { mentions, invalidated: [] };
88
+ }
89
+
90
+ const { removedStart, removedEnd } = diffTextChange(previousText, nextText);
91
+ const delta = nextText.length - previousText.length;
92
+
93
+ const kept: Mention[] = [];
94
+ const invalidated: Mention[] = [];
95
+
96
+ for (const m of mentions) {
97
+ if (m.end <= removedStart) {
98
+ kept.push(m);
99
+ } else if (m.start >= removedEnd) {
100
+ kept.push({ ...m, start: m.start + delta, end: m.end + delta });
101
+ } else {
102
+ invalidated.push(m);
103
+ }
104
+ }
105
+
106
+ return { mentions: kept, invalidated };
107
+ }
108
+
109
+ /**
110
+ * Expands a delete/replace range so it fully swallows any mention it
111
+ * merely intersects — this is what makes "atomic delete" actually happen.
112
+ * RN's Old-Architecture TextInput can't intercept a keypress before the
113
+ * native buffer mutates, so this doesn't run as a true pre-mutation guard;
114
+ * it runs on the range diffTextChange computed from an already-applied
115
+ * edit, and the caller (useMentionComposer) uses the expanded range to
116
+ * rebuild the text from previousText before React re-renders — see that
117
+ * hook's doc comment for the full repair sequence.
118
+ *
119
+ * The fixed-point loop is needed for the adjacent-mentions case: expanding
120
+ * to cover one mention can newly overlap a neighbor with zero gap between
121
+ * them (e.g. loaded from server data with no separator).
122
+ */
123
+ export function normalizeDeleteRange(
124
+ range: { start: number; end: number },
125
+ mentions: Mention[]
126
+ ): { start: number; end: number } {
127
+ let start = Math.min(range.start, range.end);
128
+ let end = Math.max(range.start, range.end);
129
+
130
+ let changed = true;
131
+ while (changed) {
132
+ changed = false;
133
+ for (const m of mentions) {
134
+ const overlaps = m.start < end && m.end > start;
135
+ if (overlaps && (m.start < start || m.end > end)) {
136
+ start = Math.min(start, m.start);
137
+ end = Math.max(end, m.end);
138
+ changed = true;
139
+ }
140
+ }
141
+ }
142
+
143
+ return { start, end };
144
+ }
145
+
146
+ export interface ApplyTextChangeResult {
147
+ text: string;
148
+ mentions: Mention[];
149
+ cursor: number;
150
+ /** True when this change touched a mention and it was atomically removed. */
151
+ mentionRemoved: boolean;
152
+ }
153
+
154
+ /**
155
+ * The full "atomic delete" pipeline, extracted as a pure function so it's
156
+ * unit-testable without a live TextInput: diff previousText -> nextTextRaw,
157
+ * expand the removed range to cover any touched mention, and if it
158
+ * expanded, discard nextTextRaw for that span and rebuild atomically from
159
+ * previousText — otherwise just diff-and-shift normally.
160
+ *
161
+ * This is what `useMentionComposer` (Phase A2) calls from its
162
+ * ref-buffered onChangeText reconciliation step; it also imperatively
163
+ * calls the native `setSelection(cursor, cursor)` after, which is RN-only
164
+ * and stays out of this pure function.
165
+ */
166
+ export function applyTextChange(
167
+ previousText: string,
168
+ mentions: Mention[],
169
+ nextTextRaw: string
170
+ ): ApplyTextChangeResult {
171
+ const { removedStart, removedEnd, insertedEnd } = diffTextChange(
172
+ previousText,
173
+ nextTextRaw
174
+ );
175
+ const expanded = normalizeDeleteRange(
176
+ { start: removedStart, end: removedEnd },
177
+ mentions
178
+ );
179
+
180
+ if (expanded.start !== removedStart || expanded.end !== removedEnd) {
181
+ const finalText =
182
+ previousText.slice(0, expanded.start) + previousText.slice(expanded.end);
183
+ const { mentions: keptMentions } = reconcileMentions({
184
+ previousText,
185
+ nextText: finalText,
186
+ mentions,
187
+ });
188
+ return {
189
+ text: finalText,
190
+ mentions: keptMentions,
191
+ cursor: expanded.start,
192
+ mentionRemoved: true,
193
+ };
194
+ }
195
+
196
+ const { mentions: nextMentions } = reconcileMentions({
197
+ previousText,
198
+ nextText: nextTextRaw,
199
+ mentions,
200
+ });
201
+ return {
202
+ text: nextTextRaw,
203
+ mentions: nextMentions,
204
+ cursor: insertedEnd,
205
+ mentionRemoved: false,
206
+ };
207
+ }
@@ -0,0 +1,53 @@
1
+ import type { Mention } from '../../types/mention';
2
+
3
+ export type Segment =
4
+ | { type: 'text'; text: string; start: number; end: number }
5
+ | { type: 'mention'; mention: Mention };
6
+
7
+ /**
8
+ * Converts edit-time state (flat string + Mention[] ranges) into an
9
+ * ordered list of plain-text runs and mention tokens — the shape both the
10
+ * composer's highlight renderer (Phase A3) and the sent-message bubble
11
+ * renderer (Phase B3) need to produce their styled `<Text>` children.
12
+ * Shared by both so the segment-splitting logic exists exactly once.
13
+ *
14
+ * O(m log m) sort + O(n) slicing (m = mention count, n = text length) —
15
+ * trivial at the sizes this feature targets (≤50 mentions, ≤2000 chars).
16
+ *
17
+ * Precondition: `mentions` is non-overlapping, guaranteed by Phase A1's own
18
+ * pipeline (insertMention/reconcileMentions never produce overlaps). The
19
+ * `m.start < cursor` skip below is a defensive backstop for
20
+ * externally-sourced data (a message loaded for editing, or an `ex` blob
21
+ * serialized by an older app version) — not a correctness dependency for
22
+ * the live composer.
23
+ */
24
+ export function tokenize(text: string, mentions: Mention[]): Segment[] {
25
+ const sorted = [...mentions].sort((a, b) => a.start - b.start);
26
+ const segments: Segment[] = [];
27
+ let cursor = 0;
28
+
29
+ for (const m of sorted) {
30
+ if (m.start < cursor) continue; // overlapping/malformed entry — skip rather than produce a negative-length slice
31
+ if (m.start > cursor) {
32
+ segments.push({
33
+ type: 'text',
34
+ text: text.slice(cursor, m.start),
35
+ start: cursor,
36
+ end: m.start,
37
+ });
38
+ }
39
+ segments.push({ type: 'mention', mention: m });
40
+ cursor = m.end;
41
+ }
42
+
43
+ if (cursor < text.length) {
44
+ segments.push({
45
+ type: 'text',
46
+ text: text.slice(cursor),
47
+ start: cursor,
48
+ end: text.length,
49
+ });
50
+ }
51
+
52
+ return segments;
53
+ }
@@ -272,7 +272,12 @@ export const getMessagePreview = (message?: MessageItem): string => {
272
272
  return urlText ? urlText : '';
273
273
 
274
274
  case MessageType.AtTextMessage:
275
- return extractTextContent(message);
275
+ // Bug: was calling extractTextContent(message) directly, which only
276
+ // checks message.textElem?.content (wrong elem for this type — always
277
+ // undefined here) before falling back to JSON.parse(message.content).
278
+ // atTextElem.text is the actual plain text for this message type —
279
+ // read it first, matching the QuoteMessage case right below.
280
+ return message?.atTextElem?.text || extractTextContent(message) || '';
276
281
 
277
282
  case MessageType.QuoteMessage:
278
283
  return message?.quoteElem?.text || extractTextContent(message) || '';
@@ -442,15 +447,21 @@ export const getSenderName = (
442
447
  currentUserId?: string,
443
448
  conversation?: ConversationItem
444
449
  ): string => {
445
- const sender =
446
- currentUserId && message.sendID === currentUserId
447
- ? 'Bạn'
448
- : message?.senderNickname ||
449
- getNotificationOpUserNickname(message) ||
450
- (conversation?.peerType === PeerType.Bot
451
- ? conversation?.showName || ''
452
- : undefined);
453
- return sender || '';
450
+ const isCurrentUser = currentUserId && message.sendID === currentUserId;
451
+
452
+ if (isCurrentUser) return 'Bạn';
453
+
454
+ switch (conversation?.peerType) {
455
+ case PeerType.Bot:
456
+ case PeerType.Customer:
457
+ case PeerType.User:
458
+ return '';
459
+
460
+ default:
461
+ return (
462
+ message?.senderNickname || getNotificationOpUserNickname(message) || ''
463
+ );
464
+ }
454
465
  };
455
466
 
456
467
  export interface GetLastMessageTextParams {
package/src/utils/url.ts CHANGED
@@ -33,6 +33,36 @@ export function extractUrls(text: string): string[] {
33
33
  return text.match(URL_REGEX) ?? [];
34
34
  }
35
35
 
36
+ export interface TextUrlPart {
37
+ text: string;
38
+ isUrl: boolean;
39
+ }
40
+
41
+ /**
42
+ * Splits `text` into an ordered run of plain/URL parts — pure data, no
43
+ * JSX, so both LinkMessage (a whole message that's just text+URLs) and
44
+ * TextMessage (a mention message whose plain-text *segments* may also
45
+ * contain a URL) can render URL styling from the same source instead of
46
+ * two divergent implementations.
47
+ */
48
+ export function splitTextByUrls(text: string, urls: string[]): TextUrlPart[] {
49
+ if (!urls.length) return [{ text, isUrl: false }];
50
+
51
+ const parts: TextUrlPart[] = [];
52
+ let remaining = text;
53
+
54
+ for (const url of urls) {
55
+ const idx = remaining.indexOf(url);
56
+ if (idx === -1) continue;
57
+ if (idx > 0) parts.push({ text: remaining.slice(0, idx), isUrl: false });
58
+ parts.push({ text: url, isUrl: true });
59
+ remaining = remaining.slice(idx + url.length);
60
+ }
61
+
62
+ if (remaining) parts.push({ text: remaining, isUrl: false });
63
+ return parts;
64
+ }
65
+
36
66
  export function normalizeUrl(url: string) {
37
67
  if (!/^https?:\/\//i.test(url)) {
38
68
  return `https://${url}`;