Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
PLT-1378 Initial version of emoji reactions (#4520)
Browse files Browse the repository at this point in the history
* Refactored emoji.json to support multiple aliases and emoji categories

* Added custom category to emoji.jsx and stabilized all fields

* Removed conflicting aliases for :mattermost: and :ca:

* fixup after store changes

* Added emoji reactions

* Removed reactions for an emoji when that emoji is deleted

* Fixed incorrect test case

* Renamed ReactionList to ReactionListView

* Fixed 👍 and 👎 not showing up as possible reactions

* Removed text emoticons from emoji reaction autocomplete

* Changed emoji reactions to be sorted by the order that they were first created

* Set a maximum number of listeners for the ReactionStore

* Removed unused code from Textbox component

* Fixed reaction permissions

* Changed error code when trying to modify reactions for another user

* Fixed merge conflicts

* Properly applied theme colours to reactions

* Fixed ESLint and gofmt errors

* Fixed ReactionListContainer to properly update when its post prop changes

* Removed unnecessary escape characters from reaction regexes

* Shared reaction message pattern between CreatePost and CreateComment

* Removed an unnecessary select query when saving a reaction

* Changed reactions route to be under /reactions

* Fixed copyright dates on newly added files

* Removed debug code that prevented all unit tests from being ran

* Cleaned up unnecessary code for reactions

* Renamed ReactionStore.List to ReactionStore.GetForPost
  • Loading branch information
hmhealey committed Nov 30, 2016
1 parent 3f3dc1d commit 90114cb
Show file tree
Hide file tree
Showing 30 changed files with 833 additions and 95 deletions.
20 changes: 20 additions & 0 deletions actions/post_actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,23 @@ export function loadProfilesForPosts(posts) {

AsyncClient.getProfilesByIds(list);
}

export function addReaction(channelId, postId, emojiName) {
const reaction = {
post_id: postId,
user_id: UserStore.getCurrentId(),
emoji_name: emojiName
};

AsyncClient.saveReaction(channelId, reaction);
}

export function removeReaction(channelId, postId, emojiName) {
const reaction = {
post_id: postId,
user_id: UserStore.getCurrentId(),
emoji_name: emojiName
};

AsyncClient.deleteReaction(channelId, reaction);
}
31 changes: 30 additions & 1 deletion actions/websocket_actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import BrowserStore from 'stores/browser_store.jsx';
import ErrorStore from 'stores/error_store.jsx';
import NotificationStore from 'stores/notification_store.jsx'; //eslint-disable-line no-unused-vars

import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import Client from 'client/web_client.jsx';
import WebSocketClient from 'client/web_websocket_client.jsx';
import * as WebrtcActions from './webrtc_actions.jsx';
Expand All @@ -23,7 +24,7 @@ import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
import * as StatusActions from 'actions/status_actions.jsx';

import {Constants, SocketEvents, UserStatuses} from 'utils/constants.jsx';
import {ActionTypes, Constants, SocketEvents, UserStatuses} from 'utils/constants.jsx';

import {browserHistory} from 'react-router/es6';

Expand Down Expand Up @@ -165,6 +166,14 @@ function handleEvent(msg) {
handleWebrtc(msg);
break;

case SocketEvents.REACTION_ADDED:
handleReactionAddedEvent(msg);
break;

case SocketEvents.REACTION_REMOVED:
handleReactionRemovedEvent(msg);
break;

default:
}
}
Expand Down Expand Up @@ -320,3 +329,23 @@ function handleWebrtc(msg) {
const data = msg.data;
return WebrtcActions.handle(data);
}

function handleReactionAddedEvent(msg) {
const reaction = JSON.parse(msg.data.reaction);

AppDispatcher.handleServerAction({
type: ActionTypes.ADDED_REACTION,
postId: reaction.post_id,
reaction
});
}

function handleReactionRemovedEvent(msg) {
const reaction = JSON.parse(msg.data.reaction);

AppDispatcher.handleServerAction({
type: ActionTypes.REMOVED_REACTION,
postId: reaction.post_id,
reaction
});
}
37 changes: 32 additions & 5 deletions client/client.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2005,11 +2005,11 @@ export default class Client {

removeCertificateFile(filename, success, error) {
request.
post(`${this.getAdminRoute()}/remove_certificate`).
set(this.defaultHeaders).
accept('application/json').
send({filename}).
end(this.handleResponse.bind(this, 'removeCertificateFile', success, error));
post(`${this.getAdminRoute()}/remove_certificate`).
set(this.defaultHeaders).
accept('application/json').
send({filename}).
end(this.handleResponse.bind(this, 'removeCertificateFile', success, error));
}

samlCertificateStatus(success, error) {
Expand All @@ -2030,6 +2030,33 @@ export default class Client {
});
}

