Merge pull request #5425 from macekj/emoji_quick_shortcut
Add keyboard shortcut for emoji reactionspull/21833/head
commit
70f24baaf1
|
@ -47,6 +47,7 @@ import AutocompleteWrapperModel from "../../../editor/autocomplete";
|
||||||
import DocumentPosition from "../../../editor/position";
|
import DocumentPosition from "../../../editor/position";
|
||||||
import {ICompletion} from "../../../autocomplete/Autocompleter";
|
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 REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||||
|
|
||||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||||
|
@ -524,7 +525,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
const range = model.startRange(position);
|
const range = model.startRange(position);
|
||||||
range.expandBackwardsWhile((index, offset, part) => {
|
range.expandBackwardsWhile((index, offset, part) => {
|
||||||
return part.text[offset] !== " " && (
|
return part.text[offset] !== " " && part.text[offset] !== "+" && (
|
||||||
part.type === "plain" ||
|
part.type === "plain" ||
|
||||||
part.type === "pill-candidate" ||
|
part.type === "pill-candidate" ||
|
||||||
part.type === "command"
|
part.type === "command"
|
||||||
|
|
|
@ -46,6 +46,8 @@ import {containsEmoji} from "../../../effects/utils";
|
||||||
import {CHAT_EFFECTS} from '../../../effects';
|
import {CHAT_EFFECTS} from '../../../effects';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
import EMOJI_REGEX from 'emojibase-regex';
|
||||||
|
|
||||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||||
|
@ -91,6 +93,24 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) {
|
||||||
return content;
|
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 {
|
export default class SendMessageComposer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
|
@ -223,6 +243,41 @@ export default class SendMessageComposer extends React.Component {
|
||||||
return false;
|
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() {
|
_getSlashCommand() {
|
||||||
const commandText = this.model.parts.reduce((text, part) => {
|
const commandText = this.model.parts.reduce((text, part) => {
|
||||||
// use mxid to textify user pills in a command
|
// use mxid to textify user pills in a command
|
||||||
|
@ -310,6 +365,11 @@ export default class SendMessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isQuickReaction(this.model)) {
|
||||||
|
shouldSend = false;
|
||||||
|
this._sendQuickReaction();
|
||||||
|
}
|
||||||
|
|
||||||
const replyToEvent = this.props.replyToEvent;
|
const replyToEvent = this.props.replyToEvent;
|
||||||
if (shouldSend) {
|
if (shouldSend) {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
|
|
|
@ -190,7 +190,9 @@ abstract class PlainBasePart extends BasePart {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// only split if the previous character is a space
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,10 @@ import Adapter from "enzyme-adapter-react-16";
|
||||||
import { configure, mount } from "enzyme";
|
import { configure, mount } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {act} from "react-dom/test-utils";
|
import {act} from "react-dom/test-utils";
|
||||||
|
import SendMessageComposer, {
|
||||||
import SendMessageComposer, {createMessageContent} from "../../../../src/components/views/rooms/SendMessageComposer";
|
createMessageContent,
|
||||||
|
isQuickReaction,
|
||||||
|
} from "../../../../src/components/views/rooms/SendMessageComposer";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
import EditorModel from "../../../../src/editor/model";
|
import EditorModel from "../../../../src/editor/model";
|
||||||
import {createPartCreator, createRenderer} from "../../../editor/mock";
|
import {createPartCreator, createRenderer} from "../../../editor/mock";
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue