Skip to content

Commit 9e75589

Browse files
committed
Implemented emoji support on the kudos admin screen
1 parent dfbdd1b commit 9e75589

File tree

3 files changed

+435
-136
lines changed

3 files changed

+435
-136
lines changed

web-ui/src/components/kudos/PublicKudosCard.jsx

Lines changed: 144 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,12 @@ import {
1717
} from '../../context/selectors';
1818
import { AppContext } from '../../context/AppContext';
1919
import { getAvatarURL } from '../../api/api';
20-
import DateFnsUtils from '@date-io/date-fns';
2120
import TeamIcon from '@mui/icons-material/Groups';
2221
import { Emoji, EmojiStyle } from 'emoji-picker-react';
2322

2423
import './PublicKudosCard.css';
2524
import emojis from 'emoji-picker-react/src/data/emojis.json';
2625

27-
const dateUtils = new DateFnsUtils();
28-
2926
const 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
4139
const 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

5555
const 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-zA-Z0-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}

web-ui/src/components/kudos_card/KudosCard.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
}
2222

2323
.kudos-card .kudos-card-content {
24-
max-width: 650px;
2524
padding: 1rem;
2625
}
2726

0 commit comments

Comments
 (0)