saveReaction(channelId, reaction, success, error) {
request.
post(`${this.getChannelNeededRoute(channelId)}/posts/${reaction.post_id}/reactions/save`).
set(this.defaultHeaders).
accept('application/json').
send(reaction).
end(this.handleResponse.bind(this, 'saveReaction', success, error));
}

deleteReaction(channelId, reaction, success, error) {
request.
post(`${this.getChannelNeededRoute(channelId)}/posts/${reaction.post_id}/reactions/delete`).
set(this.defaultHeaders).
accept('application/json').
send(reaction).
end(this.handleResponse.bind(this, 'deleteReaction', success, error));
}

listReactions(channelId, postId, success, error) {
request.
get(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/reactions`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'listReactions', success, error));
}

webrtcToken(success, error) {
request.post(`${this.getWebrtcRoute()}/token`).
set(this.defaultHeaders).
Expand Down
74 changes: 53 additions & 21 deletions components/create_comment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import $ from 'jquery';
import ReactDOM from 'react-dom';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Client from 'client/web_client.jsx';
import EmojiStore from 'stores/emoji_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PostDeletedModal from './post_deleted_modal.jsx';
import PostStore from 'stores/post_store.jsx';
Expand All @@ -17,6 +18,7 @@ import FilePreview from './file_preview.jsx';
import * as Utils from 'utils/utils.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import * as PostActions from 'actions/post_actions.jsx';

import Constants from 'utils/constants.jsx';

Expand All @@ -25,6 +27,8 @@ import {FormattedMessage} from 'react-intl';
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;

import {REACTION_PATTERN} from './create_post.jsx';

import React from 'react';

export default class CreateComment extends React.Component {
Expand All @@ -34,6 +38,8 @@ export default class CreateComment extends React.Component {
this.lastTime = 0;

this.handleSubmit = this.handleSubmit.bind(this);
this.handleSubmitPost = this.handleSubmitPost.bind(this);
this.handleSubmitReaction = this.handleSubmitReaction.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
Expand Down Expand Up @@ -100,15 +106,9 @@ export default class CreateComment extends React.Component {
return;
}

const post = {};
post.file_ids = [];
post.message = this.state.message;

if (post.message.trim().length === 0 && this.state.fileInfos.length === 0) {
return;
}
const message = this.state.message;

if (post.message.length > Constants.CHARACTER_LIMIT) {
if (message.length > Constants.CHARACTER_LIMIT) {
this.setState({
postError: (
<FormattedMessage
Expand All @@ -121,15 +121,43 @@ export default class CreateComment extends React.Component {
return;
}

MessageHistoryStore.storeMessageInHistory(this.state.message);
MessageHistoryStore.storeMessageInHistory(message);

if (message.trim().length === 0 && this.state.previews.length === 0) {
return;
}

const isReaction = REACTION_PATTERN.exec(message);
if (isReaction && EmojiStore.has(isReaction[2])) {
this.handleSubmitReaction(isReaction);
} else {
this.handleSubmitPost(message);
}

this.setState({
message: '',
submitting: false,
postError: null,
fileInfos: [],
serverError: null
});

const fasterThanHumanWillClick = 150;
const forceFocus = (Date.now() - this.state.lastBlurAt < fasterThanHumanWillClick);
this.focusTextbox(forceFocus);
}

handleSubmitPost(message) {
const userId = UserStore.getCurrentId();
const time = Utils.getTimestamp();

const post = {};
post.file_ids = [];
post.message = message;
post.channel_id = this.props.channelId;
post.root_id = this.props.rootId;
post.parent_id = this.props.rootId;
post.file_ids = this.state.fileInfos.map((info) => info.id);
const time = Utils.getTimestamp();
post.pending_post_id = `${userId}:${time}`;
post.user_id = userId;
post.create_at = time;
Expand Down Expand Up @@ -160,18 +188,21 @@ export default class CreateComment extends React.Component {
});
}
);
}

this.setState({
message: '',
submitting: false,
postError: null,
fileInfos: [],
serverError: null
});
handleSubmitReaction(isReaction) {
const action = isReaction[1];

const fasterThanHumanWillClick = 150;
const forceFocus = (Date.now() - this.state.lastBlurAt < fasterThanHumanWillClick);
this.focusTextbox(forceFocus);
const emojiName = isReaction[2];
const postId = this.props.latestPostId;

if (action === '+') {
PostActions.addReaction(this.props.channelId, postId, emojiName);
} else if (action === '-') {
PostActions.removeReaction(this.props.channelId, postId, emojiName);
}

PostStore.storeCommentDraft(this.props.rootId, null);
}

commentMsgKeyPress(e) {
Expand Down Expand Up @@ -455,5 +486,6 @@ export default class CreateComment extends React.Component {

CreateComment.propTypes = {
channelId: React.PropTypes.string.isRequired,
rootId: React.PropTypes.string.isRequired
rootId: React.PropTypes.string.isRequired,
latestPostId: React.PropTypes.string.isRequired
};
29 changes: 26 additions & 3 deletions components/create_post.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import FilePreview from './file_preview.jsx';
import PostDeletedModal from './post_deleted_modal.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';

import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
import * as PostActions from 'actions/post_actions.jsx';

import ChannelStore from 'stores/channel_store.jsx';
import EmojiStore from 'stores/emoji_store.jsx';
import PostStore from 'stores/post_store.jsx';
import MessageHistoryStore from 'stores/message_history_store.jsx';
import UserStore from 'stores/user_store.jsx';
Expand All @@ -34,6 +36,8 @@ const KeyCodes = Constants.KeyCodes;

import React from 'react';

export const REACTION_PATTERN = /^(\+|-):([^:\s]+):\s*$/;

export default class CreatePost extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -101,6 +105,7 @@ export default class CreatePost extends React.Component {

this.setState({submitting: true, serverError: null});

const isReaction = REACTION_PATTERN.exec(post.message);
if (post.message.indexOf('/') === 0) {
PostStore.storeDraft(this.state.channelId, null);
this.setState({message: '', postError: null, fileInfos: []});
Expand All @@ -123,14 +128,18 @@ export default class CreatePost extends React.Component {
const state = {};
state.serverError = err.message;
state.submitting = false;
this.setState(state);
this.setState({state});
}
}
);
} else if (isReaction && EmojiStore.has(isReaction[2])) {
this.sendReaction(isReaction);
} else {
this.sendMessage(post);
}

this.setState({message: '', submitting: false, postError: null, fileInfos: [], serverError: null});

const fasterThanHumanWillClick = 150;
const forceFocus = (Date.now() - this.state.lastBlurAt < fasterThanHumanWillClick);
this.focusTextbox(forceFocus);
Expand All @@ -148,7 +157,6 @@ export default class CreatePost extends React.Component {
post.parent_id = this.state.parentId;

GlobalActions.emitUserPostedEvent(post);
this.setState({message: '', submitting: false, postError: null, fileInfos: [], serverError: null});

Client.createPost(post,
(data) => {
Expand Down Expand Up @@ -177,6 +185,21 @@ export default class CreatePost extends React.Component {
);
}

sendReaction(isReaction) {
const action = isReaction[1];

const emojiName = isReaction[2];
const postId = PostStore.getLatestPost(this.state.channelId).id;

if (action === '+') {
PostActions.addReaction(this.state.channelId, postId, emojiName);
} else if (action === '-') {
PostActions.removeReaction(this.state.channelId, postId, emojiName);
}

PostStore.storeCurrentDraft(null);
}

focusTextbox(keepFocus = false) {
if (keepFocus || !Utils.isMobile()) {
this.refs.textbox.focus();
Expand Down
2 changes: 1 addition & 1 deletion components/emoji/components/add_emoji.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default class AddEmoji extends React.Component {
});

return;
} else if (EmojiStore.getSystemEmojis().has(emoji.name)) {
} else if (EmojiStore.hasSystemEmoji(emoji.name)) {
this.setState({
saving: false,
error: (
Expand Down
1 change: 1 addition & 0 deletions components/post_view/components/post.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ export default class Post extends React.Component {
/>
<PostBody
post={post}
currentUser={this.props.currentUser}
sameRoot={this.props.sameRoot}
parentPost={parentPost}
handleCommentClick={this.handleCommentClick}
Expand Down
6 changes: 6 additions & 0 deletions components/post_view/components/post_body.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import FileAttachmentListContainer from 'components/file_attachment_list_contain
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
import PostMessageContainer from './post_message_container.jsx';
import PendingPostOptions from './pending_post_options.jsx';
import ReactionListContainer from './reaction_list_container.jsx';

import {FormattedMessage} from 'react-intl';

Expand Down Expand Up @@ -202,6 +203,10 @@ export default class PostBody extends React.Component {
<div className={'post__body ' + mentionHighlightClass}>
{messageWithAdditionalContent}
{fileAttachmentHolder}
<ReactionListContainer
post={post}
currentUserId={this.props.currentUser.id}
/>
</div>
</div>
);
Expand All @@ -210,6 +215,7 @@ export default class PostBody extends React.Component {

PostBody.propTypes = {
post: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired,
parentPost: React.PropTypes.object,
retryPost: React.PropTypes.func,
handleCommentClick: React.PropTypes.func.isRequired,
Expand Down
Loading

0 comments on commit 90114cb

Please sign in to comment.