@@ -17,15 +17,12 @@ import {
1717} from '../../context/selectors' ;
1818import { AppContext } from '../../context/AppContext' ;
1919import { getAvatarURL } from '../../api/api' ;
20- import DateFnsUtils from '@date-io/date-fns' ;
2120import TeamIcon from '@mui/icons-material/Groups' ;
2221import { Emoji , EmojiStyle } from 'emoji-picker-react' ;
2322
2423import './PublicKudosCard.css' ;
2524import emojis from 'emoji-picker-react/src/data/emojis.json' ;
2625
27- const dateUtils = new DateFnsUtils ( ) ;
28-
2926const propTypes = {
3027 kudos : PropTypes . shape ( {
3128 id : PropTypes . string . isRequired ,
@@ -38,23 +35,26 @@ const propTypes = {
3835 } ) . isRequired
3936} ;
4037
38+ // TODO: Include support for custom Slack emojis and maybe move into it's own component and state
4139const parseEmojiData = ( ) => {
4240 let shortcodeMap = { } ;
43- for ( const category in emojis ) {
41+ for ( const category in emojis ) {
4442 if ( Object . hasOwn ( emojis , category ) ) {
4543 let emojiList = emojis [ category ] ;
4644 shortcodeMap = emojiList . reduce ( ( acc , current ) => {
47- current ?. n ?. forEach ( name => acc [ name . replace ( / \s / g, "_" ) ] = { unified : current . u } )
45+ current ?. n ?. forEach (
46+ name => ( acc [ name . replace ( / \s / g, '_' ) ] = { unified : current . u } )
47+ ) ;
4848 return acc ;
4949 } , shortcodeMap ) ;
5050 }
5151 }
52- return shortcodeMap
52+ return shortcodeMap ;
5353} ;
5454
5555const emojiShortcodeMap = parseEmojiData ( ) ;
5656
57- const getEmojiDataByShortcode = ( shortcode ) => {
57+ const getEmojiDataByShortcode = shortcode => {
5858 return emojiShortcodeMap [ shortcode . toLowerCase ( ) ] || null ;
5959} ;
6060
@@ -64,126 +64,109 @@ const KudosCard = ({ kudos }) => {
6464
6565 const sender = selectActiveOrInactiveProfile ( state , kudos . senderId ) ;
6666
67- const regexIndexOf = ( text , regex , start ) => {
67+ const regexIndexOf = useCallback ( ( text , regex , start ) => {
6868 const indexInSuffix = text . slice ( start ) . search ( regex ) ;
6969 return indexInSuffix < 0 ? indexInSuffix : indexInSuffix + start ;
70- } ;
70+ } , [ ] ) ;
7171
72- const linkMember = ( member , name , message ) => {
72+ // Replaces occurrences of a specific name in a message string with a MUI Link component.
73+ const linkMember = useCallback ( ( member , name , message ) => {
7374 const components = [ ] ;
74- let index = 0 ;
75- do {
76- index = regexIndexOf (
77- message ,
75+ let currentMessage = message ;
76+ let currentIndex = 0 ;
77+ let lastIndex = 0 ;
78+
79+ while ( currentIndex < currentMessage . length ) {
80+ const index = regexIndexOf (
81+ currentMessage ,
7882 new RegExp ( '\\b' + name + '\\b' , 'i' ) ,
79- index
83+ currentIndex
8084 ) ;
81- if ( index != - 1 ) {
82- const link = (
83- < Link key = { `${ member . id } -${ index } ` } href = { `/profile/${ member . id } ` } >
85+ if ( index !== - 1 ) {
86+ if ( index > lastIndex ) {
87+ components . push ( currentMessage . slice ( lastIndex , index ) ) ;
88+ }
89+ components . push (
90+ < Link
91+ key = { `${ member . id } -${ index } ` }
92+ href = { `/profile/${ member . id } ` }
93+ underline = "hover"
94+ >
8495 { name }
8596 </ Link >
8697 ) ;
87- if ( index > 0 ) {
88- components . push ( message . slice ( 0 , index ) ) ;
89- }
90- components . push ( link ) ;
91- message = message . slice ( index + name . length ) ;
98+ currentIndex = index + name . length ;
99+ lastIndex = currentIndex ;
100+ } else {
101+ break ;
92102 }
93- } while ( index != - 1 ) ;
94- components . push ( message ) ;
95- return components ;
96- } ;
103+ }
104+ if ( lastIndex < currentMessage . length ) {
105+ components . push ( currentMessage . slice ( lastIndex ) ) ;
106+ }
107+ return components . length === 0 ? [ message ] : components ;
108+ } , [ ] ) ;
97109
98- const searchNames = ( member , members ) => {
110+ // Generates a list of unique name variations for a member to be used for linking.
111+ const searchNames = useCallback ( ( member , members ) => {
99112 const names = [ ] ;
100- if ( member . middleName ) {
113+ if ( member . middleName )
101114 names . push ( `${ member . firstName } ${ member . middleName } ${ member . lastName } ` ) ;
102- }
103115 const firstAndLast = `${ member . firstName } ${ member . lastName } ` ;
104116 if (
105117 ! members . some (
106- k => k . id != member . id && firstAndLast == `${ k . firstName } ${ k . lastName } `
118+ k =>
119+ k . id !== member . id && firstAndLast === `${ k . firstName } ${ k . lastName } `
107120 )
108- ) {
121+ )
109122 names . push ( firstAndLast ) ;
110- }
111123 if (
112124 ! members . some (
113125 k =>
114- k . id != member . id &&
115- ( member . lastName == k . lastName || member . lastName == k . firstName )
126+ k . id !== member . id &&
127+ ( member . lastName === k . lastName || member . lastName = == k . firstName )
116128 )
117- ) {
118- // If there are no other recipients with a name that contains this
119- // member's last name, we can replace based on that.
129+ )
120130 names . push ( member . lastName ) ;
121- }
122131 if (
123132 ! members . some (
124133 k =>
125- k . id != member . id &&
126- ( member . firstName == k . lastName || member . firstName == k . firstName )
134+ k . id !== member . id &&
135+ ( member . firstName === k . lastName || member . firstName = == k . firstName )
127136 )
128- ) {
129- // If there are no other recipients with a name that contains this
130- // member's first name, we can replace based on that.
137+ )
131138 names . push ( member . firstName ) ;
132- }
133139 return names ;
134- } ;
140+ } , [ ] ) ;
135141
136- const linkSlackUrls = textLine => {
137- // Regex to find <url> or <url|text>
138- // Group 1: URL
139- // Group 2: Optional Link Text (undefined if not present)
142+ // Converts Slack-style links (<url|text> or <url>) in a string to <a> elements.
143+ const linkSlackUrls = useCallback ( textLine => {
140144 const slackLinkRegex = / < ( [ ^ < > | ] * ) (?: \| ( [ ^ < > ] * ) ) ? > / g;
141145 const components = [ ] ;
142146 let lastIndex = 0 ;
143147 let match ;
144-
145- // Find all matches in the text line
146148 while ( ( match = slackLinkRegex . exec ( textLine ) ) !== null ) {
147149 const url = match [ 1 ] ;
148- const linkText = match [ 2 ] ; // Will be undefined if there's no |text part
149- const precedingText = textLine . slice ( lastIndex , match . index ) ;
150-
151- // Add the text before the match (if any)
152- if ( precedingText ) {
153- components . push ( precedingText ) ;
154- }
155-
156- // Create and add the link component
150+ const linkText = match [ 2 ] ;
151+ if ( match . index > lastIndex )
152+ components . push ( textLine . slice ( lastIndex , match . index ) ) ;
157153 components . push (
158154 < a
159- key = { `slack-link-${ match . index } ` } // Unique key based on position
155+ key = { `slack-link-${ match . index } ` }
160156 href = { url }
161- target = "_blank" // Open in new tab
162- rel = "noopener noreferrer" // Security measure
157+ target = "_blank"
158+ rel = "noopener noreferrer"
163159 >
164160 { linkText || url }
165161 </ a >
166162 ) ;
167-
168- // Update the index for the next slice
169163 lastIndex = slackLinkRegex . lastIndex ;
170164 }
165+ if ( lastIndex < textLine . length ) components . push ( textLine . slice ( lastIndex ) ) ;
166+ return components . length === 0 ? [ textLine ] : components ;
167+ } , [ ] ) ;
171168
172- // Add any remaining text after the last match
173- const remainingText = textLine . slice ( lastIndex ) ;
174- if ( remainingText ) {
175- components . push ( remainingText ) ;
176- }
177-
178- // If no links were found at all, return the original line in an array
179- if ( components . length === 0 ) {
180- return [ textLine ] ;
181- }
182-
183- return components ;
184- } ;
185-
186- const renderTextWithEmojis = useCallback ( ( text ) => {
169+ const renderTextWithEmojis = useCallback ( text => {
187170 const emojiShortcodeRegex = / : ( [ a - z A - Z 0 - 9 _ + - ] + ) : / g; // Regex to find :shortcodes:
188171 const components = [ ] ;
189172 let lastIndex = 0 ;
@@ -210,15 +193,13 @@ const KudosCard = ({ kudos }) => {
210193 />
211194 ) ;
212195 } else if ( emojiData . customUrl ) {
213- // Render custom emoji using emojiUrl (or as an img tag)
196+ // Render custom emoji using emojiUrl
214197 components . push (
215198 < Emoji
216199 key = { `${ match . index } -${ shortcode } ` }
217200 emojiUrl = { emojiData . customUrl }
218201 size = { 20 }
219202 />
220- // Alternative: Render as an img tag directly if preferred
221- // <img key={`${match.index}-${shortcode}`} src={emojiData.customUrl} alt={`:${shortcode}:`} style={{ width: 20, height: 20, verticalAlign: 'middle' }} />
222203 ) ;
223204 }
224205 } else {
@@ -239,46 +220,90 @@ const KudosCard = ({ kudos }) => {
239220 return components . length === 0 ? [ text ] : components ;
240221 } , [ ] ) ;
241222
242- const createLinks = kudos => {
243- const lines = [ ] ;
244- let index = 0 ;
245- for ( let line of kudos . message . split ( '\n' ) ) {
246- const components = linkSlackUrls ( line ) ;
247- for ( let member of kudos . recipientMembers ) {
248- const names = searchNames ( member , kudos . recipientMembers ) ;
249- for ( let name of names ) {
250- for ( let i = 0 ; i < components . length ; i ++ ) {
251- const component = components [ i ] ;
252- if ( typeof component === 'string' ) {
253- const built = linkMember ( member , name , component ) ;
254- if ( built . length > 1 ) {
255- components . splice ( i , 1 , ...built ) ;
223+ // Creates the final array of React components for the message body,
224+ // processing Slack links, member names, and emojis.
225+ const createLinksAndEmojis = useCallback (
226+ kudosData => {
227+ const lines = [ ] ;
228+ let lineIndex = 0 ;
229+ const recipients = Array . isArray ( kudosData . recipientMembers )
230+ ? kudosData . recipientMembers
231+ : [ ] ;
232+
233+ for ( const line of kudosData . message . split ( '\n' ) ) {
234+ let components = linkSlackUrls ( line ) ;
235+
236+ // Process Member Name Links
237+ let componentsAfterNames = [ ] ;
238+ for ( const component of components ) {
239+ if ( typeof component === 'string' ) {
240+ let currentStringSegments = [ component ] ;
241+ for ( const member of recipients ) {
242+ const names = searchNames ( member , recipients ) ;
243+ let nextStringSegments = [ ] ;
244+ for ( const segment of currentStringSegments ) {
245+ if ( typeof segment === 'string' ) {
246+ let segmentProcessed = false ;
247+ for ( const name of names ) {
248+ const built = linkMember ( member , name , segment ) ;
249+ if (
250+ built . length > 1 ||
251+ ( built . length === 1 && built [ 0 ] !== segment )
252+ ) {
253+ nextStringSegments . push ( ...built ) ;
254+ segmentProcessed = true ;
255+ break ;
256+ }
257+ }
258+ if ( ! segmentProcessed ) nextStringSegments . push ( segment ) ;
259+ } else {
260+ nextStringSegments . push ( segment ) ;
261+ }
256262 }
263+ currentStringSegments = nextStringSegments ;
257264 }
265+ componentsAfterNames . push ( ...currentStringSegments ) ;
266+ } else {
267+ componentsAfterNames . push ( component ) ;
258268 }
259269 }
260- }
261-
262- let finalComponents = [ ] ;
263- for ( const comp of components ) {
264- if ( typeof comp === 'string' ) {
265- finalComponents . push ( ...renderTextWithEmojis ( comp ) ) ; // Spread the result
266- } else {
267- finalComponents . push ( comp ) ; // Keep existing non-string components
270+ components = componentsAfterNames ;
271+
272+ let finalComponents = [ ] ;
273+ for ( const comp of components ) {
274+ if ( typeof comp === 'string' ) {
275+ finalComponents . push ( ...renderTextWithEmojis ( comp ) ) ; // Spread the result
276+ } else {
277+ finalComponents . push ( comp ) ; // Keep existing non-string components
278+ }
268279 }
269- }
270280
271- lines . push (
272- < Typography key = { kudos . id + '-' + index } variant = "body1" sx = { { lineHeight : '1.6' } } >
273- { finalComponents }
274- </ Typography >
275- ) ;
276- index ++ ;
277- }
278- return lines ;
279- } ;
281+ lines . push (
282+ < Typography
283+ key = { `${ kudosData . id } -line-${ lineIndex } ` }
284+ variant = "body1"
285+ component = "div"
286+ sx = { { lineHeight : '1.6' /* Improve spacing with emojis */ } }
287+ >
288+ { finalComponents }
289+ </ Typography >
290+ ) ;
291+ lineIndex ++ ;
292+ }
293+ return lines ;
294+ } ,
295+ [
296+ kudos . id ,
297+ kudos . message ,
298+ kudos . recipientMembers ,
299+ linkMember ,
300+ linkSlackUrls ,
301+ searchNames ,
302+ renderTextWithEmojis
303+ ]
304+ ) ;
280305
281- const multiTooltip = ( num , list ) => {
306+ const multiTooltip = useCallback ( ( num , list ) => {
282307 let tooltip = '' ;
283308 let prefix = '' ;
284309 for ( let member of list . slice ( - num ) ) {
@@ -290,7 +315,7 @@ const KudosCard = ({ kudos }) => {
290315 < Typography > { `+${ num } ` } </ Typography >
291316 </ Tooltip >
292317 ) ;
293- } ;
318+ } , [ ] ) ;
294319
295320 const getRecipientComponent = useCallback ( ( ) => {
296321 if ( kudos . recipientTeam ) {
@@ -353,7 +378,7 @@ const KudosCard = ({ kudos }) => {
353378 subheaderTypographyProps = { { variant : 'subtitle1' } }
354379 />
355380 < CardContent >
356- < > { createLinks ( kudos ) } </ >
381+ < > { createLinksAndEmojis ( kudos ) } </ >
357382 { kudos . recipientTeam && (
358383 < AvatarGroup
359384 max = { 12 }
0 commit comments