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

Add keyboard shortcut for emoji reactions #5425

Merged
merged 5 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/components/views/rooms/BasicMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import AutocompleteWrapperModel from "../../../editor/autocomplete";
import DocumentPosition from "../../../editor/position";
import {ICompletion} from "../../../autocomplete/Autocompleter";

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

const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
Expand Down Expand Up @@ -524,7 +525,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
const range = model.startRange(position);
range.expandBackwardsWhile((index, offset, part) => {
return part.text[offset] !== " " && (
return part.text[offset] !== " " && part.text[offset] !== "+" && (
part.type === "plain" ||
part.type === "pill-candidate" ||
part.type === "command"
Expand Down
60 changes: 60 additions & 0 deletions src/components/views/rooms/SendMessageComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions";
import SettingsStore from "../../../settings/SettingsStore";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import EMOJI_REGEX from 'emojibase-regex';

function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
Expand Down Expand Up @@ -89,6 +91,24 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) {
return content;
}

// exported for tests
export function isQuickReaction(model) {
const parts = model.parts;
if (parts.length == 0) return false;
const text = textSerialize(model);
// shortcut takes the form "+:emoji:" or "+ :emoji:""
// can be in 1 or 2 parts
if (parts.length <= 2) {
const hasShortcut = text.startsWith("+") || text.startsWith("+ ");
const emojiMatch = text.match(EMOJI_REGEX);
if (hasShortcut && emojiMatch && emojiMatch.length == 1) {
return emojiMatch[0] === text.substring(1) ||
emojiMatch[0] === text.substring(2);
}
}
return false;
}

export default class SendMessageComposer extends React.Component {
static propTypes = {
room: PropTypes.object.isRequired,
Expand Down Expand Up @@ -221,6 +241,41 @@ export default class SendMessageComposer extends React.Component {
return false;
}

_sendQuickReaction() {
const timeline = this.props.room.getLiveTimeline();
const events = timeline.getEvents();
const reaction = this.model.parts[1].text;
for (let i = events.length - 1; i >= 0; i--) {
if (events[i].getType() === "m.room.message") {
let shouldReact = true;
const lastMessage = events[i];
const userId = MatrixClientPeg.get().getUserId();
const messageReactions = this.props.room.getUnfilteredTimelineSet()
.getRelationsForEvent(lastMessage.getId(), "m.annotation", "m.reaction");

// if we have already sent this reaction, don't redact but don't re-send
if (messageReactions) {
const myReactionEvents = messageReactions.getAnnotationsBySender()[userId] || [];
const myReactionKeys = [...myReactionEvents]
.filter(event => !event.isRedacted())
.map(event => event.getRelation().key);
shouldReact = !myReactionKeys.includes(reaction);
}
if (shouldReact) {
MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), "m.reaction", {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": lastMessage.getId(),
"key": reaction,
},
});
dis.dispatch({action: "message_sent"});
}
break;
}
}
}

_getSlashCommand() {
const commandText = this.model.parts.reduce((text, part) => {
// use mxid to textify user pills in a command
Expand Down Expand Up @@ -308,6 +363,11 @@ export default class SendMessageComposer extends React.Component {
}
}

if (isQuickReaction(this.model)) {
shouldSend = false;
this._sendQuickReaction();
}

const replyToEvent = this.props.replyToEvent;
if (shouldSend) {
const startTime = CountlyAnalytics.getTimestamp();
Expand Down
4 changes: 3 additions & 1 deletion src/editor/parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ abstract class PlainBasePart extends BasePart {
return true;
}
// only split if the previous character is a space
return this._text[offset - 1] !== " ";
// or if it is a + and this is a :
return this._text[offset - 1] !== " " &&
(this._text[offset - 1] !== "+" || chr !== ":");
}
return true;
}
Expand Down
42 changes: 40 additions & 2 deletions test/components/views/rooms/SendMessageComposer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import Adapter from "enzyme-adapter-react-16";
import { configure, mount } from "enzyme";
import React from "react";
import {act} from "react-dom/test-utils";

import SendMessageComposer, {createMessageContent} from "../../../../src/components/views/rooms/SendMessageComposer";
import SendMessageComposer, {
createMessageContent,
isQuickReaction,
} from "../../../../src/components/views/rooms/SendMessageComposer";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import EditorModel from "../../../../src/editor/model";
import {createPartCreator, createRenderer} from "../../../editor/mock";
Expand Down Expand Up @@ -227,6 +229,42 @@ describe('<SendMessageComposer/>', () => {
});
});
});

describe("isQuickReaction", () => {
it("correctly detects quick reaction", () => {
const model = new EditorModel([], createPartCreator(), createRenderer());
model.update("+😊", "insertText", {offset: 3, atNodeEnd: true});

const isReaction = isQuickReaction(model);

expect(isReaction).toBeTruthy();
});

it("correctly detects quick reaction with space", () => {
const model = new EditorModel([], createPartCreator(), createRenderer());
model.update("+ 😊", "insertText", {offset: 4, atNodeEnd: true});

const isReaction = isQuickReaction(model);

expect(isReaction).toBeTruthy();
});

it("correctly rejects quick reaction with extra text", () => {
const model = new EditorModel([], createPartCreator(), createRenderer());
const model2 = new EditorModel([], createPartCreator(), createRenderer());
const model3 = new EditorModel([], createPartCreator(), createRenderer());
const model4 = new EditorModel([], createPartCreator(), createRenderer());
model.update("+😊hello", "insertText", {offset: 8, atNodeEnd: true});
model2.update(" +😊", "insertText", {offset: 4, atNodeEnd: true});
model3.update("+ 😊😊", "insertText", {offset: 6, atNodeEnd: true});
model4.update("+smiley", "insertText", {offset: 7, atNodeEnd: true});

expect(isQuickReaction(model)).toBeFalsy();
expect(isQuickReaction(model2)).toBeFalsy();
expect(isQuickReaction(model3)).toBeFalsy();
expect(isQuickReaction(model4)).toBeFalsy();
});
});
});