diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | web/web_test.go | 3 | ||||
-rw-r--r-- | webapp/action_creators/global_actions.jsx | 34 | ||||
-rw-r--r-- | webapp/action_creators/websocket_actions.jsx | 227 | ||||
-rw-r--r-- | webapp/components/create_comment.jsx | 8 | ||||
-rw-r--r-- | webapp/components/create_post.jsx | 7 | ||||
-rw-r--r-- | webapp/components/logged_in.jsx | 13 | ||||
-rw-r--r-- | webapp/components/msg_typing.jsx | 78 | ||||
-rw-r--r-- | webapp/components/sidebar.jsx | 15 | ||||
-rw-r--r-- | webapp/components/tutorial/tutorial_intro_screens.jsx | 19 | ||||
-rw-r--r-- | webapp/components/tutorial/tutorial_tip.jsx | 11 | ||||
-rw-r--r-- | webapp/components/user_settings/user_settings_advanced.jsx | 46 | ||||
-rw-r--r-- | webapp/components/user_settings/user_settings_display.jsx | 40 | ||||
-rw-r--r-- | webapp/root.jsx | 14 | ||||
-rw-r--r-- | webapp/stores/notificaiton_store.jsx | 98 | ||||
-rw-r--r-- | webapp/stores/preference_store.jsx | 113 | ||||
-rw-r--r-- | webapp/stores/socket_store.jsx | 343 | ||||
-rw-r--r-- | webapp/stores/user_typing_store.jsx | 108 | ||||
-rw-r--r-- | webapp/utils/async_client.jsx | 17 | ||||
-rw-r--r-- | webapp/utils/constants.jsx | 2 | ||||
-rw-r--r-- | webapp/utils/utils.jsx | 10 |
21 files changed, 624 insertions, 584 deletions
@@ -174,7 +174,7 @@ endif # Create package tar -C dist -czf $(DIST_PATH).tar.gz mattermost -run-server: prepare-enterprise +run-server: prepare-enterprise start-docker @echo Running mattermost for development mkdir -p $(BUILD_WEBAPP_DIR)/dist/files diff --git a/web/web_test.go b/web/web_test.go index 8305ca34e..8dde5d747 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -4,7 +4,6 @@ package web import ( - "net/http" "net/url" "strings" "testing" @@ -40,6 +39,7 @@ func TearDown() { } } +/* Test disabled for now so we don't requrie the client to build. Maybe re-enable after client gets moved out. func TestStatic(t *testing.T) { Setup() @@ -54,6 +54,7 @@ func TestStatic(t *testing.T) { t.Fatalf("couldn't get static files %v", resp.StatusCode) } } +*/ func TestGetAccessToken(t *testing.T) { Setup() diff --git a/webapp/action_creators/global_actions.jsx b/webapp/action_creators/global_actions.jsx index 0280d5974..ab38532a6 100644 --- a/webapp/action_creators/global_actions.jsx +++ b/webapp/action_creators/global_actions.jsx @@ -10,6 +10,7 @@ const ActionTypes = Constants.ActionTypes; import * as AsyncClient from 'utils/async_client.jsx'; import * as Client from 'utils/client.jsx'; import * as Utils from 'utils/utils.jsx'; +import * as Websockets from './websocket_actions.jsx'; import * as I18n from 'i18n/i18n.jsx'; import en from 'i18n/en.json'; @@ -97,10 +98,21 @@ export function emitLoadMorePostsFocusedBottomEvent() { AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE); } -export function emitPostRecievedEvent(post) { +export function emitPostRecievedEvent(post, websocketMessageProps) { + if (ChannelStore.getCurrentId() === post.channel_id) { + if (window.isActive) { + AsyncClient.updateLastViewedAt(); + } else { + AsyncClient.getChannel(post.channel_id); + } + } else { + AsyncClient.getChannel(post.channel_id); + } + AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST, - post + post, + websocketMessageProps }); } @@ -261,3 +273,21 @@ export function viewLoggedIn() { // Clear pending posts (shouldn't have pending posts if we are loading) PostStore.clearPendingPosts(); } + +var lastTimeTypingSent = 0; +export function emitLocalUserTypingEvent(channelId, parentId) { + const t = Date.now(); + if ((t - lastTimeTypingSent) > Constants.UPDATE_TYPING_MS) { + Websockets.sendMessage({channel_id: channelId, action: 'typing', props: {parent_id: parentId}, state: {}}); + lastTimeTypingSent = t; + } +} + +export function emitRemoteUserTypingEvent(channelId, userId, postParentId) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.USER_TYPING, + channelId, + userId, + postParentId + }); +} diff --git a/webapp/action_creators/websocket_actions.jsx b/webapp/action_creators/websocket_actions.jsx new file mode 100644 index 000000000..55a76dbea --- /dev/null +++ b/webapp/action_creators/websocket_actions.jsx @@ -0,0 +1,227 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import UserStore from 'stores/user_store.jsx'; +import PostStore from 'stores/post_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import BrowserStore from 'stores/browser_store.jsx'; +import ErrorStore from 'stores/error_store.jsx'; + +import * as Utils from 'utils/utils.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; + +import Constants from 'utils/constants.jsx'; +const SocketEvents = Constants.SocketEvents; + +const MAX_WEBSOCKET_FAILS = 7; +const WEBSOCKET_RETRY_TIME = 3000; + +var conn = null; +var connectFailCount = 0; +var pastFirstInit = false; + +export function initialize() { + if (window.WebSocket && !conn) { + let protocol = 'ws://'; + if (window.location.protocol === 'https:') { + protocol = 'wss://'; + } + + const connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket'; + + if (connectFailCount === 0) { + console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console + } + + conn = new WebSocket(connUrl); + + conn.onopen = () => { + if (connectFailCount > 0) { + console.log('websocket re-established connection'); //eslint-disable-line no-console + AsyncClient.getChannels(); + AsyncClient.getPosts(ChannelStore.getCurrentId()); + } + + if (pastFirstInit) { + ErrorStore.clearLastError(); + ErrorStore.emitChange(); + } + + pastFirstInit = true; + connectFailCount = 0; + }; + + conn.onclose = () => { + conn = null; + + if (connectFailCount === 0) { + console.log('websocket closed'); //eslint-disable-line no-console + } + + connectFailCount = connectFailCount + 1; + + if (connectFailCount > MAX_WEBSOCKET_FAILS) { + ErrorStore.storeLastError(Utils.localizeMessage('channel_loader.socketError', 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.')); + } + + ErrorStore.setConnectionErrorCount(connectFailCount); + ErrorStore.emitChange(); + + setTimeout( + () => { + initialize(); + }, + WEBSOCKET_RETRY_TIME + ); + }; + + conn.onerror = (evt) => { + if (connectFailCount <= 1) { + console.log('websocket error'); //eslint-disable-line no-console + console.log(evt); //eslint-disable-line no-console + } + }; + + conn.onmessage = (evt) => { + const msg = JSON.parse(evt.data); + handleMessage(msg); + }; + } +} + +function handleMessage(msg) { + // Let the store know we are online. This probably shouldn't be here. + UserStore.setStatus(msg.user_id, 'online'); + + switch (msg.action) { + case SocketEvents.POSTED: + case SocketEvents.EPHEMERAL_MESSAGE: + handleNewPostEvent(msg); + break; + + case SocketEvents.POST_EDITED: + handlePostEditEvent(msg); + break; + + case SocketEvents.POST_DELETED: + handlePostDeleteEvent(msg); + break; + + case SocketEvents.NEW_USER: + handleNewUserEvent(); + break; + + case SocketEvents.USER_ADDED: + handleUserAddedEvent(msg); + break; + + case SocketEvents.USER_REMOVED: + handleUserRemovedEvent(msg); + break; + + case SocketEvents.CHANNEL_VIEWED: + handleChannelViewedEvent(msg); + break; + + case SocketEvents.PREFERENCE_CHANGED: + handlePreferenceChangedEvent(msg); + break; + + case SocketEvents.TYPING: + handleUserTypingEvent(msg); + break; + + default: + } +} + +export function sendMessage(msg) { + if (conn && conn.readyState === WebSocket.OPEN) { + conn.send(JSON.stringify(msg)); + } else if (!conn || conn.readyState === WebSocket.Closed) { + conn = null; + this.initialize(); + } +} + +export function close() { + if (conn && conn.readyState === WebSocket.OPEN) { + conn.close(); + } +} + +function handleNewPostEvent(msg) { + const post = JSON.parse(msg.props.post); + GlobalActions.emitPostRecievedEvent(post, msg.props); +} + +function handlePostEditEvent(msg) { + // Store post + const post = JSON.parse(msg.props.post); + PostStore.storePost(post); + PostStore.emitChange(); + + // Update channel state + if (ChannelStore.getCurrentId() === msg.channel_id) { + if (window.isActive) { + AsyncClient.updateLastViewedAt(); + } + } +} + +function handlePostDeleteEvent(msg) { + const post = JSON.parse(msg.props.post); + GlobalActions.emitPostDeletedEvent(post); +} + +function handleNewUserEvent() { + AsyncClient.getProfiles(); + AsyncClient.getChannelExtraInfo(); +} + +function handleUserAddedEvent(msg) { + if (ChannelStore.getCurrentId() === msg.channel_id) { + AsyncClient.getChannelExtraInfo(); + } + + if (UserStore.getCurrentId() === msg.user_id) { + AsyncClient.getChannel(msg.channel_id); + } +} + +function handleUserRemovedEvent(msg) { + if (UserStore.getCurrentId() === msg.user_id) { + AsyncClient.getChannels(); + + if (msg.props.remover_id !== msg.user_id && + msg.channel_id === ChannelStore.getCurrentId() && + $('#removed_from_channel').length > 0) { + var sentState = {}; + sentState.channelName = ChannelStore.getCurrent().display_name; + sentState.remover = UserStore.getProfile(msg.props.remover_id).username; + + BrowserStore.setItem('channel-removed-state', sentState); + $('#removed_from_channel').modal('show'); + } + } else if (ChannelStore.getCurrentId() === msg.channel_id) { + AsyncClient.getChannelExtraInfo(); + } +} + +function handleChannelViewedEvent(msg) { + // Useful for when multiple devices have the app open to different channels + if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) { + AsyncClient.getChannel(msg.channel_id); + } +} + +function handlePreferenceChangedEvent(msg) { + const preference = JSON.parse(msg.props.preference); + GlobalActions.emitPreferenceChangedEvent(preference); +} + +function handleUserTypingEvent(msg) { + GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.props.parent_id); +} diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx index 0aeb70c57..177f282d3 100644 --- a/webapp/components/create_comment.jsx +++ b/webapp/components/create_comment.jsx @@ -6,7 +6,6 @@ import ReactDOM from 'react-dom'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as Client from 'utils/client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; -import SocketStore from 'stores/socket_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PostDeletedModal from './post_deleted_modal.jsx'; @@ -17,6 +16,7 @@ import MsgTyping from './msg_typing.jsx'; import FileUpload from './file_upload.jsx'; import FilePreview from './file_preview.jsx'; import * as Utils from 'utils/utils.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import Constants from 'utils/constants.jsx'; @@ -196,11 +196,7 @@ class CreateComment extends React.Component { } } - const t = Date.now(); - if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) { - SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}}); - this.lastTime = t; - } + GlobalActions.emitLocalUserTypingEvent(this.props.channelId, this.props.rootId); } handleUserInput(messageText) { let draft = PostStore.getCommentDraft(this.props.rootId); diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx index 36bfbf22d..e5e99debd 100644 --- a/webapp/components/create_post.jsx +++ b/webapp/components/create_post.jsx @@ -19,7 +19,6 @@ import ChannelStore from 'stores/channel_store.jsx'; import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; -import SocketStore from 'stores/socket_store.jsx'; import Constants from 'utils/constants.jsx'; @@ -213,11 +212,7 @@ class CreatePost extends React.Component { } } - const t = Date.now(); - if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) { - SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}}); - this.lastTime = t; - } + GlobalActions.emitLocalUserTypingEvent(this.state.channelId, ''); } handleUserInput(messageText) { this.setState({messageText}); diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx index 6d35ff8c2..7ddb6b83d 100644 --- a/webapp/components/logged_in.jsx +++ b/webapp/components/logged_in.jsx @@ -5,12 +5,12 @@ import $ from 'jquery'; import * as AsyncClient from 'utils/async_client.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import UserStore from 'stores/user_store.jsx'; -import SocketStore from 'stores/socket_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; import ErrorBar from 'components/error_bar.jsx'; +import * as Websockets from 'action_creators/websocket_actions.jsx'; import {browserHistory} from 'react-router'; @@ -66,11 +66,6 @@ export default class LoggedIn extends React.Component { } } } - onSocketChange(msg) { - if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) { - UserStore.setStatus(msg.user_id, 'online'); - } - } componentWillMount() { // Emit view action GlobalActions.viewLoggedIn(); @@ -78,8 +73,8 @@ export default class LoggedIn extends React.Component { // Listen for user UserStore.addChangeListener(this.onUserChanged); - // Add listner for socker store - SocketStore.addChangeListener(this.onSocketChange); + // Initalize websockets + Websockets.initialize(); // Get all statuses regularally. (Soon to be switched to websocket) this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL); @@ -178,7 +173,7 @@ export default class LoggedIn extends React.Component { $(window).off('focus'); $(window).off('blur'); - SocketStore.removeChangeListener(this.onSocketChange); + Websockets.close(); UserStore.removeChangeListener(this.onUserChanged); $('body').off('click.userpopover'); diff --git a/webapp/components/msg_typing.jsx b/webapp/components/msg_typing.jsx index b1781623c..b2d414287 100644 --- a/webapp/components/msg_typing.jsx +++ b/webapp/components/msg_typing.jsx @@ -1,21 +1,9 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import SocketStore from 'stores/socket_store.jsx'; -import UserStore from 'stores/user_store.jsx'; +import UserTypingStore from 'stores/user_typing_store.jsx'; -import Constants from 'utils/constants.jsx'; - -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; - -const SocketEvents = Constants.SocketEvents; - -const holders = defineMessages({ - someone: { - id: 'msg_typing.someone', - defaultMessage: 'Someone' - } -}); +import {FormattedMessage} from 'react-intl'; import React from 'react'; @@ -23,69 +11,40 @@ class MsgTyping extends React.Component { constructor(props) { super(props); - this.onChange = this.onChange.bind(this); + this.onTypingChange = this.onTypingChange.bind(this); this.updateTypingText = this.updateTypingText.bind(this); this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this); - this.typingUsers = {}; this.state = { text: '' }; } - componentDidMount() { - SocketStore.addChangeListener(this.onChange); + componentWillMount() { + UserTypingStore.addChangeListener(this.onTypingChange); + this.onTypingChange(); + } + + componentWillUnmount() { + UserTypingStore.removeChangeListener(this.onTypingChange); } componentWillReceiveProps(nextProps) { if (this.props.channelId !== nextProps.channelId) { - for (const u in this.typingUsers) { - if (!this.typingUsers.hasOwnProperty(u)) { - continue; - } - - clearTimeout(this.typingUsers[u]); - } - this.typingUsers = {}; - this.setState({text: ''}); + this.updateTypingText(UserTypingStore.getUsersTyping(nextProps.channelId, nextProps.parentId)); } } - componentWillUnmount() { - SocketStore.removeChangeListener(this.onChange); + onTypingChange() { + this.updateTypingText(UserTypingStore.getUsersTyping(this.props.channelId, this.props.parentId)); } - onChange(msg) { - let username = this.props.intl.formatMessage(holders.someone); - if (msg.action === SocketEvents.TYPING && - this.props.channelId === msg.channel_id && - this.props.parentId === msg.props.parent_id) { - if (UserStore.hasProfile(msg.user_id)) { - username = UserStore.getProfile(msg.user_id).username; - } - - if (this.typingUsers[username]) { - clearTimeout(this.typingUsers[username]); - } - - this.typingUsers[username] = setTimeout(function myTimer(user) { - delete this.typingUsers[user]; - this.updateTypingText(); - }.bind(this, username), Constants.UPDATE_TYPING_MS); - - this.updateTypingText(); - } else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) { - if (UserStore.hasProfile(msg.user_id)) { - username = UserStore.getProfile(msg.user_id).username; - } - clearTimeout(this.typingUsers[username]); - delete this.typingUsers[username]; - this.updateTypingText(); + updateTypingText(typingUsers) { + if (!typingUsers) { + return; } - } - updateTypingText() { - const users = Object.keys(this.typingUsers); + const users = Object.keys(typingUsers); let text = ''; switch (users.length) { case 0: @@ -129,9 +88,8 @@ class MsgTyping extends React.Component { } MsgTyping.propTypes = { - intl: intlShape.isRequired, channelId: React.PropTypes.string, parentId: React.PropTypes.string }; -export default injectIntl(MsgTyping);
\ No newline at end of file +export default MsgTyping; diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index f9f31092f..0e1b7dd0e 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -93,12 +93,12 @@ export default class Sidebar extends React.Component { const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); const directChannels = []; - for (const preference of preferences) { - if (preference.value !== 'true') { + for (const [name, value] of preferences) { + if (value !== 'true') { continue; } - const teammateId = preference.name; + const teammateId = name; let directChannel = channels.find(Utils.isDirectChannelForUser.bind(null, teammateId)); @@ -242,11 +242,10 @@ export default class Sidebar extends React.Component { if (!this.isLeaving.get(channel.id)) { this.isLeaving.set(channel.id, true); - const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'false'); - - // bypass AsyncClient since we've already saved the updated preferences - Client.savePreferences( - [preference], + AsyncClient.savePreference( + Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + channel.teammate_id, + 'false', () => { this.isLeaving.set(channel.id, false); }, diff --git a/webapp/components/tutorial/tutorial_intro_screens.jsx b/webapp/components/tutorial/tutorial_intro_screens.jsx index 5db45523e..734842cad 100644 --- a/webapp/components/tutorial/tutorial_intro_screens.jsx +++ b/webapp/components/tutorial/tutorial_intro_screens.jsx @@ -36,17 +36,22 @@ export default class TutorialIntroScreens extends React.Component { Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL)); - let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + let step = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 0); - const newValue = (parseInt(preference.value, 10) + 1).toString(); - - preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue); - AsyncClient.savePreferences([preference]); + AsyncClient.savePreference( + Preferences.TUTORIAL_STEP, + UserStore.getCurrentId(), + step + 1 + ); } skipTutorial(e) { e.preventDefault(); - const preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), '999'); - AsyncClient.savePreferences([preference]); + + AsyncClient.savePreference( + Preferences.TUTORIAL_STEP, + UserStore.getCurrentId(), + 999 + ); } createScreen() { switch (this.state.currentScreen) { diff --git a/webapp/components/tutorial/tutorial_tip.jsx b/webapp/components/tutorial/tutorial_tip.jsx index ab49d4b04..d93fff1b1 100644 --- a/webapp/components/tutorial/tutorial_tip.jsx +++ b/webapp/components/tutorial/tutorial_tip.jsx @@ -29,12 +29,13 @@ export default class TutorialTip extends React.Component { this.setState({show}); if (!show && this.state.currentScreen >= this.props.screens.length - 1) { - let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + let step = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 0); - const newValue = (parseInt(preference.value, 10) + 1).toString(); - - preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue); - AsyncClient.savePreferences([preference]); + AsyncClient.savePreference( + Preferences.TUTORIAL_STEP, + UserStore.getCurrentId(), + step + 1 + ); } } handleNext() { diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx index 7c496f57b..4fcdc9a41 100644 --- a/webapp/components/user_settings/user_settings_advanced.jsx +++ b/webapp/components/user_settings/user_settings_advanced.jsx @@ -1,11 +1,12 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; import Constants from 'utils/constants.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; +import UserStore from 'stores/user_store.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; @@ -68,25 +69,27 @@ class AdvancedSettingsDisplay extends React.Component { const preReleaseFeaturesKeys = Object.keys(PreReleaseFeatures); const advancedSettings = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS); const settings = { - send_on_ctrl_enter: PreferenceStore.getPreference( + send_on_ctrl_enter: PreferenceStore.get( Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', - {value: 'false'} - ).value + 'false' + ) }; let enabledFeatures = 0; - advancedSettings.forEach((setting) => { - preReleaseFeaturesKeys.forEach((key) => { + for (const [name, value] of advancedSettings) { + for (const key of preReleaseFeaturesKeys) { const feature = PreReleaseFeatures[key]; - if (setting.name === Constants.FeatureTogglePrefix + feature.label) { - settings[setting.name] = setting.value; - if (setting.value === 'true') { - enabledFeatures++; + + if (name === Constants.FeatureTogglePrefix + feature.label) { + settings[name] = value; + + if (value === 'true') { + enabledFeatures += 1; } } - }); - }); + } + } this.state = {preReleaseFeatures: PreReleaseFeatures, settings, preReleaseFeaturesKeys, enabledFeatures}; } @@ -124,20 +127,21 @@ class AdvancedSettingsDisplay extends React.Component { handleSubmit(settings) { const preferences = []; + const userId = UserStore.getCurrentId(); + // this should be refactored so we can actually be certain about what type everything is (Array.isArray(settings) ? settings : [settings]).forEach((setting) => { - preferences.push( - PreferenceStore.setPreference( - Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, - setting, - String(this.state.settings[setting]) - ) - ); + preferences.push({ + user_id: userId, + category: Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, + name: setting, + value: this.state.settings[setting] + }); }); - Client.savePreferences(preferences, + AsyncClient.savePreferences( + preferences, () => { - PreferenceStore.emitChange(); this.updateSection(''); }, (err) => { diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx index 3299588f7..e56156049 100644 --- a/webapp/components/user_settings/user_settings_display.jsx +++ b/webapp/components/user_settings/user_settings_display.jsx @@ -6,24 +6,22 @@ import SettingItemMax from '../setting_item_max.jsx'; import ManageLanguages from './manage_languages.jsx'; import ThemeSetting from './user_settings_theme.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; +import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; import * as I18n from 'i18n/i18n.jsx'; import Constants from 'utils/constants.jsx'; +const Preferences = Constants.Preferences; -import {savePreferences} from 'utils/client.jsx'; import {FormattedMessage} from 'react-intl'; function getDisplayStateFromStores() { - const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'}); - const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'username'}); - const selectedFont = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', {value: Constants.DEFAULT_FONT}); - return { - militaryTime: militaryTime.value, - nameFormat: nameFormat.value, - selectedFont: selectedFont.value + militaryTime: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', 'false'), + nameFormat: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'username'), + selectedFont: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT) }; } @@ -44,13 +42,29 @@ export default class UserSettingsDisplay extends React.Component { this.state = getDisplayStateFromStores(); } handleSubmit() { - const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime); - const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat); - const fontPreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', this.state.selectedFont); + const userId = UserStore.getCurrentId(); + + const timePreference = { + user_id: userId, + category: Preferences.CATEGORY_DISPLAY_SETTINGS, + name: 'use_military_time', + value: this.state.militaryTime + }; + const namePreference = { + user_id: userId, + category: Preferences.CATEGORY_DISPLAY_SETTINGS, + name: 'name_format', + value: this.state.nameFormat + }; + const fontPreference = { + user_id: userId, + category: Preferences.CATEGORY_DISPLAY_SETTINGS, + name: 'selected_font', + value: this.state.selectedFont + }; - savePreferences([timePreference, namePreference, fontPreference], + AsyncClient.savePreferences([timePreference, namePreference, fontPreference], () => { - PreferenceStore.emitChange(); this.updateSection(''); }, (err) => { diff --git a/webapp/root.jsx b/webapp/root.jsx index 2f0ef441c..63fbb4422 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -25,11 +25,11 @@ import Sidebar from 'components/sidebar.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; -import SocketStore from 'stores/socket_store.jsx'; import ErrorStore from 'stores/error_store.jsx'; import BrowserStore from 'stores/browser_store.jsx'; import SignupTeam from 'components/signup_team.jsx'; import * as Client from 'utils/client.jsx'; +import * as Websockets from 'action_creators/websocket_actions.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import SignupTeamConfirm from 'components/signup_team_confirm.jsx'; import SignupUserComplete from 'components/signup_user_complete.jsx'; @@ -102,11 +102,10 @@ function preRenderSetup(callwhendone) { // Do Nothing }; + // Make sure the websockets close $(window).on('beforeunload', () => { - if (window.SocketStore) { - SocketStore.close(); - } + Websockets.close(); } ); @@ -139,11 +138,7 @@ function preRenderSetup(callwhendone) { function preLoggedIn(nextState, replace, callback) { const d1 = Client.getAllPreferences( (data) => { - if (!data) { - return; - } - - PreferenceStore.setPreferences(data); + PreferenceStore.setPreferencesFromServer(data); }, (err) => { AsyncClient.dispatchError(err, 'getAllPreferences'); @@ -199,6 +194,7 @@ function onLoggedOut(nextState) { BrowserStore.signalLogout(); BrowserStore.clear(); ErrorStore.clearLastError(); + PreferenceStore.clear(); }, () => { browserHistory.push('/' + teamName + '/login'); diff --git a/webapp/stores/notificaiton_store.jsx b/webapp/stores/notificaiton_store.jsx new file mode 100644 index 000000000..70caffeb6 --- /dev/null +++ b/webapp/stores/notificaiton_store.jsx @@ -0,0 +1,98 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; +import Constants from 'utils/constants.jsx'; +import UserStore from './user_store.jsx'; +import ChannelStore from './channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; + +class NotificationStoreClass extends EventEmitter { + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + handleRecievedPost(post, msgProps) { + // Send desktop notification + if ((UserStore.getCurrentId() !== post.user_id || post.props.from_webhook === 'true') && !Utils.isSystemMessage(post)) { + let mentions = []; + if (msgProps.mentions) { + mentions = JSON.parse(msgProps.mentions); + } + + const channel = ChannelStore.get(post.channel_id); + const user = UserStore.getCurrentUser(); + const member = ChannelStore.getMember(post.channel_id); + + let notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default'; + if (notifyLevel === 'default') { + notifyLevel = user.notify_props.desktop; + } + + if (notifyLevel === 'none') { + return; + } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== Constants.DM_CHANNEL) { + return; + } + + let username = Utils.localizeMessage('channel_loader.someone', 'Someone'); + if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { + username = post.props.override_username; + } else if (UserStore.hasProfile(post.user_id)) { + username = UserStore.getProfile(post.user_id).username; + } + + let title = Utils.localizeMessage('channel_loader.posted', 'Posted'); + if (channel) { + title = channel.display_name; + } + + let notifyText = post.message.replace(/\n+/g, ' '); + if (notifyText.length > 50) { + notifyText = notifyText.substring(0, 49) + '...'; + } + + if (notifyText.length === 0) { + if (msgProps.image) { + Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.uploadedImage', ' uploaded an image'), channel); + } else if (msgProps.otherFile) { + Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.uploadedFile', ' uploaded a file'), channel); + } else { + Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.something', ' did something new'), channel); + } + } else { + Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.wrote', ' wrote: ') + notifyText, channel); + } + if (!user.notify_props || user.notify_props.desktop_sound === 'true') { + Utils.ding(); + } + } + } +} + +var NotificationStore = new NotificationStoreClass(); + +NotificationStore.dispatchToken = AppDispatcher.register((payload) => { + const action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_POST: + NotificationStore.handleRecievedPost(action.post, action.webspcketMessageProps); + NotificationStore.emitChange(); + break; + } +}); + +export default NotificationStore; diff --git a/webapp/stores/preference_store.jsx b/webapp/stores/preference_store.jsx index df77f0d51..fcfd1c426 100644 --- a/webapp/stores/preference_store.jsx +++ b/webapp/stores/preference_store.jsx @@ -4,143 +4,80 @@ import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import BrowserStore from './browser_store.jsx'; import EventEmitter from 'events'; -import UserStore from 'stores/user_store.jsx'; const CHANGE_EVENT = 'change'; -function getPreferenceKey(category, name) { - return `${category}-${name}`; -} - -function getPreferenceKeyForModel(preference) { - return `${preference.category}-${preference.name}`; -} - class PreferenceStoreClass extends EventEmitter { constructor() { super(); - this.getAllPreferences = this.getAllPreferences.bind(this); - this.get = this.get.bind(this); - this.getBool = this.getBool.bind(this); - this.getInt = this.getInt.bind(this); - this.getPreference = this.getPreference.bind(this); - this.getCategory = this.getCategory.bind(this); - this.getPreferencesWhere = this.getPreferencesWhere.bind(this); - this.setAllPreferences = this.setAllPreferences.bind(this); - this.setPreference = this.setPreference.bind(this); - - this.emitChange = this.emitChange.bind(this); - this.addChangeListener = this.addChangeListener.bind(this); - this.removeChangeListener = this.removeChangeListener.bind(this); - this.handleEventPayload = this.handleEventPayload.bind(this); this.dispatchToken = AppDispatcher.register(this.handleEventPayload); + + this.preferences = new Map(); } - getAllPreferences() { - return new Map(BrowserStore.getItem('preferences', [])); + getKey(category, name) { + return `${category}--${name}`; } get(category, name, defaultValue = '') { - const preference = this.getAllPreferences().get(getPreferenceKey(category, name)); + const key = this.getKey(category, name); - if (!preference) { + if (!this.preferences.has(key)) { return defaultValue; } - return preference.value || defaultValue; + return this.preferences.get(key); } getBool(category, name, defaultValue = false) { - const preference = this.getAllPreferences().get(getPreferenceKey(category, name)); + const key = this.getKey(category, name); - if (!preference) { + if (!this.preferences.has(key)) { return defaultValue; } - // prevent a non-false default value from being returned instead of an actual false value - if (preference.value === 'false') { - return false; - } - - return (preference.value !== 'false') || defaultValue; + return this.preferences.get(key) !== 'false'; } getInt(category, name, defaultValue = 0) { - const preference = this.getAllPreferences().get(getPreferenceKey(category, name)); + const key = this.getKey(category, name); - if (!preference) { + if (!this.preferences.has(key)) { return defaultValue; } - // prevent a non-zero default value from being returned instead of an actual 0 value - if (preference.value === '0') { - return 0; - } - - return parseInt(preference.value, 10) || defaultValue; - } - - getPreference(category, name, defaultValue = {}) { - return this.getAllPreferences().get(getPreferenceKey(category, name)) || defaultValue; + return parseInt(this.preferences.get(key), 10); } getCategory(category) { - return this.getPreferencesWhere((preference) => (preference.category === category)); - } + const prefix = category + '--'; - getPreferencesWhere(pred) { - const all = this.getAllPreferences(); - const preferences = []; + const preferences = new Map(); - for (const [, preference] of all) { - if (pred(preference)) { - preferences.push(preference); + for (const [key, value] of this.preferences) { + if (key.startsWith(prefix)) { + preferences.set(key.substring(prefix.length), value); } } return preferences; } - setAllPreferences(preferences) { - // note that we store the preferences as an array of key-value pairs so that we can deserialize - // it as a proper Map instead of an object - BrowserStore.setItem('preferences', [...preferences]); - } - setPreference(category, name, value) { - const preferences = this.getAllPreferences(); - - const key = getPreferenceKey(category, name); - let preference = preferences.get(key); - - if (!preference) { - preference = { - user_id: UserStore.getCurrentId(), - category, - name - }; - } - preference.value = value; - - preferences.set(key, preference); - - this.setAllPreferences(preferences); - - return preference; + this.preferences.set(this.getKey(category, name), value); } - setPreferences(newPreferences) { - const preferences = this.getAllPreferences(); - + setPreferencesFromServer(newPreferences) { for (const preference of newPreferences) { - preferences.set(getPreferenceKeyForModel(preference), preference); + this.setPreference(preference.category, preference.name, preference.value); } + } - this.setAllPreferences(preferences); + clear() { + this.preferences.clear(); } emitChange() { @@ -166,7 +103,7 @@ class PreferenceStoreClass extends EventEmitter { break; } case ActionTypes.RECEIVED_PREFERENCES: - this.setPreferences(action.preferences); + this.setPreferencesFromServer(action.preferences); this.emitChange(); break; } diff --git a/webapp/stores/socket_store.jsx b/webapp/stores/socket_store.jsx deleted file mode 100644 index 5d6302743..000000000 --- a/webapp/stores/socket_store.jsx +++ /dev/null @@ -1,343 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import $ from 'jquery'; -import UserStore from './user_store.jsx'; -import PostStore from './post_store.jsx'; -import ChannelStore from './channel_store.jsx'; -import BrowserStore from './browser_store.jsx'; -import ErrorStore from './error_store.jsx'; -import EventEmitter from 'events'; - -import * as Utils from 'utils/utils.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; -import * as GlobalActions from 'action_creators/global_actions.jsx'; - -import Constants from 'utils/constants.jsx'; -const SocketEvents = Constants.SocketEvents; - -const CHANGE_EVENT = 'change'; - -var conn; - -class SocketStoreClass extends EventEmitter { - constructor() { - super(); - - this.initialize = this.initialize.bind(this); - this.emitChange = this.emitChange.bind(this); - this.addChangeListener = this.addChangeListener.bind(this); - this.removeChangeListener = this.removeChangeListener.bind(this); - this.sendMessage = this.sendMessage.bind(this); - this.close = this.close.bind(this); - - this.failCount = 0; - this.isInitialize = false; - - this.translations = this.getDefaultTranslations(); - - this.initialize(); - } - - initialize() { - if (!UserStore.getCurrentId()) { - return; - } - - this.setMaxListeners(0); - - if (window.WebSocket && !conn) { - var protocol = 'ws://'; - if (window.location.protocol === 'https:') { - protocol = 'wss://'; - } - - var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket'; - - if (this.failCount === 0) { - console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console - } - - conn = new WebSocket(connUrl); - - conn.onopen = () => { - if (this.failCount > 0) { - console.log('websocket re-established connection'); //eslint-disable-line no-console - AsyncClient.getChannels(); - AsyncClient.getPosts(ChannelStore.getCurrentId()); - } - - if (this.isInitialize) { - ErrorStore.clearLastError(); - ErrorStore.emitChange(); - } - - this.isInitialize = true; - this.failCount = 0; - }; - - conn.onclose = () => { - conn = null; - - if (this.failCount === 0) { - console.log('websocket closed'); //eslint-disable-line no-console - } - - this.failCount = this.failCount + 1; - - if (this.failCount > 7) { - ErrorStore.storeLastError({message: this.translations.socketError}); - } - - ErrorStore.setConnectionErrorCount(this.failCount); - ErrorStore.emitChange(); - - setTimeout( - () => { - this.initialize(); - }, - 3000 - ); - }; - - conn.onerror = (evt) => { - if (this.failCount <= 1) { - console.log('websocket error'); //eslint-disable-line no-console - console.log(evt); //eslint-disable-line no-console - } - }; - - conn.onmessage = (evt) => { - const msg = JSON.parse(evt.data); - this.handleMessage(msg); - this.emitChange(msg); - }; - } - } - - emitChange(msg) { - this.emit(CHANGE_EVENT, msg); - } - - addChangeListener(callback) { - this.on(CHANGE_EVENT, callback); - } - - removeChangeListener(callback) { - this.removeListener(CHANGE_EVENT, callback); - } - - handleMessage(msg) { - switch (msg.action) { - case SocketEvents.POSTED: - case SocketEvents.EPHEMERAL_MESSAGE: - handleNewPostEvent(msg, this.translations); - break; - - case SocketEvents.POST_EDITED: - handlePostEditEvent(msg); - break; - - case SocketEvents.POST_DELETED: - handlePostDeleteEvent(msg); - break; - - case SocketEvents.NEW_USER: - handleNewUserEvent(); - break; - - case SocketEvents.USER_ADDED: - handleUserAddedEvent(msg); - break; - - case SocketEvents.USER_REMOVED: - handleUserRemovedEvent(msg); - break; - - case SocketEvents.CHANNEL_VIEWED: - handleChannelViewedEvent(msg); - break; - - case SocketEvents.PREFERENCE_CHANGED: - handlePreferenceChangedEvent(msg); - break; - - default: - } - } - - sendMessage(msg) { - if (conn && conn.readyState === WebSocket.OPEN) { - conn.send(JSON.stringify(msg)); - } else if (!conn || conn.readyState === WebSocket.Closed) { - conn = null; - this.initialize(); - } - } - - setTranslations(messages) { - this.translations = messages; - } - - getDefaultTranslations() { - return ({ - socketError: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.', - someone: 'Someone', - posted: 'Posted', - uploadedImage: ' uploaded an image', - uploadedFile: ' uploaded a file', - something: ' did something new', - wrote: ' wrote: ' - }); - } - - close() { - if (conn && conn.readyState === WebSocket.OPEN) { - conn.close(); - } - } -} - -function handleNewPostEvent(msg, translations) { - // Store post - const post = JSON.parse(msg.props.post); - GlobalActions.emitPostRecievedEvent(post); - - // Update channel state - if (ChannelStore.getCurrentId() === msg.channel_id) { - if (window.isActive) { - AsyncClient.updateLastViewedAt(); - } else { - AsyncClient.getChannel(msg.channel_id); - } - } else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) { - AsyncClient.getChannel(msg.channel_id); - } - - // Send desktop notification - if ((UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') && !Utils.isSystemMessage(post)) { - const msgProps = msg.props; - - let mentions = []; - if (msgProps.mentions) { - mentions = JSON.parse(msg.props.mentions); - } - - const channel = ChannelStore.get(msg.channel_id); - const user = UserStore.getCurrentUser(); - const member = ChannelStore.getMember(msg.channel_id); - - let notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default'; - if (notifyLevel === 'default') { - notifyLevel = user.notify_props.desktop; - } - - if (notifyLevel === 'none') { - return; - } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== Constants.DM_CHANNEL) { - return; - } - - let username = translations.someone; - if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { - username = post.props.override_username; - } else if (UserStore.hasProfile(msg.user_id)) { - username = UserStore.getProfile(msg.user_id).username; - } - - let title = translations.posted; - if (channel) { - title = channel.display_name; - } - - let notifyText = post.message.replace(/\n+/g, ' '); - if (notifyText.length > 50) { - notifyText = notifyText.substring(0, 49) + '...'; - } - - if (notifyText.length === 0) { - if (msgProps.image) { - Utils.notifyMe(title, username + translations.uploadedImage, channel); - } else if (msgProps.otherFile) { - Utils.notifyMe(title, username + translations.uploadedFile, channel); - } else { - Utils.notifyMe(title, username + translations.something, channel); - } - } else { - Utils.notifyMe(title, username + translations.wrote + notifyText, channel); - } - if (!user.notify_props || user.notify_props.desktop_sound === 'true') { - Utils.ding(); - } - } -} - -function handlePostEditEvent(msg) { - // Store post - const post = JSON.parse(msg.props.post); - PostStore.storePost(post); - PostStore.emitChange(); - - // Update channel state - if (ChannelStore.getCurrentId() === msg.channel_id) { - if (window.isActive) { - AsyncClient.updateLastViewedAt(); - } - } -} - -function handlePostDeleteEvent(msg) { - const post = JSON.parse(msg.props.post); - GlobalActions.emitPostDeletedEvent(post); -} - -function handleNewUserEvent() { - AsyncClient.getProfiles(); - AsyncClient.getChannelExtraInfo(); -} - -function handleUserAddedEvent(msg) { - if (ChannelStore.getCurrentId() === msg.channel_id) { - AsyncClient.getChannelExtraInfo(); - } - - if (UserStore.getCurrentId() === msg.user_id) { - AsyncClient.getChannel(msg.channel_id); - } -} - -function handleUserRemovedEvent(msg) { - if (UserStore.getCurrentId() === msg.user_id) { - AsyncClient.getChannels(); - - if (msg.props.remover_id !== msg.user_id && - msg.channel_id === ChannelStore.getCurrentId() && - $('#removed_from_channel').length > 0) { - var sentState = {}; - sentState.channelName = ChannelStore.getCurrent().display_name; - sentState.remover = UserStore.getProfile(msg.props.remover_id).username; - - BrowserStore.setItem('channel-removed-state', sentState); - $('#removed_from_channel').modal('show'); - } - } else if (ChannelStore.getCurrentId() === msg.channel_id) { - AsyncClient.getChannelExtraInfo(); - } -} - -function handleChannelViewedEvent(msg) { - // Useful for when multiple devices have the app open to different channels - if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) { - AsyncClient.getChannel(msg.channel_id); - } -} - -function handlePreferenceChangedEvent(msg) { - const preference = JSON.parse(msg.props.preference); - GlobalActions.emitPreferenceChangedEvent(preference); -} - -var SocketStore = new SocketStoreClass(); - -export default SocketStore; -window.SocketStore = SocketStore; diff --git a/webapp/stores/user_typing_store.jsx b/webapp/stores/user_typing_store.jsx new file mode 100644 index 000000000..ab0a9af1d --- /dev/null +++ b/webapp/stores/user_typing_store.jsx @@ -0,0 +1,108 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import UserStore from 'stores/user_store.jsx'; +import EventEmitter from 'events'; +import * as Utils from 'utils/utils.jsx'; + +import Constants from 'utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; + +class UserTypingStoreClass extends EventEmitter { + constructor() { + super(); + + // All typeing users by channel + // this.typingUsers.[channelId+postParentId].user if present then user us typing + // Value is timeout to remove user + this.typingUsers = {}; + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + usernameFromId(userId) { + let username = Utils.localizeMessage('msg_typing.someone', 'Someone'); + if (UserStore.hasProfile(userId)) { + username = UserStore.getProfile(userId).username; + } + return username; + } + + userTyping(channelId, userId, postParentId) { + const username = this.usernameFromId(userId); + + // Key representing a location where users can type + const loc = channelId + postParentId; + + // Create entry + if (!this.typingUsers[loc]) { + this.typingUsers[loc] = {}; + } + + // If we already have this user, clear it's timeout to be deleted + if (this.typingUsers[loc][username]) { + clearTimeout(this.typingUsers[loc][username].timeout); + } + + // Set the user and a timeout to remove it + this.typingUsers[loc][username] = setTimeout(() => { + delete this.typingUsers[loc][username]; + if (this.typingUsers[loc] === {}) { + delete this.typingUsers[loc]; + } + this.emitChange(); + }, Constants.UPDATE_TYPING_MS); + this.emitChange(); + } + + getUsersTyping(channelId, postParentId) { + // Key representing a location where users can type + const loc = channelId + postParentId; + + return this.typingUsers[loc]; + } + + userPosted(userId, channelId, postParentId) { + const username = this.usernameFromId(userId); + const loc = channelId + postParentId; + + if (this.typingUsers[loc]) { + clearTimeout(this.typingUsers[loc][username]); + delete this.typingUsers[loc][username]; + if (this.typingUsers[loc] === {}) { + delete this.typingUsers[loc]; + } + this.emitChange(); + } + } +} + +var UserTypingStore = new UserTypingStoreClass(); + +UserTypingStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_POST: + UserTypingStore.userPosted(action.post.user_id, action.post.channel_id, action.post.parent_id); + break; + case ActionTypes.USER_TYPING: + UserTypingStore.userTyping(action.channelId, action.userId, action.postParentId); + break; + } +}); + +export default UserTypingStore; diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index d3f91bb0e..6140fd9e0 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -673,9 +673,9 @@ export function getStatuses() { const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); const teammateIds = []; - for (const preference of preferences) { - if (preference.value === 'true') { - teammateIds.push(preference.name); + for (const [name, value] of preferences) { + if (value === 'true') { + teammateIds.push(name); } } @@ -756,6 +756,17 @@ export function getAllPreferences() { ); } +export function savePreference(category, name, value, success, error) { + const preference = { + user_id: UserStore.getCurrentId(), + category, + name, + value + }; + + savePreferences([preference], success, error); +} + export function savePreferences(preferences, success, error) { client.savePreferences( preferences, diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 872bdb8ab..859348c73 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -83,6 +83,8 @@ export default { SHOW_SEARCH: null, + USER_TYPING: null, + TOGGLE_IMPORT_THEME_MODAL: null, TOGGLE_INVITE_MEMBER_MODAL: null, TOGGLE_DELETE_POST_MODAL: null, diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 2e9f2b773..1379455ca 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -314,7 +314,13 @@ export function getTimestamp() { // extracts links not styled by Markdown export function extractLinks(text) { - const links = []; + text; // eslint-disable-line no-unused-expressions + Autolinker; // eslint-disable-line no-unused-expressions + + // skip this operation because autolinker is having issues + return []; + + /*const links = []; let inText = text; // strip out code blocks @@ -348,7 +354,7 @@ export function extractLinks(text) { } ); - return links; + return links;*/ } export function escapeRegExp(string) { |