Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 70f24ba

Browse files
authored
Merge pull request #5425 from macekj/emoji_quick_shortcut
Add keyboard shortcut for emoji reactions
2 parents b870de1 + 0c85cb5 commit 70f24ba

File tree

4 files changed

+105
-4
lines changed

4 files changed

+105
-4
lines changed

Diff for: src/components/views/rooms/BasicMessageComposer.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import AutocompleteWrapperModel from "../../../editor/autocomplete";
4747
import DocumentPosition from "../../../editor/position";
4848
import {ICompletion} from "../../../autocomplete/Autocompleter";
4949

50+
// matches emoticons which follow the start of a line or whitespace
5051
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
5152

5253
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
@@ -524,7 +525,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
524525
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
525526
const range = model.startRange(position);
526527
range.expandBackwardsWhile((index, offset, part) => {
527-
return part.text[offset] !== " " && (
528+
return part.text[offset] !== " " && part.text[offset] !== "+" && (
528529
part.type === "plain" ||
529530
part.type === "pill-candidate" ||
530531
part.type === "command"

Diff for: src/components/views/rooms/SendMessageComposer.js

+60
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import {containsEmoji} from "../../../effects/utils";
4646
import {CHAT_EFFECTS} from '../../../effects';
4747
import SettingsStore from "../../../settings/SettingsStore";
4848
import CountlyAnalytics from "../../../CountlyAnalytics";
49+
import {MatrixClientPeg} from "../../../MatrixClientPeg";
50+
import EMOJI_REGEX from 'emojibase-regex';
4951

5052
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
5153
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@@ -91,6 +93,24 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) {
9193
return content;
9294
}
9395

96+
// exported for tests
97+
export function isQuickReaction(model) {
98+
const parts = model.parts;
99+
if (parts.length == 0) return false;
100+
const text = textSerialize(model);
101+
// shortcut takes the form "+:emoji:" or "+ :emoji:""
102+
// can be in 1 or 2 parts
103+
if (parts.length <= 2) {
104+
const hasShortcut = text.startsWith("+") || text.startsWith("+ ");
105+
const emojiMatch = text.match(EMOJI_REGEX);
106+
if (hasShortcut && emojiMatch && emojiMatch.length == 1) {
107+
return emojiMatch[0] === text.substring(1) ||
108+
emojiMatch[0] === text.substring(2);
109+
}
110+
}
111+
return false;
112+
}
113+
94114
export default class SendMessageComposer extends React.Component {
95115
static propTypes = {
96116
room: PropTypes.object.isRequired,
@@ -223,6 +243,41 @@ export default class SendMessageComposer extends React.Component {
223243
return false;
224244
}
225245

246+
_sendQuickReaction() {
247+
const timeline = this.props.room.getLiveTimeline();
248+
const events = timeline.getEvents();
249+
const reaction = this.model.parts[1].text;
250+
for (let i = events.length - 1; i >= 0; i--) {
251+
if (events[i].getType() === "m.room.message") {
252+
let shouldReact = true;
253+
const lastMessage = events[i];
254+
const userId = MatrixClientPeg.get().getUserId();
255+
const messageReactions = this.props.room.getUnfilteredTimelineSet()
256+
.getRelationsForEvent(lastMessage.getId(), "m.annotation", "m.reaction");
257+
258+
// if we have already sent this reaction, don't redact but don't re-send
259+
if (messageReactions) {
260+
const myReactionEvents = messageReactions.getAnnotationsBySender()[userId] || [];
261+
const myReactionKeys = [...myReactionEvents]
262+
.filter(event => !event.isRedacted())
263+
.map(event => event.getRelation().key);
264+
shouldReact = !myReactionKeys.includes(reaction);
265+
}
266+
if (shouldReact) {
267+
MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), "m.reaction", {
268+
"m.relates_to": {
269+
"rel_type": "m.annotation",
270+
"event_id": lastMessage.getId(),
271+
"key": reaction,
272+
},
273+
});
274+
dis.dispatch({action: "message_sent"});
275+
}
276+
break;
277+
}
278+
}
279+
}
280+
226281
_getSlashCommand() {
227282
const commandText = this.model.parts.reduce((text, part) => {
228283
// use mxid to textify user pills in a command
@@ -310,6 +365,11 @@ export default class SendMessageComposer extends React.Component {
310365
}
311366
}
312367

368+
if (isQuickReaction(this.model)) {
369+
shouldSend = false;
370+
this._sendQuickReaction();
371+
}
372+
313373
const replyToEvent = this.props.replyToEvent;
314374
if (shouldSend) {
315375
const startTime = CountlyAnalytics.getTimestamp();

Diff for: src/editor/parts.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ abstract class PlainBasePart extends BasePart {
190190
return true;
191191
}
192192
// only split if the previous character is a space
193-
return this._text[offset - 1] !== " ";
193+
// or if it is a + and this is a :
194+
return this._text[offset - 1] !== " " &&
195+
(this._text[offset - 1] !== "+" || chr !== ":");
194196
}
195197
return true;
196198
}

Diff for: test/components/views/rooms/SendMessageComposer-test.js

+40-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import Adapter from "enzyme-adapter-react-16";
1818
import { configure, mount } from "enzyme";
1919
import React from "react";
2020
import {act} from "react-dom/test-utils";
21-
22-
import SendMessageComposer, {createMessageContent} from "../../../../src/components/views/rooms/SendMessageComposer";
21+
import SendMessageComposer, {
22+
createMessageContent,
23+
isQuickReaction,
24+
} from "../../../../src/components/views/rooms/SendMessageComposer";
2325
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
2426
import EditorModel from "../../../../src/editor/model";
2527
import {createPartCreator, createRenderer} from "../../../editor/mock";
@@ -227,6 +229,42 @@ describe('<SendMessageComposer/>', () => {
227229
});
228230
});
229231
});
232+
233+
describe("isQuickReaction", () => {
234+
it("correctly detects quick reaction", () => {
235+
const model = new EditorModel([], createPartCreator(), createRenderer());
236+
model.update("+😊", "insertText", {offset: 3, atNodeEnd: true});
237+
238+
const isReaction = isQuickReaction(model);
239+
240+
expect(isReaction).toBeTruthy();
241+
});
242+
243+
it("correctly detects quick reaction with space", () => {
244+
const model = new EditorModel([], createPartCreator(), createRenderer());
245+
model.update("+ 😊", "insertText", {offset: 4, atNodeEnd: true});
246+
247+
const isReaction = isQuickReaction(model);
248+
249+
expect(isReaction).toBeTruthy();
250+
});
251+
252+
it("correctly rejects quick reaction with extra text", () => {
253+
const model = new EditorModel([], createPartCreator(), createRenderer());
254+
const model2 = new EditorModel([], createPartCreator(), createRenderer());
255+
const model3 = new EditorModel([], createPartCreator(), createRenderer());
256+
const model4 = new EditorModel([], createPartCreator(), createRenderer());
257+
model.update("+😊hello", "insertText", {offset: 8, atNodeEnd: true});
258+
model2.update(" +😊", "insertText", {offset: 4, atNodeEnd: true});
259+
model3.update("+ 😊😊", "insertText", {offset: 6, atNodeEnd: true});
260+
model4.update("+smiley", "insertText", {offset: 7, atNodeEnd: true});
261+
262+
expect(isQuickReaction(model)).toBeFalsy();
263+
expect(isQuickReaction(model2)).toBeFalsy();
264+
expect(isQuickReaction(model3)).toBeFalsy();
265+
expect(isQuickReaction(model4)).toBeFalsy();
266+
});
267+
});
230268
});
231269

232270

0 commit comments

Comments
 (0)