1+ import React , { useCallback , useContext , useEffect , useState } from 'react' ;
2+ import PropTypes from 'prop-types' ;
3+ import {
4+ Typography ,
5+ Link
6+ } from '@mui/material' ;
7+ import { AppContext } from '../../context/AppContext' ;
8+ import {
9+ selectCsrfToken ,
10+ selectActiveOrInactiveProfile
11+ } from '../../context/selectors' ;
12+ import { UPDATE_TOAST } from '../../context/actions' ;
13+ import { getCustomEmoji } from '../../api/emoji.js' ;
14+ import { Emoji } from 'emoji-picker-react' ;
15+
16+ import emojis from 'emoji-picker-react/src/data/emojis.json' ;
17+
18+ const propTypes = {
19+ kudos : PropTypes . shape ( {
20+ id : PropTypes . string . isRequired ,
21+ message : PropTypes . string . isRequired ,
22+ senderId : PropTypes . string . isRequired ,
23+ recipientTeam : PropTypes . object ,
24+ dateCreated : PropTypes . array . isRequired ,
25+ dateApproved : PropTypes . array ,
26+ recipientMembers : PropTypes . array
27+ } ) . isRequired ,
28+ } ;
29+
30+ const EnhancedKudos = ( { kudos} ) => {
31+ const { state, dispatch } = useContext ( AppContext ) ;
32+ const csrf = selectCsrfToken ( state ) ;
33+ const [ emojiShortcodeMap , setEmojiShortcodeMap ] = useState ( { } ) ;
34+ const [ customLoaded , setCustomLoaded ] = useState ( false ) ;
35+
36+ useEffect ( ( ) => {
37+ let shortcodeMap = { } ;
38+ for ( const category in emojis ) {
39+ if ( Object . hasOwn ( emojis , category ) ) {
40+ let emojiList = emojis [ category ] ;
41+ emojiList . reduce ( ( acc , current ) => {
42+ current ?. n ?. forEach (
43+ name => ( acc [ name . replace ( / \s / g, '_' ) ] = { unified : current . u } )
44+ ) ;
45+ return acc ;
46+ } , shortcodeMap ) ;
47+ }
48+ }
49+ setEmojiShortcodeMap ( shortcodeMap ) ;
50+ } , [ ] ) ;
51+
52+ useEffect ( ( ) => {
53+ const loadCustomEmoji = async ( ) => {
54+ let res = await getCustomEmoji ( csrf ) ;
55+ if ( res && res . payload && res . payload . data && ! res . error ) {
56+ const shortcodeMap = { ...emojiShortcodeMap } ;
57+ let aliases = { } ;
58+ let customEmoji = res . payload . data ;
59+ for ( const emoji in customEmoji ) {
60+ if ( Object . hasOwn ( customEmoji , emoji ) ) {
61+ if ( customEmoji [ emoji ] . startsWith ( "alias:" ) ) {
62+ aliases [ emoji ] = { alias : customEmoji [ emoji ] . substring ( "alias:" . length ) } ;
63+ } else {
64+ shortcodeMap [ emoji ] = { customUrl : customEmoji [ emoji ] } ;
65+ }
66+ }
67+ }
68+ for ( const emoji in aliases ) {
69+ if ( Object . hasOwn ( aliases , emoji ) ) {
70+ shortcodeMap [ emoji ] = shortcodeMap [ aliases [ emoji ] . alias ] ;
71+ }
72+ }
73+ setEmojiShortcodeMap ( shortcodeMap ) ;
74+ setCustomLoaded ( true ) ;
75+ } else {
76+ window . snackDispatch ( {
77+ type : UPDATE_TOAST ,
78+ payload : {
79+ severity : 'warning' ,
80+ toast : `Custom emoji could not be loaded: ${ res . error } `
81+ }
82+ } ) ;
83+ }
84+ }
85+
86+ if ( csrf && ! customLoaded ) {
87+ loadCustomEmoji ( ) ;
88+ }
89+ } , [ csrf , customLoaded , emojiShortcodeMap ] ) ;
90+
91+ const getEmojiDataByShortcode = useCallback ( shortcode => {
92+ return emojiShortcodeMap [ shortcode . toLowerCase ( ) ] || null ;
93+ } , [ emojiShortcodeMap ] ) ;
94+
95+ const regexIndexOf = useCallback ( ( text , regex , start ) => {
96+ const indexInSuffix = text . slice ( start ) . search ( regex ) ;
97+ return indexInSuffix < 0 ? indexInSuffix : indexInSuffix + start ;
98+ } , [ ] ) ;
99+
100+ // Replaces occurrences of a specific name in a message string with a MUI Link component.
101+ const linkMember = useCallback ( ( member , name , message ) => {
102+ const components = [ ] ;
103+ let currentMessage = message ;
104+ let currentIndex = 0 ;
105+ let lastIndex = 0 ;
106+
107+ while ( currentIndex < currentMessage . length ) {
108+ const index = regexIndexOf (
109+ currentMessage ,
110+ new RegExp ( '\\b' + name + '\\b' , 'i' ) ,
111+ currentIndex
112+ ) ;
113+ if ( index !== - 1 ) {
114+ if ( index > lastIndex ) {
115+ components . push ( currentMessage . slice ( lastIndex , index ) ) ;
116+ }
117+ components . push (
118+ < Link
119+ key = { `${ member . id } -${ index } ` }
120+ href = { `/profile/${ member . id } ` }
121+ underline = "hover"
122+ >
123+ { name }
124+ </ Link >
125+ ) ;
126+ currentIndex = index + name . length ;
127+ lastIndex = currentIndex ;
128+ } else {
129+ break ;
130+ }
131+ }
132+ if ( lastIndex < currentMessage . length ) {
133+ components . push ( currentMessage . slice ( lastIndex ) ) ;
134+ }
135+ return components . length === 0 ? [ message ] : components ;
136+ } , [ ] ) ;
137+
138+ // Generates a list of unique name variations for a member to be used for linking.
139+ const searchNames = useCallback ( ( member , members ) => {
140+ const names = [ ] ;
141+ if ( member . middleName )
142+ names . push ( `${ member . firstName } ${ member . middleName } ${ member . lastName } ` ) ;
143+ const firstAndLast = `${ member . firstName } ${ member . lastName } ` ;
144+ if (
145+ ! members . some (
146+ k =>
147+ k . id !== member . id && firstAndLast === `${ k . firstName } ${ k . lastName } `
148+ )
149+ )
150+ names . push ( firstAndLast ) ;
151+ if (
152+ ! members . some (
153+ k =>
154+ k . id !== member . id &&
155+ ( member . lastName === k . lastName || member . lastName === k . firstName )
156+ )
157+ )
158+ names . push ( member . lastName ) ;
159+ if (
160+ ! members . some (
161+ k =>
162+ k . id !== member . id &&
163+ ( member . firstName === k . lastName || member . firstName === k . firstName )
164+ )
165+ )
166+ names . push ( member . firstName ) ;
167+ return names ;
168+ } , [ ] ) ;
169+
170+ // Converts Slack-style links (<url|text> or <url>) in a string to <a> elements.
171+ const linkSlackUrls = useCallback ( textLine => {
172+ const slackLinkRegex = / < ( [ ^ < > | ] * ) (?: \| ( [ ^ < > ] * ) ) ? > / g;
173+ const components = [ ] ;
174+ let lastIndex = 0 ;
175+ let match ;
176+ while ( ( match = slackLinkRegex . exec ( textLine ) ) !== null ) {
177+ const url = match [ 1 ] ;
178+ const linkText = match [ 2 ] ;
179+ if ( match . index > lastIndex )
180+ components . push ( textLine . slice ( lastIndex , match . index ) ) ;
181+ components . push (
182+ < a
183+ key = { `slack-link-${ match . index } ` }
184+ href = { url }
185+ target = "_blank"
186+ rel = "noopener noreferrer"
187+ >
188+ { linkText || url }
189+ </ a >
190+ ) ;
191+ lastIndex = slackLinkRegex . lastIndex ;
192+ }
193+ if ( lastIndex < textLine . length ) components . push ( textLine . slice ( lastIndex ) ) ;
194+ return components . length === 0 ? [ textLine ] : components ;
195+ } , [ ] ) ;
196+
197+ const renderTextWithEmojis = useCallback ( text => {
198+ const emojiShortcodeRegex = / : ( [ a - z A - Z 0 - 9 _ + - ] + ) : / g; // Regex to find :shortcodes:
199+ const components = [ ] ;
200+ let lastIndex = 0 ;
201+ let match ;
202+
203+ while ( ( match = emojiShortcodeRegex . exec ( text ) ) !== null ) {
204+ const shortcode = match [ 1 ] ;
205+ const emojiData = getEmojiDataByShortcode ( shortcode ) ;
206+ const precedingText = text . slice ( lastIndex , match . index ) ;
207+
208+ // Add text before the emoji shortcode
209+ if ( precedingText ) {
210+ components . push ( precedingText ) ;
211+ }
212+
213+ // Add the Emoji component or the original shortcode text
214+ if ( emojiData ) {
215+ if ( emojiData . unified ) {
216+ components . push (
217+ < Emoji
218+ key = { `${ match . index } -${ shortcode } ` } // Unique key
219+ unified = { emojiData . unified }
220+ size = { 20 } // Adjust size as needed
221+ />
222+ ) ;
223+ } else if ( emojiData . customUrl ) {
224+ // Render custom emoji using emojiUrl
225+ components . push (
226+ < img src = { emojiData . customUrl } alt = { shortcode } style = { { height : '20px' , width : '20px' , fontSize : '20px' } } />
227+ // Not sure why the below doesn't work. It seems like it should according to the docs. :shrug:s
228+ // <Emoji
229+ // key={`${match.index}-${shortcode}`}
230+ // emojiUrl={emojiData.customUrl}
231+ // size={20}
232+ // />
233+ ) ;
234+ }
235+ } else {
236+ // If shortcode not found in map, render the original text
237+ components . push ( match [ 0 ] ) ;
238+ }
239+
240+ lastIndex = emojiShortcodeRegex . lastIndex ;
241+ }
242+
243+ // Add any remaining text after the last shortcode
244+ const remainingText = text . slice ( lastIndex ) ;
245+ if ( remainingText ) {
246+ components . push ( remainingText ) ;
247+ }
248+
249+ // If the original text had no shortcodes, return it in an array
250+ return components . length === 0 ? [ text ] : components ;
251+ } , [ getEmojiDataByShortcode ] ) ;
252+
253+ // Creates the final array of React components for the message body,
254+ // processing Slack links, member names, and emojis.
255+ const createLinksAndEmojis = useCallback (
256+ kudosData => {
257+ const lines = [ ] ;
258+ let lineIndex = 0 ;
259+ const recipients = Array . isArray ( kudosData . recipientMembers )
260+ ? kudosData . recipientMembers
261+ : [ ] ;
262+
263+ for ( const line of kudosData . message . split ( '\n' ) ) {
264+ let components = linkSlackUrls ( line ) ;
265+
266+ // Process Member Name Links
267+ let componentsAfterNames = [ ] ;
268+ for ( const component of components ) {
269+ if ( typeof component === 'string' ) {
270+ let currentStringSegments = [ component ] ;
271+ for ( const member of recipients ) {
272+ const names = searchNames ( member , recipients ) ;
273+ let nextStringSegments = [ ] ;
274+ for ( const segment of currentStringSegments ) {
275+ if ( typeof segment === 'string' ) {
276+ let segmentProcessed = false ;
277+ for ( const name of names ) {
278+ const built = linkMember ( member , name , segment ) ;
279+ if (
280+ built . length > 1 ||
281+ ( built . length === 1 && built [ 0 ] !== segment )
282+ ) {
283+ nextStringSegments . push ( ...built ) ;
284+ segmentProcessed = true ;
285+ break ;
286+ }
287+ }
288+ if ( ! segmentProcessed ) nextStringSegments . push ( segment ) ;
289+ } else {
290+ nextStringSegments . push ( segment ) ;
291+ }
292+ }
293+ currentStringSegments = nextStringSegments ;
294+ }
295+ componentsAfterNames . push ( ...currentStringSegments ) ;
296+ } else {
297+ componentsAfterNames . push ( component ) ;
298+ }
299+ }
300+ components = componentsAfterNames ;
301+
302+ let finalComponents = [ ] ;
303+ for ( const comp of components ) {
304+ if ( typeof comp === 'string' ) {
305+ finalComponents . push ( ...renderTextWithEmojis ( comp ) ) ; // Spread the result
306+ } else {
307+ finalComponents . push ( comp ) ; // Keep existing non-string components
308+ }
309+ }
310+
311+ lines . push (
312+ < Typography
313+ key = { `${ kudosData . id } -line-${ lineIndex } ` }
314+ variant = "body1"
315+ component = "div"
316+ sx = { { lineHeight : '1.6' /* Improve spacing with emojis */ } }
317+ >
318+ { finalComponents }
319+ </ Typography >
320+ ) ;
321+ lineIndex ++ ;
322+ }
323+ return lines ;
324+ } ,
325+ [
326+ kudos . id ,
327+ kudos . message ,
328+ kudos . recipientMembers ,
329+ linkMember ,
330+ linkSlackUrls ,
331+ searchNames ,
332+ renderTextWithEmojis
333+ ]
334+ ) ;
335+
336+ return createLinksAndEmojis ( kudos ) ;
337+ } ;
338+
339+ EnhancedKudos . propTypes = propTypes ;
340+
341+ export default EnhancedKudos ;
0 commit comments