diff options
Diffstat (limited to 'webapp/components')
-rw-r--r-- | webapp/components/autosize_textarea.jsx | 3 | ||||
-rw-r--r-- | webapp/components/create_comment.jsx | 99 | ||||
-rw-r--r-- | webapp/components/create_post.jsx | 98 | ||||
-rw-r--r-- | webapp/components/emoji_picker/components/emoji_picker_category.jsx | 42 | ||||
-rw-r--r-- | webapp/components/emoji_picker/components/emoji_picker_item.jsx | 70 | ||||
-rw-r--r-- | webapp/components/emoji_picker/components/emoji_picker_preview.jsx | 66 | ||||
-rw-r--r-- | webapp/components/emoji_picker/emoji_picker.jsx | 417 | ||||
-rw-r--r-- | webapp/components/emoji_picker/emoji_picker_container.jsx | 46 | ||||
-rw-r--r-- | webapp/components/file_upload.jsx | 43 | ||||
-rw-r--r-- | webapp/components/rhs_root_post.jsx | 5 | ||||
-rw-r--r-- | webapp/components/textbox.jsx | 5 | ||||
-rw-r--r-- | webapp/components/user_settings/user_settings_advanced.jsx | 7 |
12 files changed, 868 insertions, 33 deletions
diff --git a/webapp/components/autosize_textarea.jsx b/webapp/components/autosize_textarea.jsx index a55a27aef..e14835737 100644 --- a/webapp/components/autosize_textarea.jsx +++ b/webapp/components/autosize_textarea.jsx @@ -62,6 +62,7 @@ export default class AutosizeTextarea extends React.Component { const { value, placeholder, + id, ...otherProps } = props; @@ -77,12 +78,14 @@ export default class AutosizeTextarea extends React.Component { <div> <textarea ref='textarea' + id={id + '-textarea'} {...heightProps} {...props} /> <div style={{height: 0, overflow: 'hidden'}}> <textarea ref='reference' + id={id + '-reference'} style={{height: 'auto', width: '100%'}} disabled={true} value={value} diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx index 96280bbc1..899609ed0 100644 --- a/webapp/components/create_comment.jsx +++ b/webapp/components/create_comment.jsx @@ -15,6 +15,7 @@ import Textbox from './textbox.jsx'; import MsgTyping from './msg_typing.jsx'; import FileUpload from './file_upload.jsx'; import FilePreview from './file_preview.jsx'; +import EmojiPicker from './emoji_picker/emoji_picker.jsx'; import * as Utils from 'utils/utils.jsx'; import * as UserAgent from 'utils/user_agent.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; @@ -28,8 +29,7 @@ import {browserHistory} from 'react-router/es6'; const ActionTypes = Constants.ActionTypes; const KeyCodes = Constants.KeyCodes; -import {REACTION_PATTERN} from './create_post.jsx'; - +import {REACTION_PATTERN, EMOJI_PATTERN} from './create_post.jsx'; import React from 'react'; export default class CreateComment extends React.Component { @@ -56,6 +56,10 @@ export default class CreateComment extends React.Component { this.showPostDeletedModal = this.showPostDeletedModal.bind(this); this.hidePostDeletedModal = this.hidePostDeletedModal.bind(this); this.handlePostError = this.handlePostError.bind(this); + this.handleEmojiPickerClick = this.handleEmojiPickerClick.bind(this); + this.handleEmojiClick = this.handleEmojiClick.bind(this); + this.onKeyPress = this.onKeyPress.bind(this); + this.closeEmoji = this.closeEmoji.bind(this); PostStore.clearCommentDraftUploads(); MessageHistoryStore.resetHistoryIndex('comment'); @@ -69,24 +73,85 @@ export default class CreateComment extends React.Component { submitting: false, ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'), showPostDeletedModal: false, - enableAddButton + enableAddButton, + showEmojiPicker: false, + emojiOffset: 0, + emojiPickerEnabled: Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW) }; this.lastBlurAt = 0; } + closeEmoji(clickEvent) { + /* + if the user clicked something outside the component, except the RHS emojipicker icon + and the picker is open, then close it + */ + if (clickEvent && clickEvent.srcElement && + clickEvent.srcElement.className !== '' && + clickEvent.srcElement.className.indexOf('emoji-rhs') === -1 && + this.state.showEmojiPicker) { + this.setState({showEmojiPicker: !this.state.showEmojiPicker}); + } + } + + handleEmojiPickerClick() { + const threadHeight = document.getElementById('thread--root') ? document.getElementById('thread--root').offsetHeight : 0; + const messagesHeight = document.querySelector('div.post-right-comments-container') ? document.querySelector('div.post-right-comments-container').offsetHeight : 0; + + const totalHeight = threadHeight + messagesHeight; + let pickerOffset = 0; + if (totalHeight > 361) { + pickerOffset = -361; + } else { + pickerOffset = -1 * totalHeight; + } + this.setState({showEmojiPicker: !this.state.showEmojiPicker, emojiOffset: pickerOffset}); + } + + handleEmojiClick(emoji) { + const emojiAlias = emoji.name || emoji.aliases[0]; + + if (!emojiAlias) { + //Oops.. There went something wrong + return; + } + + if (this.state.message === '') { + this.setState({message: ':' + emojiAlias + ': ', showEmojiPicker: false}); + } else { + //check whether there is already a blank at the end of the current message + const newMessage = (/\s+$/.test(this.state.message)) ? + this.state.message + ':' + emojiAlias + ': ' : this.state.message + ' :' + emojiAlias + ': '; + + this.setState({message: newMessage, showEmojiPicker: false}); + } + + this.focusTextbox(); + } + componentDidMount() { PreferenceStore.addChangeListener(this.onPreferenceChange); + document.addEventListener('keydown', this.onKeyPress); + this.focusTextbox(); } componentWillUnmount() { PreferenceStore.removeChangeListener(this.onPreferenceChange); + document.removeEventListener('keydown', this.onKeyPress); + } + + onKeyPress(e) { + if (e.which === Constants.KeyCodes.ESCAPE && this.state.showEmojiPicker === true) { + this.setState({showEmojiPicker: !this.state.showEmojiPicker}); + } } onPreferenceChange() { this.setState({ - ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter') + ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'), + emojiPickerEnabled: Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW) }); } @@ -205,6 +270,14 @@ export default class CreateComment extends React.Component { GlobalActions.emitUserCommentedEvent(post); + const emojiResult = post.message.match(EMOJI_PATTERN); + if (emojiResult) { + // parse message and emit emoji event + emojiResult.forEach((emoji) => { + PostActions.emitEmojiPosted(emoji); + }); + } + PostActions.queuePost(post, false, null, (err) => { if (err.id === 'api.post.create_post.root_id.app_error') { @@ -502,6 +575,18 @@ export default class CreateComment extends React.Component { addButtonClass += ' disabled'; } + let emojiPicker = null; + if (this.state.showEmojiPicker) { + emojiPicker = ( + <EmojiPicker + onEmojiClick={this.handleEmojiClick} + topOrBottom='bottom' + emojiOffset={this.state.emojiOffset} + outsideClick={this.closeEmoji} + /> + ); + } + return ( <form onSubmit={this.handleSubmit}> <div className='post-create'> @@ -518,6 +603,7 @@ export default class CreateComment extends React.Component { value={this.state.message} onBlur={this.handleBlur} createMessage={Utils.localizeMessage('create_comment.addComment', 'Add a comment...')} + emojiEnabled={this.state.emojiPickerEnabled} initialText='' channelId={this.props.channelId} id='reply_textbox' @@ -532,7 +618,12 @@ export default class CreateComment extends React.Component { onUploadError={this.handleUploadError} postType='comment' channelId={this.props.channelId} + onEmojiClick={this.handleEmojiPickerClick} + emojiEnabled={this.state.emojiPickerEnabled} + navBarName='rhs' /> + + {emojiPicker} </div> </div> <MsgTyping diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx index faa880acc..93a299b89 100644 --- a/webapp/components/create_post.jsx +++ b/webapp/components/create_post.jsx @@ -8,6 +8,7 @@ import FileUpload from './file_upload.jsx'; import FilePreview from './file_preview.jsx'; import PostDeletedModal from './post_deleted_modal.jsx'; import TutorialTip from './tutorial/tutorial_tip.jsx'; +import EmojiPicker from './emoji_picker/emoji_picker.jsx'; import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; @@ -36,6 +37,7 @@ const KeyCodes = Constants.KeyCodes; import React from 'react'; export const REACTION_PATTERN = /^(\+|-):([^:\s]+):\s*$/; +export const EMOJI_PATTERN = /:[A-Za-z-_0-9]*:/g; export default class CreatePost extends React.Component { constructor(props) { @@ -61,7 +63,10 @@ export default class CreatePost extends React.Component { this.showPostDeletedModal = this.showPostDeletedModal.bind(this); this.hidePostDeletedModal = this.hidePostDeletedModal.bind(this); this.showShortcuts = this.showShortcuts.bind(this); + this.handleEmojiClick = this.handleEmojiClick.bind(this); + this.handleEmojiPickerClick = this.handleEmojiPickerClick.bind(this); this.handlePostError = this.handlePostError.bind(this); + this.closeEmoji = this.closeEmoji.bind(this); PostStore.clearDraftUploads(); @@ -77,7 +82,9 @@ export default class CreatePost extends React.Component { fullWidthTextBox: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN, showTutorialTip: false, showPostDeletedModal: false, - enableSendButton: false + enableSendButton: false, + showEmojiPicker: false, + emojiPickerEnabled: Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW) }; this.lastBlurAt = 0; @@ -87,6 +94,18 @@ export default class CreatePost extends React.Component { this.setState({postError}); } + closeEmoji(clickEvent) { + /* + if the user clicked something outside the component, except the main emojipicker icon + and the picker is open, then close it + */ + if (clickEvent && clickEvent.srcElement && + clickEvent.srcElement.className.indexOf('emoji-main') === -1 && + this.state.showEmojiPicker) { + this.setState({showEmojiPicker: !this.state.showEmojiPicker}); + } + } + handleSubmit(e) { e.preventDefault(); @@ -185,6 +204,14 @@ export default class CreatePost extends React.Component { GlobalActions.emitUserPostedEvent(post); + // parse message and emit emoji event + const emojiResult = post.message.match(EMOJI_PATTERN); + if (emojiResult) { + emojiResult.forEach((emoji) => { + PostActions.emitEmojiPosted(emoji); + }); + } + PostActions.queuePost(post, false, null, (err) => { if (err.id === 'api.post.create_post.root_id.app_error') { @@ -379,6 +406,10 @@ export default class CreatePost extends React.Component { } showShortcuts(e) { + if (e.which === Constants.KeyCodes.ESCAPE && this.state.showEmojiPicker === true) { + this.setState({showEmojiPicker: !this.state.showEmojiPicker}); + } + if ((e.ctrlKey || e.metaKey) && e.keyCode === Constants.KeyCodes.FORWARD_SLASH) { e.preventDefault(); const args = {}; @@ -411,7 +442,8 @@ export default class CreatePost extends React.Component { this.setState({ showTutorialTip: tutorialStep === TutorialSteps.POST_POPOVER, ctrlSend: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'), - fullWidthTextBox: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN + fullWidthTextBox: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN, + emojiPickerEnabled: Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW) }); } @@ -484,6 +516,31 @@ export default class CreatePost extends React.Component { }); } + handleEmojiClick(emoji) { + const emojiAlias = emoji.name || emoji.aliases[0]; + + if (!emojiAlias) { + //Oops.. There went something wrong + return; + } + + if (this.state.message === '') { + this.setState({message: ':' + emojiAlias + ': ', showEmojiPicker: false}); + } else { + //check whether there is already a blank at the end of the current message + const newMessage = (/\s+$/.test(this.state.message)) ? + this.state.message + ':' + emojiAlias + ': ' : this.state.message + ' :' + emojiAlias + ': '; + + this.setState({message: newMessage, showEmojiPicker: false}); + } + + this.focusTextbox(); + } + + handleEmojiPickerClick() { + this.setState({showEmojiPicker: !this.state.showEmojiPicker}); + } + createTutorialTip() { const screens = []; @@ -556,6 +613,17 @@ export default class CreatePost extends React.Component { if (!this.state.enableSendButton) { sendButtonClass += ' disabled'; } + let emojiPicker = null; + if (this.state.showEmojiPicker) { + emojiPicker = ( + <EmojiPicker + onEmojiClick={this.handleEmojiClick} + topOrBottom='top' + outsideClick={this.closeEmoji} + + /> + ); + } return ( <form @@ -575,22 +643,28 @@ export default class CreatePost extends React.Component { handlePostError={this.handlePostError} value={this.state.message} onBlur={this.handleBlur} + emojiEnabled={this.state.emojiPickerEnabled} createMessage={Utils.localizeMessage('create_post.write', 'Write a message...')} channelId={this.state.channelId} id='post_textbox' ref='textbox' /> + <FileUpload + ref='fileUpload' + getFileCount={this.getFileCount} + onFileUploadChange={this.handleFileUploadChange} + onUploadStart={this.handleUploadStart} + onFileUpload={this.handleFileUploadComplete} + onUploadError={this.handleUploadError} + postType='post' + channelId='' + onEmojiClick={this.handleEmojiPickerClick} + emojiEnabled={this.state.emojiPickerEnabled} + navBarName='main' + /> + + {emojiPicker} </div> - <FileUpload - ref='fileUpload' - getFileCount={this.getFileCount} - onFileUploadChange={this.handleFileUploadChange} - onUploadStart={this.handleUploadStart} - onFileUpload={this.handleFileUploadComplete} - onUploadError={this.handleUploadError} - postType='post' - channelId='' - /> <a className={sendButtonClass} onClick={this.handleSubmit} diff --git a/webapp/components/emoji_picker/components/emoji_picker_category.jsx b/webapp/components/emoji_picker/components/emoji_picker_category.jsx new file mode 100644 index 000000000..1d5b12095 --- /dev/null +++ b/webapp/components/emoji_picker/components/emoji_picker_category.jsx @@ -0,0 +1,42 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +export default class EmojiPickerCategory extends React.Component { + static propTypes = { + category: React.PropTypes.string.isRequired, + icon: React.PropTypes.node.isRequired, + onCategoryClick: React.PropTypes.func.isRequired, + selected: React.PropTypes.bool.isRequired + } + + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + } + + handleClick(e) { + e.preventDefault(); + + this.props.onCategoryClick(this.props.category); + } + + render() { + let className = 'emoji-picker__category'; + if (this.props.selected) { + className += ' emoji-picker__category--selected'; + } + + return ( + <a + className={className} + href='#' + onClick={this.handleClick} + > + {this.props.icon} + </a> + ); + } +}
\ No newline at end of file diff --git a/webapp/components/emoji_picker/components/emoji_picker_item.jsx b/webapp/components/emoji_picker/components/emoji_picker_item.jsx new file mode 100644 index 000000000..3f38343fa --- /dev/null +++ b/webapp/components/emoji_picker/components/emoji_picker_item.jsx @@ -0,0 +1,70 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import EmojiStore from 'stores/emoji_store.jsx'; + +export default class EmojiPickerItem extends React.Component { + static propTypes = { + emoji: React.PropTypes.object.isRequired, + onItemOver: React.PropTypes.func.isRequired, + onItemOut: React.PropTypes.func.isRequired, + onItemClick: React.PropTypes.func.isRequired, + onItemUnmount: React.PropTypes.func.isRequired, + category: React.PropTypes.string.isRequired + } + + constructor(props) { + super(props); + + this.handleMouseOver = this.handleMouseOver.bind(this); + this.handleMouseOut = this.handleMouseOut.bind(this); + this.handleClick = this.handleClick.bind(this); + } + + componentWillUnmount() { + this.props.onItemUnmount(this.props.emoji); + } + + handleMouseOver() { + this.props.onItemOver(this.props.emoji); + } + + handleMouseOut() { + this.props.onItemOut(this.props.emoji); + } + + handleClick() { + this.props.onItemClick(this.props.emoji); + } + + render() { + let item = null; + + if (this.props.category === 'recent' || this.props.category === 'custom') { + item = + (<span> + <img + className='emoji-picker__item emoticon' + onMouseOver={this.handleMouseOver} + onMouseOut={this.handleMouseOut} + onClick={this.handleClick} + src={EmojiStore.getEmojiImageUrl(this.props.emoji)} + /> + </span>); + } else { + item = + (<div > + <img + src='/static/emoji/img_trans.gif' + className={' emojisprite emoji-' + this.props.emoji.filename + ' '} + onMouseOver={this.handleMouseOver} + onMouseOut={this.handleMouseOut} + onClick={this.handleClick} + /> + </div>); + } + return item; + } +} diff --git a/webapp/components/emoji_picker/components/emoji_picker_preview.jsx b/webapp/components/emoji_picker/components/emoji_picker_preview.jsx new file mode 100644 index 000000000..ac3f07025 --- /dev/null +++ b/webapp/components/emoji_picker/components/emoji_picker_preview.jsx @@ -0,0 +1,66 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import EmojiStore from 'stores/emoji_store.jsx'; + +import {FormattedMessage} from 'react-intl'; + +export default class EmojiPickerPreview extends React.Component { + static propTypes = { + emoji: React.PropTypes.object + } + + render() { + const emoji = this.props.emoji; + + if (emoji) { + let name; + let aliases; + let previewImage; + + if (emoji.aliases) { + // This is a system emoji which only has a list of aliases + name = emoji.aliases[0]; + aliases = emoji.aliases; + previewImage = (<span className='sprite-preview'><img + src='/static/emoji/img_trans.gif' + className={' emojisprite-preview emoji-' + emoji.filename + ' '} + align='absmiddle' + /></span>); + } else { + // This is a custom emoji that matches the model on the server + name = emoji.name; + aliases = [emoji.name]; + previewImage = (<img + className='emoji-picker__preview-image' + src={EmojiStore.getEmojiImageUrl(emoji)} + />); + } + + return ( + <div className='emoji-picker__preview'> + <div className='emoji-picker__preview-image-box'> + {previewImage} + </div> + <div className='emoji-picker__preview-image-box'> + <span className='emoji-picker__preview-name'>{name}</span> + <span + className='emoji-picker__preview-aliases' + >{ ':' + aliases[0] + ':'}</span> + </div> + </div> + ); + } + + return ( + <div className='emoji-picker__preview emoji-picker__preview-placeholder'> + <FormattedMessage + id='emoji_picker.emojiPicker' + defaultMessage='Emoji Picker' + /> + </div> + ); + } +} diff --git a/webapp/components/emoji_picker/emoji_picker.jsx b/webapp/components/emoji_picker/emoji_picker.jsx new file mode 100644 index 000000000..e12974054 --- /dev/null +++ b/webapp/components/emoji_picker/emoji_picker.jsx @@ -0,0 +1,417 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import $ from 'jquery'; +import * as Emoji from 'utils/emoji.jsx'; +import EmojiStore from 'stores/emoji_store.jsx'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ReactDOM from 'react-dom'; +import * as Utils from 'utils/utils.jsx'; +import ReactOutsideEvent from 'react-outside-event'; +import {FormattedMessage} from 'react-intl'; + +import EmojiPickerCategory from './components/emoji_picker_category.jsx'; +import EmojiPickerItem from './components/emoji_picker_item.jsx'; +import EmojiPickerPreview from './components/emoji_picker_preview.jsx'; + +// This should include all the categories available in Emoji.CategoryNames +const CATEGORIES = [ + 'recent', + 'people', + 'nature', + 'food', + 'activity', + 'travel', + 'objects', + 'symbols', + 'flags', + 'custom' +]; + +class EmojiPicker extends React.Component { + static propTypes = { + customEmojis: React.PropTypes.object, + onEmojiClick: React.PropTypes.func.isRequired, + topOrBottom: React.PropTypes.string.isRequired, + emojiOffset: React.PropTypes.number, + outsideClick: React.PropTypes.func + } + + constructor(props) { + super(props); + + // All props are primitives or treated as immutable + this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); + + this.handleCategoryClick = this.handleCategoryClick.bind(this); + this.handleFilterChange = this.handleFilterChange.bind(this); + this.handleItemOver = this.handleItemOver.bind(this); + this.handleItemOut = this.handleItemOut.bind(this); + this.handleItemClick = this.handleItemClick.bind(this); + this.handleScroll = this.handleScroll.bind(this); + this.handleItemUnmount = this.handleItemUnmount.bind(this); + this.renderCategory = this.renderCategory.bind(this); + this.onOutsideEvent = this.onOutsideEvent.bind(this); + + this.state = { + category: 'recent', + filter: '', + selected: null + }; + } + + componentDidMount() { + this.searchInput.focus(); + } + + onOutsideEvent = (event) => { + // Handle the event. + this.props.outsideClick(event); + } + + handleCategoryClick(category) { + const items = this.refs.items; + + if (category === CATEGORIES[0]) { + // First category includes the search box so just scroll to the top + items.scrollTop = 0; + } else { + const cat = this.refs[category]; + items.scrollTop = cat.offsetTop; + } + } + + handleFilterChange(e) { + this.setState({filter: e.target.value}); + } + + handleItemOver(emoji) { + clearTimeout(this.timeouthandler); + this.setState({selected: emoji}); + } + + handleItemOut() { + this.timeouthandler = setTimeout(() => this.setState({selected: null}), 500); + } + + handleItemUnmount(emoji) { + //Prevent emoji preview from showing emoji which is not present anymore (due to filter) + if (this.state.selected === emoji) { + this.setState({selected: null}); + } + } + + handleItemClick(emoji) { + this.props.onEmojiClick(emoji); + } + + handleScroll() { + const items = $(ReactDOM.findDOMNode(this.refs.items)); + + const contentTop = items.scrollTop(); + const contentTopPadding = parseInt(items.css('padding-top'), 10); + const scrollPct = (contentTop / (items[0].scrollHeight - items[0].clientHeight)) * 100.0; + + if (scrollPct > 99.0) { + this.setState({category: 'custom'}); + return; + } + + for (const category of CATEGORIES) { + const header = $(ReactDOM.findDOMNode(this.refs[category])); + const headerBottomMargin = parseInt(header.css('margin-bottom'), 10) + parseInt(header.css('padding-bottom'), 10); + const headerBottom = header[0].offsetTop + header.height() + headerBottomMargin; + + // If category is the first one visible, highlight it in the bar at the top + if (headerBottom - contentTopPadding >= contentTop) { + if (this.state.category !== category) { + this.setState({category: String(category)}); + } + + break; + } + } + } + renderCategory(category, filter) { + const items = []; + let indices = []; + let recentEmojis = []; + + if (category === 'recent') { + recentEmojis = EmojiStore.getRecentEmojis(); + indices = [...Array(recentEmojis.length).keys()]; + + // reverse indices so most recently added is first + indices.reverse(); + } else { + indices = Emoji.EmojiIndicesByCategory.get(category) || []; + } + + for (const index of indices) { + let emoji = {}; + if (category === 'recent') { + emoji = recentEmojis[index]; + } else { + emoji = Emoji.Emojis[index]; + } + if (filter) { + let matches = false; + + for (const alias of emoji.aliases || [...emoji.name]) { + if (alias.indexOf(filter) !== -1) { + matches = true; + break; + } + } + + if (!matches) { + continue; + } + } + + items.push( + <EmojiPickerItem + key={'system_' + (category === 'recent' ? 'recent_' : '') + (emoji.name || emoji.aliases[0])} + emoji={emoji} + category={category} + onItemOver={this.handleItemOver} + onItemOut={this.handleItemOut} + onItemClick={this.handleItemClick} + onItemUnmount={this.handleItemUnmount} + /> + ); + } + + if (category === 'custom') { + const customEmojis = EmojiStore.getCustomEmojiMap().values(); + + for (const emoji of customEmojis) { + if (filter && emoji.name.indexOf(filter) === -1) { + continue; + } + + items.push( + <EmojiPickerItem + key={'custom_' + emoji.name} + emoji={emoji} + category={category} + onItemOver={this.handleItemOver} + onItemOut={this.handleItemOut} + onItemClick={this.handleItemClick} + onItemUnmount={this.handleItemUnmount} + + /> + ); + } + } + + // Only render the header if there's any visible items + let header = null; + if (items.length > 0) { + header = ( + <div + className='emoji-picker__category-header' + > + <FormattedMessage id={'emoji_picker.' + category}/> + </div> + ); + } + + return ( + <div + key={'category_' + category} + id={'emojipickercat-' + category} + ref={category} + > + {header} + <div className='emoji-picker-items__container'> + {items} + </div> + </div> + ); + } + + renderPreview(selected) { + if (selected) { + let name; + let aliases; + let previewImage; + if (selected.name) { + // This is a custom emoji that matches the model on the server + name = selected.name; + aliases = [selected.name]; + previewImage = (<img + className='emoji-picker__preview-image' + align='absmiddle' + src={EmojiStore.getEmojiImageUrl(selected)} + />); + } else { + // This is a system emoji which only has a list of aliases + name = selected.aliases[0]; + aliases = selected.aliases; + previewImage = (<span ><img + src='/static/emoji/img_trans.gif' + className={' emojisprite-preview emoji-' + selected.filename + ' '} + align='absmiddle' + /></span>); + } + + return ( + <div className='emoji-picker__preview'> + {previewImage} + <span className='emoji-picker__preview-name'>{name}</span> + <span className='emoji-picker__preview-aliases'>{aliases.map((alias) => ':' + alias + ':').join(' ')}</span> + </div> + ); + } + + return ( + <span className='emoji-picker__preview-placeholder'> + <FormattedMessage + id='emoji_picker.emojiPicker' + defaultMessage='Emoji Picker' + /> + </span> + ); + } + + render() { + const items = []; + + for (const category of CATEGORIES) { + if (category === 'custom') { + items.push(this.renderCategory('custom', this.state.filter, this.props.customEmojis)); + } else { + items.push(this.renderCategory(category, this.state.filter)); + } + } + const cssclass = this.props.topOrBottom === 'top' ? 'emoji-picker' : 'emoji-picker-bottom'; + const pickerStyle = this.props.emojiOffset ? {top: this.props.emojiOffset} : {}; + return ( + <div + style={pickerStyle} + className={cssclass} + > + <div className='emoji-picker__categories'> + <EmojiPickerCategory + category='recent' + icon={<i + className='fa fa-clock-o' + title={Utils.localizeMessage('emoji_picker.recent', 'Recently Used')} + />} + onCategoryClick={this.handleCategoryClick} + selected={this.state.category === 'recent'} + /> + <EmojiPickerCategory + category='people' + icon={<i + className='fa fa-smile-o' + title={Utils.localizeMessage('emoji_picker.people', 'People')} + />} + onCategoryClick={this.handleCategoryClick} + selected={this.state.category === 'people'} + /> + <EmojiPickerCategory + category='nature' + icon={<i + className='fa fa-leaf' + title={Utils.localizeMessage('emoji_picker.nature', 'Nature')} + />} + onCategoryClick={this.handleCategoryClick} + selected={this.state.category === 'nature'} + /> + <EmojiPickerCategory + category='food' + icon={<i + className='fa fa-cutlery' + title={Utils.localizeMessage('emoji_picker.food', 'Food')} + />} + onCategoryClick={this.handleCategoryClick} + selected={this.state.category === 'food'} + /> + <EmojiPickerCategory + category='activity' + icon={<i + className='fa fa-futbol-o' + title={Utils.localizeMessage('emoji_picker.activity', 'Activity')} + />} + onCategoryClick={this.handleCategoryClick} + selected={this.state.category === 'activity'} + /> + <EmojiPickerCategory + category='travel' + icon={<i + className='fa fa-plane' + title={Utils.localizeMessage('emoji_picker.travel', 'Travel')} + />} + onCategoryClick={this.handleCategoryClick} + selected={this.state.category === 'travel'} + /> + <EmojiPickerCategory + category='objects' + icon={<i + className='fa fa-lightbulb-o' + title={Utils.localizeMessage('emoji_picker.objects', 'Objects')} + />} + onCategoryClick={this.handleCategoryClick} + selected={this.state.category === 'objects'} + /> + <EmojiPickerCategory + category='symbols' + icon={<i + className='fa fa-heart-o' + title={Utils.localizeMessage('emoji_picker.symbols', 'Symbols')} + />} + onCategoryClick={this.handleCategoryClick} + selected={this.state.category === 'symbols'} + /> + <EmojiPickerCategory + category='flags' + icon={<i + className='fa fa-flag-o' + title={Utils.localizeMessage('emoji_picker.flags', 'Flags')} + />} + onCategoryClick={this.handleCategoryClick} + selected={this.state.category === 'flags'} + /> + <EmojiPickerCategory + category='custom' + icon={<i + className='fa fa-at' + title={Utils.localizeMessage('emoji_picker.custom', 'Custom')} + />} + onCategoryClick={this.handleCategoryClick} + selected={this.state.category === 'custom'} + /> + </div> + <div className='emoji-picker__search-container'> + <span className='fa fa-search emoji-picker__search-icon'/> + <input + ref={(input) => { + this.searchInput = input; + }} + className='emoji-picker__search' + type='text' + onChange={this.handleFilterChange} + placeholder={Utils.localizeMessage('emoji_picker.search', 'search')} + /> + </div> + <div + ref='items' + id='emojipickeritems' + className='emoji-picker__items' + onScroll={this.handleScroll} + > + {items} + </div> + <EmojiPickerPreview emoji={this.state.selected}/> + </div> + ); + } +} + +// disabling eslint check for outslide click handler +// eslint-disable-next-line new-cap +export default ReactOutsideEvent(EmojiPicker, ['click']); diff --git a/webapp/components/emoji_picker/emoji_picker_container.jsx b/webapp/components/emoji_picker/emoji_picker_container.jsx new file mode 100644 index 000000000..7cdc0e4b9 --- /dev/null +++ b/webapp/components/emoji_picker/emoji_picker_container.jsx @@ -0,0 +1,46 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import EmojiStore from 'stores/emoji_store.jsx'; + +import EmojiPicker from './emoji_picker.jsx'; + +export default class EmojiPickerContainer extends React.Component { + static propTypes = { + onEmojiClick: React.PropTypes.func.isRequred + } + + constructor(props) { + super(props); + this.handleEmojiChange = this.handleEmojiChange.bind(this); + + this.state = { + customEmojis: EmojiStore.getCustomEmojiMap().values() ? EmojiStore.getCustomEmojiMap().values() : [] + }; + } + + componentDidMount() { + EmojiStore.addChangeListener(this.handleEmojiChange); + } + + componentWillUnount() { + EmojiStore.removeChangeListener(this.handleEmojiChange); + } + + handleEmojiChange() { + this.setState({ + customEmojis: EmojiStore.getCustomEmojiMap().values() + }); + } + + render() { + return ( + <EmojiPicker + customEmojis={EmojiStore.getCustomEmojiMap().values()} + onEmojiClick={this.props.onEmojiClick} + /> + ); + } +} diff --git a/webapp/components/file_upload.jsx b/webapp/components/file_upload.jsx index e1535c0bb..297095e0a 100644 --- a/webapp/components/file_upload.jsx +++ b/webapp/components/file_upload.jsx @@ -49,6 +49,7 @@ class FileUpload extends React.Component { this.pasteUpload = this.pasteUpload.bind(this); this.keyUpload = this.keyUpload.bind(this); this.handleMaxUploadReached = this.handleMaxUploadReached.bind(this); + this.emojiClick = this.emojiClick.bind(this); this.state = { requests: {} @@ -210,7 +211,9 @@ class FileUpload extends React.Component { // jquery-dragster doesn't provide a function to unregister itself so do it manually target.off('dragenter dragleave dragover drop dragster:enter dragster:leave dragster:over dragster:drop'); } - + emojiClick() { + this.props.onEmojiClick(); + } pasteUpload(e) { var inputDiv = ReactDOM.findDOMNode(this.refs.input); const {formatMessage} = this.props.intl; @@ -347,24 +350,33 @@ class FileUpload extends React.Component { const channelId = this.props.channelId || ChannelStore.getCurrentId(); const uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId); + const emojiSpan = (<span + className={'fa fa-smile-o icon--emoji-picker emoji-' + this.props.navBarName} + onClick={this.emojiClick} + />); + const filestyle = {visibility: 'hidden'}; return ( <span ref='input' className={'btn btn-file' + (uploadsRemaining <= 0 ? ' btn-file__disabled' : '')} > - <span - className='icon' - dangerouslySetInnerHTML={{__html: Constants.ATTACHMENT_ICON_SVG}} - /> - <input - ref='fileInput' - type='file' - onChange={this.handleChange} - onClick={uploadsRemaining > 0 ? this.props.onClick : this.handleMaxUploadReached} - multiple={multiple} - accept={accept} - /> + <div className='icon--attachment'> + <span + dangerouslySetInnerHTML={{__html: Constants.ATTACHMENT_ICON_SVG}} + onClick={() => this.refs.fileInput.click()} + /> + <input + ref='fileInput' + type='file' + style={filestyle} + onChange={this.handleChange} + onClick={uploadsRemaining > 0 ? this.props.onClick : this.handleMaxUploadReached} + multiple={multiple} + accept={accept} + /> + </div> + {this.props.emojiEnabled ? emojiSpan : ''} </span> ); } @@ -380,7 +392,10 @@ FileUpload.propTypes = { onFileUploadChange: React.PropTypes.func, onTextDrop: React.PropTypes.func, channelId: React.PropTypes.string, - postType: React.PropTypes.string + postType: React.PropTypes.string, + onEmojiClick: React.PropTypes.func, + navBarName: React.PropTypes.string, + emojiEnabled: React.PropTypes.bool }; export default injectIntl(FileUpload, {withRef: true}); diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 83d930bca..231033fb1 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -517,7 +517,10 @@ export default class RhsRootPost extends React.Component { }; return ( - <div className={'post post--root post--thread ' + userCss + ' ' + systemMessageClass + ' ' + compactClass}> + <div + id='thread--root' + className={'post post--root post--thread ' + userCss + ' ' + systemMessageClass + ' ' + compactClass} + > <div className='post-right-channel__name'>{channelName}</div> <div className='post__content'> {profilePicContainer} diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx index f1f6d2a0a..7f3dc1891 100644 --- a/webapp/components/textbox.jsx +++ b/webapp/components/textbox.jsx @@ -32,7 +32,8 @@ export default class Textbox extends React.Component { onBlur: React.PropTypes.func, supportsCommands: React.PropTypes.bool.isRequired, handlePostError: React.PropTypes.func, - suggestionListStyle: React.PropTypes.string + suggestionListStyle: React.PropTypes.string, + emojiEnabled: React.PropTypes.bool }; static defaultProps = { @@ -249,7 +250,7 @@ export default class Textbox extends React.Component { <SuggestionBox id={this.props.id} ref='message' - className={`form-control custom-textarea ${this.state.connection}`} + className={`form-control custom-textarea${this.props.emojiEnabled ? '-emoji' : ''} ${this.state.connection}`} type='textarea' spellCheck='true' placeholder={this.props.createMessage} diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx index 970856acc..cf05ab402 100644 --- a/webapp/components/user_settings/user_settings_advanced.jsx +++ b/webapp/components/user_settings/user_settings_advanced.jsx @@ -351,6 +351,13 @@ export default class AdvancedSettingsDisplay extends React.Component { defaultMessage='Enable the ability to make and receive one-on-one WebRTC calls' /> ); + case 'EMOJI_PICKER_PREVIEW': + return ( + <FormattedMessage + id='user.settings.advance.emojipicker' + defaultMessage='Enable the emoji picker' + /> + ); default: return null; } |