mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Merge pull request #5425 from macekj/emoji_quick_shortcut
Add keyboard shortcut for emoji reactions
This commit is contained in:
commit
70f24baaf1
@ -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;
|
||||
@ -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"
|
||||
|
@ -46,6 +46,8 @@ import {containsEmoji} from "../../../effects/utils";
|
||||
import {CHAT_EFFECTS} from '../../../effects';
|
||||
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);
|
||||
@ -91,6 +93,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,
|
||||
@ -223,6 +243,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
|
||||
@ -310,6 +365,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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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";
|
||||
@ -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
Block a user