Skip to content

Commit 3c48ea3

Browse files
committed
Added support for custom kudos
1 parent 9531513 commit 3c48ea3

File tree

4 files changed

+143
-25
lines changed

4 files changed

+143
-25
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.objectcomputing.checkins.services.slack;
2+
3+
import io.micronaut.http.HttpResponse;
4+
import io.micronaut.http.annotation.Controller;
5+
import io.micronaut.http.annotation.Get;
6+
import io.micronaut.scheduling.TaskExecutors;
7+
import io.micronaut.scheduling.annotation.ExecuteOn;
8+
import io.micronaut.security.annotation.Secured;
9+
import io.micronaut.security.rules.SecurityRule;
10+
import io.swagger.v3.oas.annotations.tags.Tag;
11+
12+
import java.net.URI;
13+
import java.util.Map;
14+
import java.util.UUID;
15+
16+
@Controller("/services/slack")
17+
@ExecuteOn(TaskExecutors.BLOCKING)
18+
@Secured(SecurityRule.IS_AUTHENTICATED)
19+
@Tag(name = "slack")
20+
public class SlackController {
21+
22+
private final SlackSearch slackSearch;
23+
24+
public SlackController(SlackSearch slackSearch) {
25+
this.slackSearch = slackSearch;
26+
}
27+
28+
@Get("/emoji")
29+
public HttpResponse<Map<String, String>> customEmoji() {
30+
Map<String, String> customEmoji = slackSearch.getCustomEmoji();
31+
32+
return HttpResponse
33+
.ok()
34+
.headers(headers -> headers.location(URI.create("/services/slack/emoji")))
35+
.body(customEmoji);
36+
}
37+
38+
protected URI location(UUID uuid) {
39+
return URI.create("/services/slack/" + uuid);
40+
}
41+
}

server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSearch.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import com.objectcomputing.checkins.configuration.CheckInsConfiguration;
44
import com.slack.api.methods.request.conversations.ConversationsInfoRequest;
5+
import com.slack.api.methods.request.emoji.EmojiListRequest;
56
import com.slack.api.methods.response.conversations.ConversationsInfoResponse;
7+
import com.slack.api.methods.response.emoji.EmojiListResponse;
68
import com.slack.api.model.block.LayoutBlock;
79
import com.slack.api.Slack;
810
import com.slack.api.methods.MethodsClient;
@@ -20,6 +22,7 @@
2022

2123
import java.util.List;
2224
import java.io.IOException;
25+
import java.util.Map;
2326

2427
import jnr.ffi.annotations.In;
2528
import org.slf4j.Logger;
@@ -122,5 +125,26 @@ public String findUserEmail(String userId) {
122125
}
123126
return null;
124127
}
128+
129+
130+
131+
public Map<String, String> getCustomEmoji() {
132+
String token = configuration.getApplication().getSlack().getBotToken();
133+
if (token != null) {
134+
try {
135+
MethodsClient client = Slack.getInstance().methods(token);
136+
EmojiListResponse response = client.emojiList(EmojiListRequest.builder().build());
137+
138+
if (response.isOk()) {
139+
return response.getEmoji();
140+
}
141+
} catch(IOException e) {
142+
LOG.error("SlackSearch.getCustomEmoji: " + e.toString());
143+
} catch(SlackApiException e) {
144+
LOG.error("SlackSearch.getCustomEmoji: " + e.toString());
145+
}
146+
}
147+
return null;
148+
}
125149
}
126150

web-ui/src/api/emoji.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { resolve } from './api.js';
2+
3+
const emojiUrl = '/services/slack/emoji';
4+
5+
export const getCustomEmoji = async (cookie) => {
6+
return resolve({
7+
url: emojiUrl,
8+
responseType: 'json',
9+
headers: { 'X-CSRF-Header': cookie, Accept: 'application/json' }
10+
});
11+
};

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

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useContext } from 'react';
1+
import React, { useCallback, useEffect, useState, useContext } from 'react';
22
import PropTypes from 'prop-types';
33
import {
44
Card,
@@ -15,8 +15,12 @@ import {
1515
selectCsrfToken,
1616
selectActiveOrInactiveProfile
1717
} from '../../context/selectors';
18+
import {
19+
UPDATE_TOAST
20+
} from '../../context/actions';
1821
import { AppContext } from '../../context/AppContext';
1922
import { getAvatarURL } from '../../api/api';
23+
import { getCustomEmoji } from '../../api/emoji.js';
2024
import TeamIcon from '@mui/icons-material/Groups';
2125
import { Emoji, EmojiStyle } from 'emoji-picker-react';
2226

@@ -35,35 +39,73 @@ const propTypes = {
3539
}).isRequired
3640
};
3741

38-
// TODO: Include support for custom Slack emojis and maybe move into it's own component and state
39-
const parseEmojiData = () => {
40-
let shortcodeMap = {};
41-
for (const category in emojis) {
42-
if (Object.hasOwn(emojis, category)) {
43-
let emojiList = emojis[category];
44-
shortcodeMap = emojiList.reduce((acc, current) => {
45-
current?.n?.forEach(
46-
name => (acc[name.replace(/\s/g, '_')] = { unified: current.u })
47-
);
48-
return acc;
49-
}, shortcodeMap);
50-
}
51-
}
52-
return shortcodeMap;
53-
};
54-
55-
const emojiShortcodeMap = parseEmojiData();
56-
57-
const getEmojiDataByShortcode = shortcode => {
58-
return emojiShortcodeMap[shortcode.toLowerCase()] || null;
59-
};
60-
6142
const KudosCard = ({ kudos }) => {
6243
const { state, dispatch } = useContext(AppContext);
6344
const csrf = selectCsrfToken(state);
45+
const [ emojiShortcodeMap, setEmojiShortcodeMap ] = useState({});
46+
const [ customLoaded, setCustomLoaded ] = useState(false);
6447

6548
const sender = selectActiveOrInactiveProfile(state, kudos.senderId);
6649

50+
useEffect(() => {
51+
let shortcodeMap = {};
52+
for (const category in emojis) {
53+
if (Object.hasOwn(emojis, category)) {
54+
let emojiList = emojis[category];
55+
emojiList.reduce((acc, current) => {
56+
current?.n?.forEach(
57+
name => (acc[name.replace(/\s/g, '_')] = { unified: current.u })
58+
);
59+
return acc;
60+
}, shortcodeMap);
61+
}
62+
}
63+
setEmojiShortcodeMap(shortcodeMap);
64+
}, []);
65+
66+
useEffect(() => {
67+
const loadCustomEmoji = async () => {
68+
let res = await getCustomEmoji(csrf);
69+
if (res && res.payload && res.payload.data && !res.error) {
70+
const shortcodeMap = { ...emojiShortcodeMap };
71+
let aliases = {};
72+
let customEmoji = res.payload.data;
73+
for(const emoji in customEmoji) {
74+
if(Object.hasOwn(customEmoji, emoji)) {
75+
if(customEmoji[emoji].startsWith("alias:")) {
76+
aliases[emoji] = { alias: customEmoji[emoji].substring("alias:".length) };
77+
} else {
78+
shortcodeMap[emoji] = { customUrl: customEmoji[emoji] };
79+
}
80+
}
81+
}
82+
for(const emoji in aliases) {
83+
if (Object.hasOwn(aliases, emoji)) {
84+
shortcodeMap[emoji] = shortcodeMap[aliases[emoji]];
85+
}
86+
}
87+
setEmojiShortcodeMap(shortcodeMap);
88+
setCustomLoaded(true);
89+
} else {
90+
window.snackDispatch({
91+
type: UPDATE_TOAST,
92+
payload: {
93+
severity: 'warning',
94+
toast: `Custom emoji could not be loaded: ${res.error}`
95+
}
96+
});
97+
}
98+
}
99+
100+
if(csrf && !customLoaded) {
101+
loadCustomEmoji();
102+
}
103+
}, [csrf, customLoaded, emojiShortcodeMap]);
104+
105+
const getEmojiDataByShortcode = useCallback(shortcode => {
106+
return emojiShortcodeMap[shortcode.toLowerCase()] || null;
107+
}, [emojiShortcodeMap]);
108+
67109
const regexIndexOf = useCallback((text, regex, start) => {
68110
const indexInSuffix = text.slice(start).search(regex);
69111
return indexInSuffix < 0 ? indexInSuffix : indexInSuffix + start;
@@ -218,7 +260,7 @@ const KudosCard = ({ kudos }) => {
218260

219261
// If the original text had no shortcodes, return it in an array
220262
return components.length === 0 ? [text] : components;
221-
}, []);
263+
}, [getEmojiDataByShortcode]);
222264

223265
// Creates the final array of React components for the message body,
224266
// processing Slack links, member names, and emojis.

0 commit comments

Comments
 (0)