From 12896bd23eeba79884245c1c29fdc568cf21a7fa Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Mon, 14 Mar 2016 08:50:46 -0400 Subject: Converting to Webpack. Stage 1. --- webapp/stores/admin_store.jsx | 162 ++++++++++ webapp/stores/analytics_store.jsx | 85 +++++ webapp/stores/browser_store.jsx | 225 +++++++++++++ webapp/stores/channel_store.jsx | 352 ++++++++++++++++++++ webapp/stores/error_store.jsx | 80 +++++ webapp/stores/file_store.jsx | 60 ++++ webapp/stores/localization_store.jsx | 60 ++++ webapp/stores/modal_store.jsx | 47 +++ webapp/stores/post_store.jsx | 610 +++++++++++++++++++++++++++++++++++ webapp/stores/preference_store.jsx | 178 ++++++++++ webapp/stores/search_store.jsx | 137 ++++++++ webapp/stores/socket_store.jsx | 343 ++++++++++++++++++++ webapp/stores/suggestion_store.jsx | 261 +++++++++++++++ webapp/stores/team_store.jsx | 142 ++++++++ webapp/stores/user_store.jsx | 305 ++++++++++++++++++ 15 files changed, 3047 insertions(+) create mode 100644 webapp/stores/admin_store.jsx create mode 100644 webapp/stores/analytics_store.jsx create mode 100644 webapp/stores/browser_store.jsx create mode 100644 webapp/stores/channel_store.jsx create mode 100644 webapp/stores/error_store.jsx create mode 100644 webapp/stores/file_store.jsx create mode 100644 webapp/stores/localization_store.jsx create mode 100644 webapp/stores/modal_store.jsx create mode 100644 webapp/stores/post_store.jsx create mode 100644 webapp/stores/preference_store.jsx create mode 100644 webapp/stores/search_store.jsx create mode 100644 webapp/stores/socket_store.jsx create mode 100644 webapp/stores/suggestion_store.jsx create mode 100644 webapp/stores/team_store.jsx create mode 100644 webapp/stores/user_store.jsx (limited to 'webapp/stores') diff --git a/webapp/stores/admin_store.jsx b/webapp/stores/admin_store.jsx new file mode 100644 index 000000000..0a4c8c442 --- /dev/null +++ b/webapp/stores/admin_store.jsx @@ -0,0 +1,162 @@ +// 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 BrowserStore from 'stores/browser_store.jsx'; + +import Constants from 'utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; + +const LOG_CHANGE_EVENT = 'log_change'; +const SERVER_AUDIT_CHANGE_EVENT = 'server_audit_change'; +const CONFIG_CHANGE_EVENT = 'config_change'; +const ALL_TEAMS_EVENT = 'all_team_change'; + +class AdminStoreClass extends EventEmitter { + constructor() { + super(); + + this.logs = null; + this.audits = null; + this.config = null; + this.teams = null; + + this.emitLogChange = this.emitLogChange.bind(this); + this.addLogChangeListener = this.addLogChangeListener.bind(this); + this.removeLogChangeListener = this.removeLogChangeListener.bind(this); + + this.emitAuditChange = this.emitAuditChange.bind(this); + this.addAuditChangeListener = this.addAuditChangeListener.bind(this); + this.removeAuditChangeListener = this.removeAuditChangeListener.bind(this); + + this.emitConfigChange = this.emitConfigChange.bind(this); + this.addConfigChangeListener = this.addConfigChangeListener.bind(this); + this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this); + + this.emitAllTeamsChange = this.emitAllTeamsChange.bind(this); + this.addAllTeamsChangeListener = this.addAllTeamsChangeListener.bind(this); + this.removeAllTeamsChangeListener = this.removeAllTeamsChangeListener.bind(this); + } + + emitLogChange() { + this.emit(LOG_CHANGE_EVENT); + } + + addLogChangeListener(callback) { + this.on(LOG_CHANGE_EVENT, callback); + } + + removeLogChangeListener(callback) { + this.removeListener(LOG_CHANGE_EVENT, callback); + } + + emitAuditChange() { + this.emit(SERVER_AUDIT_CHANGE_EVENT); + } + + addAuditChangeListener(callback) { + this.on(SERVER_AUDIT_CHANGE_EVENT, callback); + } + + removeAuditChangeListener(callback) { + this.removeListener(SERVER_AUDIT_CHANGE_EVENT, callback); + } + + emitConfigChange() { + this.emit(CONFIG_CHANGE_EVENT); + } + + addConfigChangeListener(callback) { + this.on(CONFIG_CHANGE_EVENT, callback); + } + + removeConfigChangeListener(callback) { + this.removeListener(CONFIG_CHANGE_EVENT, callback); + } + + emitAllTeamsChange() { + this.emit(ALL_TEAMS_EVENT); + } + + addAllTeamsChangeListener(callback) { + this.on(ALL_TEAMS_EVENT, callback); + } + + removeAllTeamsChangeListener(callback) { + this.removeListener(ALL_TEAMS_EVENT, callback); + } + + getLogs() { + return this.logs; + } + + saveLogs(logs) { + this.logs = logs; + } + + getAudits() { + return this.audits; + } + + saveAudits(audits) { + this.audits = audits; + } + + getConfig() { + return this.config; + } + + saveConfig(config) { + this.config = config; + } + + getAllTeams() { + return this.teams; + } + + saveAllTeams(teams) { + this.teams = teams; + } + + getSelectedTeams() { + const result = BrowserStore.getItem('seleted_teams'); + if (!result) { + return {}; + } + return result; + } + + saveSelectedTeams(teams) { + BrowserStore.setItem('seleted_teams', teams); + } +} + +var AdminStore = new AdminStoreClass(); + +AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_LOGS: + AdminStore.saveLogs(action.logs); + AdminStore.emitLogChange(); + break; + case ActionTypes.RECEIVED_SERVER_AUDITS: + AdminStore.saveAudits(action.audits); + AdminStore.emitAuditChange(); + break; + case ActionTypes.RECEIVED_CONFIG: + AdminStore.saveConfig(action.config); + AdminStore.emitConfigChange(); + break; + case ActionTypes.RECEIVED_ALL_TEAMS: + AdminStore.saveAllTeams(action.teams); + AdminStore.emitAllTeamsChange(); + break; + default: + } +}); + +export default AdminStore; diff --git a/webapp/stores/analytics_store.jsx b/webapp/stores/analytics_store.jsx new file mode 100644 index 000000000..565622c76 --- /dev/null +++ b/webapp/stores/analytics_store.jsx @@ -0,0 +1,85 @@ +// 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'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; + +class AnalyticsStoreClass extends EventEmitter { + constructor() { + super(); + this.systemStats = {}; + this.teamStats = {}; + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + getAllSystem() { + return JSON.parse(JSON.stringify(this.systemStats)); + } + + getAllTeam(id) { + if (id in this.teamStats) { + return JSON.parse(JSON.stringify(this.teamStats[id])); + } + + return {}; + } + + storeSystemStats(newStats) { + for (const stat in newStats) { + if (!newStats.hasOwnProperty(stat)) { + continue; + } + this.systemStats[stat] = newStats[stat]; + } + } + + storeTeamStats(id, newStats) { + if (!(id in this.teamStats)) { + this.teamStats[id] = {}; + } + + for (const stat in newStats) { + if (!newStats.hasOwnProperty(stat)) { + continue; + } + this.teamStats[id][stat] = newStats[stat]; + } + } + +} + +var AnalyticsStore = new AnalyticsStoreClass(); + +AnalyticsStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_ANALYTICS: + if (action.teamId == null) { + AnalyticsStore.storeSystemStats(action.stats); + } else { + AnalyticsStore.storeTeamStats(action.teamId, action.stats); + } + AnalyticsStore.emitChange(); + break; + default: + } +}); + +export default AnalyticsStore; diff --git a/webapp/stores/browser_store.jsx b/webapp/stores/browser_store.jsx new file mode 100644 index 000000000..66190f6a2 --- /dev/null +++ b/webapp/stores/browser_store.jsx @@ -0,0 +1,225 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {generateId} from 'utils/utils.jsx'; + +function getPrefix() { + if (global.window.mm_current_user_id) { + return global.window.mm_current_user_id + '_'; + } + + return 'unknown_'; +} + +class BrowserStoreClass { + constructor() { + this.getItem = this.getItem.bind(this); + this.setItem = this.setItem.bind(this); + this.removeItem = this.removeItem.bind(this); + this.setGlobalItem = this.setGlobalItem.bind(this); + this.getGlobalItem = this.getGlobalItem.bind(this); + this.removeGlobalItem = this.removeGlobalItem.bind(this); + this.actionOnItemsWithPrefix = this.actionOnItemsWithPrefix.bind(this); + this.actionOnGlobalItemsWithPrefix = this.actionOnGlobalItemsWithPrefix.bind(this); + this.isLocalStorageSupported = this.isLocalStorageSupported.bind(this); + this.getLastServerVersion = this.getLastServerVersion.bind(this); + this.setLastServerVersion = this.setLastServerVersion.bind(this); + this.clear = this.clear.bind(this); + this.clearAll = this.clearAll.bind(this); + this.checkedLocalStorageSupported = ''; + this.signalLogout = this.signalLogout.bind(this); + this.isSignallingLogout = this.isSignallingLogout.bind(this); + this.signalLogin = this.signalLogin.bind(this); + this.isSignallingLogin = this.isSignallingLogin.bind(this); + } + + checkVersion() { + var currentVersion = sessionStorage.getItem('storage_version'); + if (currentVersion !== global.window.mm_config.Version) { + sessionStorage.clear(); + try { + sessionStorage.setItem('storage_version', global.window.mm_config.Version); + } catch (e) { + // Do nothing + } + } + } + + getItem(name, defaultValue) { + var result = null; + try { + result = JSON.parse(sessionStorage.getItem(getPrefix() + name)); + } catch (err) { + result = null; + } + + if (result === null && typeof defaultValue !== 'undefined') { + result = defaultValue; + } + + return result; + } + + setItem(name, value) { + sessionStorage.setItem(getPrefix() + name, JSON.stringify(value)); + } + + removeItem(name) { + sessionStorage.removeItem(getPrefix() + name); + } + + setGlobalItem(name, value) { + try { + if (this.isLocalStorageSupported()) { + localStorage.setItem(getPrefix() + name, JSON.stringify(value)); + } else { + sessionStorage.setItem(getPrefix() + name, JSON.stringify(value)); + } + } catch (err) { + console.log('An error occurred while setting local storage, clearing all props'); //eslint-disable-line no-console + localStorage.clear(); + sessionStorage.clear(); + window.location.reload(true); + } + } + + getGlobalItem(name, defaultValue) { + var result = null; + try { + if (this.isLocalStorageSupported()) { + result = JSON.parse(localStorage.getItem(getPrefix() + name)); + } else { + result = JSON.parse(sessionStorage.getItem(getPrefix() + name)); + } + } catch (err) { + result = null; + } + + if (result === null && typeof defaultValue !== 'undefined') { + result = defaultValue; + } + + return result; + } + + removeGlobalItem(name) { + if (this.isLocalStorageSupported()) { + localStorage.removeItem(getPrefix() + name); + } else { + sessionStorage.removeItem(getPrefix() + name); + } + } + + getLastServerVersion() { + return sessionStorage.getItem('last_server_version'); + } + + setLastServerVersion(version) { + sessionStorage.setItem('last_server_version', version); + } + + signalLogout() { + if (this.isLocalStorageSupported()) { + // PLT-1285 store an identifier in session storage so we can catch if the logout came from this tab on IE11 + const logoutId = generateId(); + + sessionStorage.setItem('__logout__', logoutId); + localStorage.setItem('__logout__', logoutId); + localStorage.removeItem('__logout__'); + } + } + + isSignallingLogout(logoutId) { + return logoutId === sessionStorage.getItem('__logout__'); + } + + signalLogin() { + if (this.isLocalStorageSupported()) { + // PLT-1285 store an identifier in session storage so we can catch if the logout came from this tab on IE11 + const loginId = generateId(); + + sessionStorage.setItem('__login__', loginId); + localStorage.setItem('__login__', loginId); + localStorage.removeItem('__login__'); + } + } + + isSignallingLogin(loginId) { + return loginId === sessionStorage.getItem('__login__'); + } + + /** + * Preforms the given action on each item that has the given prefix + * Signature for action is action(key, value) + */ + actionOnGlobalItemsWithPrefix(prefix, action) { + var globalPrefix = getPrefix(); + var globalPrefixiLen = globalPrefix.length; + + var storage = sessionStorage; + if (this.isLocalStorageSupported()) { + storage = localStorage; + } + + for (var key in storage) { + if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) { + var userkey = key.substring(globalPrefixiLen); + action(userkey, this.getGlobalItem(key)); + } + } + } + + actionOnItemsWithPrefix(prefix, action) { + var globalPrefix = getPrefix(); + var globalPrefixiLen = globalPrefix.length; + for (var key in sessionStorage) { + if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) { + var userkey = key.substring(globalPrefixiLen); + action(userkey, this.getGlobalItem(key)); + } + } + } + + clear() { + // don't clear the logout id so IE11 can tell which tab sent a logout request + const logoutId = sessionStorage.getItem('__logout__'); + + sessionStorage.clear(); + + if (logoutId) { + sessionStorage.setItem('__logout__', logoutId); + } + } + + clearAll() { + sessionStorage.clear(); + localStorage.clear(); + } + + isLocalStorageSupported() { + if (this.checkedLocalStorageSupported !== '') { + return this.checkedLocalStorageSupported; + } + + try { + sessionStorage.setItem('__testSession__', '1'); + sessionStorage.removeItem('__testSession__'); + + localStorage.setItem('__testLocal__', '1'); + if (localStorage.getItem('__testLocal__') !== '1') { + this.checkedLocalStorageSupported = false; + } + localStorage.removeItem('__testLocal__', '1'); + + this.checkedLocalStorageSupported = true; + } catch (e) { + this.checkedLocalStorageSupported = false; + } + + return this.checkedLocalStorageSupported; + } +} + +var BrowserStore = new BrowserStoreClass(); +export default BrowserStore; +window.BrowserStore = BrowserStore; diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx new file mode 100644 index 000000000..b2946e326 --- /dev/null +++ b/webapp/stores/channel_store.jsx @@ -0,0 +1,352 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; + +var Utils; +import Constants from 'utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; +const NotificationPrefs = Constants.NotificationPrefs; + +const CHANGE_EVENT = 'change'; +const LEAVE_EVENT = 'leave'; +const MORE_CHANGE_EVENT = 'change'; +const EXTRA_INFO_EVENT = 'extra_info'; + +class ChannelStoreClass extends EventEmitter { + constructor(props) { + super(props); + + this.setMaxListeners(15); + + this.emitChange = this.emitChange.bind(this); + this.addChangeListener = this.addChangeListener.bind(this); + this.removeChangeListener = this.removeChangeListener.bind(this); + this.emitMoreChange = this.emitMoreChange.bind(this); + this.addMoreChangeListener = this.addMoreChangeListener.bind(this); + this.removeMoreChangeListener = this.removeMoreChangeListener.bind(this); + this.emitExtraInfoChange = this.emitExtraInfoChange.bind(this); + this.addExtraInfoChangeListener = this.addExtraInfoChangeListener.bind(this); + this.removeExtraInfoChangeListener = this.removeExtraInfoChangeListener.bind(this); + this.emitLeave = this.emitLeave.bind(this); + this.addLeaveListener = this.addLeaveListener.bind(this); + this.removeLeaveListener = this.removeLeaveListener.bind(this); + this.findFirstBy = this.findFirstBy.bind(this); + this.get = this.get.bind(this); + this.getMember = this.getMember.bind(this); + this.getByName = this.getByName.bind(this); + this.setPostMode = this.setPostMode.bind(this); + this.getPostMode = this.getPostMode.bind(this); + this.setUnreadCount = this.setUnreadCount.bind(this); + this.setUnreadCounts = this.setUnreadCounts.bind(this); + this.getUnreadCount = this.getUnreadCount.bind(this); + this.getUnreadCounts = this.getUnreadCounts.bind(this); + + this.currentId = null; + this.postMode = this.POST_MODE_CHANNEL; + this.channels = []; + this.channelMembers = {}; + this.moreChannels = {}; + this.moreChannels.loading = true; + this.extraInfos = {}; + this.unreadCounts = {}; + } + get POST_MODE_CHANNEL() { + return 1; + } + get POST_MODE_FOCUS() { + return 2; + } + emitChange() { + this.emit(CHANGE_EVENT); + } + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + emitMoreChange() { + this.emit(MORE_CHANGE_EVENT); + } + addMoreChangeListener(callback) { + this.on(MORE_CHANGE_EVENT, callback); + } + removeMoreChangeListener(callback) { + this.removeListener(MORE_CHANGE_EVENT, callback); + } + emitExtraInfoChange() { + this.emit(EXTRA_INFO_EVENT); + } + addExtraInfoChangeListener(callback) { + this.on(EXTRA_INFO_EVENT, callback); + } + removeExtraInfoChangeListener(callback) { + this.removeListener(EXTRA_INFO_EVENT, callback); + } + emitLeave(id) { + this.emit(LEAVE_EVENT, id); + } + addLeaveListener(callback) { + this.on(LEAVE_EVENT, callback); + } + removeLeaveListener(callback) { + this.removeListener(LEAVE_EVENT, callback); + } + findFirstBy(field, value) { + var channels = this.getChannels(); + for (var i = 0; i < channels.length; i++) { + if (channels[i][field] === value) { + return channels[i]; + } + } + + return null; + } + get(id) { + return this.findFirstBy('id', id); + } + getMember(id) { + return this.getAllMembers()[id]; + } + getByName(name) { + return this.findFirstBy('name', name); + } + getAll() { + return this.getChannels(); + } + getAllMembers() { + return this.getChannelMembers(); + } + getMoreAll() { + return this.getMoreChannels(); + } + setCurrentId(id) { + this.currentId = id; + } + resetCounts(id) { + const cm = this.channelMembers; + for (var cmid in cm) { + if (cm[cmid].channel_id === id) { + var c = this.get(id); + if (c) { + cm[cmid].msg_count = this.get(id).total_msg_count; + cm[cmid].mention_count = 0; + this.setUnreadCount(id); + } + break; + } + } + } + getCurrentId() { + return this.currentId; + } + getCurrent() { + var currentId = this.getCurrentId(); + + if (currentId) { + return this.get(currentId); + } + + return null; + } + getCurrentMember() { + var currentId = this.getCurrentId(); + + if (currentId) { + return this.getAllMembers()[currentId]; + } + + return null; + } + setChannelMember(member) { + var members = this.getChannelMembers(); + members[member.channel_id] = member; + this.storeChannelMembers(members); + this.emitChange(); + } + getCurrentExtraInfo() { + return this.getExtraInfo(this.getCurrentId()); + } + getExtraInfo(channelId) { + var extra = null; + + if (channelId) { + extra = this.getExtraInfos()[channelId]; + } + + if (extra) { + // create a defensive copy + extra = JSON.parse(JSON.stringify(extra)); + } else { + extra = {members: []}; + } + + return extra; + } + pStoreChannel(channel) { + var channels = this.getChannels(); + var found; + + for (var i = 0; i < channels.length; i++) { + if (channels[i].id === channel.id) { + channels[i] = channel; + found = true; + break; + } + } + + if (!found) { + channels.push(channel); + } + + if (!Utils) { + Utils = require('utils/utils.jsx'); //eslint-disable-line global-require + } + + channels.sort(Utils.sortByDisplayName); + this.storeChannels(channels); + } + storeChannels(channels) { + this.channels = channels; + } + getChannels() { + return this.channels; + } + pStoreChannelMember(channelMember) { + var members = this.getChannelMembers(); + members[channelMember.channel_id] = channelMember; + this.storeChannelMembers(members); + } + storeChannelMembers(channelMembers) { + this.channelMembers = channelMembers; + } + getChannelMembers() { + return this.channelMembers; + } + storeMoreChannels(channels) { + this.moreChannels = channels; + } + getMoreChannels() { + return this.moreChannels; + } + storeExtraInfos(extraInfos) { + this.extraInfos = extraInfos; + } + getExtraInfos() { + return this.extraInfos; + } + isDefault(channel) { + return channel.name === Constants.DEFAULT_CHANNEL; + } + + setPostMode(mode) { + this.postMode = mode; + } + + getPostMode() { + return this.postMode; + } + + setUnreadCount(id) { + const ch = this.get(id); + const chMember = this.getMember(id); + + let chMentionCount = chMember.mention_count; + let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount; + + if (ch.type === 'D') { + chMentionCount = chUnreadCount; + chUnreadCount = 0; + } else if (chMember.notify_props && chMember.notify_props.mark_unread === NotificationPrefs.MENTION) { + chUnreadCount = 0; + } + + this.unreadCounts[id] = {msgs: chUnreadCount, mentions: chMentionCount}; + } + + setUnreadCounts() { + const channels = this.getAll(); + channels.forEach((ch) => { + this.setUnreadCount(ch.id); + }); + } + + getUnreadCount(id) { + return this.unreadCounts[id] || {msgs: 0, mentions: 0}; + } + + getUnreadCounts() { + return this.unreadCounts; + } +} + +var ChannelStore = new ChannelStoreClass(); + +ChannelStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + var currentId; + + switch (action.type) { + case ActionTypes.CLICK_CHANNEL: + ChannelStore.setCurrentId(action.id); + ChannelStore.resetCounts(action.id); + ChannelStore.setPostMode(ChannelStore.POST_MODE_CHANNEL); + ChannelStore.emitChange(); + break; + + case ActionTypes.RECEIVED_FOCUSED_POST: { + const post = action.post_list.posts[action.postId]; + ChannelStore.setCurrentId(post.channel_id); + ChannelStore.setPostMode(ChannelStore.POST_MODE_FOCUS); + ChannelStore.emitChange(); + break; + } + + case ActionTypes.RECEIVED_CHANNELS: + ChannelStore.storeChannels(action.channels); + ChannelStore.storeChannelMembers(action.members); + currentId = ChannelStore.getCurrentId(); + if (currentId && window.isActive) { + ChannelStore.resetCounts(currentId); + } + ChannelStore.setUnreadCounts(); + ChannelStore.emitChange(); + break; + + case ActionTypes.RECEIVED_CHANNEL: + ChannelStore.pStoreChannel(action.channel); + if (action.member) { + ChannelStore.pStoreChannelMember(action.member); + } + currentId = ChannelStore.getCurrentId(); + if (currentId && window.isActive) { + ChannelStore.resetCounts(currentId); + } + ChannelStore.setUnreadCount(action.channel.id); + ChannelStore.emitChange(); + break; + + case ActionTypes.RECEIVED_MORE_CHANNELS: + ChannelStore.storeMoreChannels(action.channels); + ChannelStore.emitMoreChange(); + break; + + case ActionTypes.RECEIVED_CHANNEL_EXTRA_INFO: + var extraInfos = ChannelStore.getExtraInfos(); + extraInfos[action.extra_info.id] = action.extra_info; + ChannelStore.storeExtraInfos(extraInfos); + ChannelStore.emitExtraInfoChange(); + break; + + case ActionTypes.LEAVE_CHANNEL: + ChannelStore.emitLeave(action.id); + break; + + default: + break; + } +}); + +export default ChannelStore; diff --git a/webapp/stores/error_store.jsx b/webapp/stores/error_store.jsx new file mode 100644 index 000000000..776375a82 --- /dev/null +++ b/webapp/stores/error_store.jsx @@ -0,0 +1,80 @@ +// Copyright (c) 2015 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'; +const ActionTypes = Constants.ActionTypes; + +import BrowserStore from 'stores/browser_store.jsx'; + +const CHANGE_EVENT = 'change'; + +class ErrorStoreClass extends EventEmitter { + constructor() { + super(); + + this.emitChange = this.emitChange.bind(this); + this.addChangeListener = this.addChangeListener.bind(this); + this.removeChangeListener = this.removeChangeListener.bind(this); + this.getLastError = this.getLastError.bind(this); + this.storeLastError = this.storeLastError.bind(this); + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + getLastError() { + return BrowserStore.getItem('last_error'); + } + + storeLastError(error) { + BrowserStore.setItem('last_error', error); + } + + getConnectionErrorCount() { + var count = BrowserStore.getItem('last_error_conn'); + + if (count == null) { + return 0; + } + + return count; + } + + setConnectionErrorCount(count) { + BrowserStore.setItem('last_error_conn', count); + } + + clearLastError() { + BrowserStore.removeItem('last_error'); + BrowserStore.removeItem('last_error_conn'); + } +} + +var ErrorStore = new ErrorStoreClass(); + +ErrorStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + switch (action.type) { + case ActionTypes.RECEIVED_ERROR: + ErrorStore.storeLastError(action.err); + ErrorStore.emitChange(); + break; + + default: + } +}); + +export default ErrorStore; +window.ErrorStore = ErrorStore; diff --git a/webapp/stores/file_store.jsx b/webapp/stores/file_store.jsx new file mode 100644 index 000000000..2628685cc --- /dev/null +++ b/webapp/stores/file_store.jsx @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import Constants from 'utils/constants.jsx'; +import EventEmitter from 'events'; + +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'changed'; + +class FileStore extends EventEmitter { + constructor() { + super(); + + this.addChangeListener = this.addChangeListener.bind(this); + this.removeChangeListener = this.removeChangeListener.bind(this); + this.emitChange = this.emitChange.bind(this); + + this.handleEventPayload = this.handleEventPayload.bind(this); + this.dispatchToken = AppDispatcher.register(this.handleEventPayload); + + this.fileInfo = new Map(); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + emitChange(filename) { + this.emit(CHANGE_EVENT, filename); + } + + hasInfo(filename) { + return this.fileInfo.has(filename); + } + + getInfo(filename) { + return this.fileInfo.get(filename); + } + + setInfo(filename, info) { + this.fileInfo.set(filename, info); + } + + handleEventPayload(payload) { + const action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_FILE_INFO: + this.setInfo(action.filename, action.info); + this.emitChange(action.filename); + break; + } + } +} + +export default new FileStore(); diff --git a/webapp/stores/localization_store.jsx b/webapp/stores/localization_store.jsx new file mode 100644 index 000000000..64380d650 --- /dev/null +++ b/webapp/stores/localization_store.jsx @@ -0,0 +1,60 @@ +// Copyright (c) 2015 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'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; + +class LocalizationStoreClass extends EventEmitter { + constructor() { + super(); + + this.currentLocale = 'en'; + this.currentTranslations = null; + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + setCurrentLocale(locale, translations) { + this.currentLocale = locale; + this.currentTranslations = translations; + } + + getLocale() { + return this.currentLocale; + } + + getTranslations() { + return this.currentTranslations; + } +} + +var LocalizationStore = new LocalizationStoreClass(); +LocalizationStore.setMaxListeners(0); + +LocalizationStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_LOCALE: + LocalizationStore.setCurrentLocale(action.locale, action.translations); + LocalizationStore.emitChange(); + break; + default: + } +}); + +export default LocalizationStore; diff --git a/webapp/stores/modal_store.jsx b/webapp/stores/modal_store.jsx new file mode 100644 index 000000000..2a7921c40 --- /dev/null +++ b/webapp/stores/modal_store.jsx @@ -0,0 +1,47 @@ +// Copyright (c) 2015 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'; +const ActionTypes = Constants.ActionTypes; + +class ModalStoreClass extends EventEmitter { + constructor() { + super(); + + this.addModalListener = this.addModalListener.bind(this); + this.removeModalListener = this.removeModalListener.bind(this); + + this.handleEventPayload = this.handleEventPayload.bind(this); + this.dispatchToken = AppDispatcher.register(this.handleEventPayload); + } + + addModalListener(action, callback) { + this.on(action, callback); + } + + removeModalListener(action, callback) { + this.removeListener(action, callback); + } + + handleEventPayload(payload) { + // toggle event handlers should accept a boolean show/hide value and can accept a map of arguments + const {type, value, ...args} = payload.action; //eslint-disable-line no-use-before-define + + switch (type) { + case ActionTypes.TOGGLE_IMPORT_THEME_MODAL: + case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL: + case ActionTypes.TOGGLE_DELETE_POST_MODAL: + case ActionTypes.TOGGLE_GET_POST_LINK_MODAL: + case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL: + case ActionTypes.TOGGLE_REGISTER_APP_MODAL: + this.emit(type, value, args); + break; + } + } +} + +const ModalStore = new ModalStoreClass(); +export default ModalStore; diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx new file mode 100644 index 000000000..903085760 --- /dev/null +++ b/webapp/stores/post_store.jsx @@ -0,0 +1,610 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; + +import ChannelStore from 'stores/channel_store.jsx'; +import BrowserStore from 'stores/browser_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import Constants from 'utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; +const FOCUSED_POST_CHANGE = 'focused_post_change'; +const EDIT_POST_EVENT = 'edit_post'; +const POSTS_VIEW_JUMP_EVENT = 'post_list_jump'; +const SELECTED_POST_CHANGE_EVENT = 'selected_post_change'; + +class PostStoreClass extends EventEmitter { + constructor() { + super(); + this.selectedPostId = null; + this.postsInfo = {}; + this.currentFocusedPostId = null; + } + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + emitPostFocused() { + this.emit(FOCUSED_POST_CHANGE); + } + + addPostFocusedListener(callback) { + this.on(FOCUSED_POST_CHANGE, callback); + } + + removePostFocusedListener(callback) { + this.removeListener(FOCUSED_POST_CHANGE, callback); + } + + emitEditPost(post) { + this.emit(EDIT_POST_EVENT, post); + } + + addEditPostListener(callback) { + this.on(EDIT_POST_EVENT, callback); + } + + removeEditPostListner(callback) { + this.removeListener(EDIT_POST_EVENT, callback); + } + + emitPostsViewJump(type, post) { + this.emit(POSTS_VIEW_JUMP_EVENT, type, post); + } + + addPostsViewJumpListener(callback) { + this.on(POSTS_VIEW_JUMP_EVENT, callback); + } + + removePostsViewJumpListener(callback) { + this.removeListener(POSTS_VIEW_JUMP_EVENT, callback); + } + + jumpPostsViewToBottom() { + this.emitPostsViewJump(Constants.PostsViewJumpTypes.BOTTOM, null); + } + + jumpPostsViewToPost(post) { + this.emitPostsViewJump(Constants.PostsViewJumpTypes.POST, post); + } + + jumpPostsViewSidebarOpen() { + this.emitPostsViewJump(Constants.PostsViewJumpTypes.SIDEBAR_OPEN, null); + } + + // All this does is makes sure the postsInfo is not null for the specified channel + makePostsInfo(id) { + if (!this.postsInfo.hasOwnProperty(id)) { + this.postsInfo[id] = {}; + } + } + + getPost(channelId, postId) { + const posts = this.postsInfo[channelId].postList; + let post = null; + + if (posts.posts.hasOwnProperty(postId)) { + post = Object.assign({}, posts.posts[postId]); + } + + return post; + } + + getAllPosts(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return Object.assign({}, this.postsInfo[id].postList); + } + + return null; + } + + getEarliestPost(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[this.postsInfo[id].postList.order.length - 1]]; + } + + return null; + } + + getLatestPost(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[0]]; + } + + return null; + } + + getVisiblePosts(id) { + if (this.postsInfo.hasOwnProperty(id) && this.postsInfo[id].hasOwnProperty('postList')) { + const postList = JSON.parse(JSON.stringify(this.postsInfo[id].postList)); + + // Only limit visibility if we are not focused on a post + if (this.currentFocusedPostId === null) { + postList.order = postList.order.slice(0, this.postsInfo[id].endVisible); + } + + // Add pending posts + if (this.postsInfo[id].hasOwnProperty('pendingPosts')) { + Object.assign(postList.posts, this.postsInfo[id].pendingPosts.posts); + postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order); + } + + return postList; + } + + return null; + } + + getVisibilityAtTop(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return this.postsInfo[id].atTop && this.postsInfo[id].endVisible >= this.postsInfo[id].postList.order.length; + } + + return false; + } + + getVisibilityAtBottom(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return this.postsInfo[id].atBottom; + } + + return false; + } + + // Returns true if posts need to be fetched + requestVisibilityIncrease(id, ammount) { + const endVisible = this.postsInfo[id].endVisible; + const postList = this.postsInfo[id].postList; + if (this.getVisibilityAtTop(id)) { + return false; + } + this.postsInfo[id].endVisible += ammount; + this.emitChange(); + return endVisible + ammount > postList.order.length; + } + + getFocusedPostId() { + return this.currentFocusedPostId; + } + + storePosts(id, newPosts) { + if (isPostListNull(newPosts)) { + return; + } + + const combinedPosts = makePostListNonNull(this.getAllPosts(id)); + + for (const pid in newPosts.posts) { + if (newPosts.posts.hasOwnProperty(pid)) { + const np = newPosts.posts[pid]; + if (np.delete_at === 0) { + combinedPosts.posts[pid] = np; + if (combinedPosts.order.indexOf(pid) === -1 && newPosts.order.indexOf(pid) !== -1) { + combinedPosts.order.push(pid); + } + } + } + } + + combinedPosts.order.sort((a, b) => { + if (combinedPosts.posts[a].create_at > combinedPosts.posts[b].create_at) { + return -1; + } + if (combinedPosts.posts[a].create_at < combinedPosts.posts[b].create_at) { + return 1; + } + + return 0; + }); + + this.makePostsInfo(id); + this.postsInfo[id].postList = combinedPosts; + } + + storePost(post) { + const postList = makePostListNonNull(this.getAllPosts(post.channel_id)); + + if (post.pending_post_id !== '') { + this.removePendingPost(post.channel_id, post.pending_post_id); + } + + post.pending_post_id = ''; + + postList.posts[post.id] = post; + if (postList.order.indexOf(post.id) === -1) { + postList.order.unshift(post.id); + } + + this.makePostsInfo(post.channel_id); + this.postsInfo[post.channel_id].postList = postList; + } + + storeFocusedPost(postId, postList) { + const focusedPost = postList.posts[postId]; + if (!focusedPost) { + return; + } + this.currentFocusedPostId = postId; + this.storePosts(postId, postList); + } + + checkBounds(id, numRequested, postList, before) { + if (numRequested > postList.order.length) { + if (before) { + this.postsInfo[id].atTop = true; + } else { + this.postsInfo[id].atBottom = true; + } + } + } + + clearFocusedPost() { + if (this.currentFocusedPostId != null) { + Reflect.deleteProperty(this.postsInfo, this.currentFocusedPostId); + this.currentFocusedPostId = null; + } + } + + clearChannelVisibility(id, atBottom) { + this.makePostsInfo(id); + this.postsInfo[id].endVisible = Constants.POST_CHUNK_SIZE; + this.postsInfo[id].atTop = false; + this.postsInfo[id].atBottom = atBottom; + } + + deletePost(post) { + const postInfo = this.postsInfo[post.channel_id]; + if (!postInfo) { + // the post that has been deleted is in a channel that we haven't seen so just ignore it + return; + } + + const postList = this.postsInfo[post.channel_id].postList; + + if (isPostListNull(postList)) { + return; + } + + if (post.id in postList.posts) { + // make sure to copy the post so that component state changes work properly + postList.posts[post.id] = Object.assign({}, post, { + state: Constants.POST_DELETED, + filenames: [] + }); + } + } + + removePost(post) { + const channelId = post.channel_id; + this.makePostsInfo(channelId); + const postList = this.postsInfo[channelId].postList; + if (isPostListNull(postList)) { + return; + } + + if (post.id in postList.posts) { + Reflect.deleteProperty(postList.posts, post.id); + } + + const index = postList.order.indexOf(post.id); + if (index !== -1) { + postList.order.splice(index, 1); + } + + for (const pid in postList.posts) { + if (!postList.posts.hasOwnProperty(pid)) { + continue; + } + + if (postList.posts[pid].root_id === post.id) { + Reflect.deleteProperty(postList.posts, pid); + const commentIndex = postList.order.indexOf(pid); + if (commentIndex !== -1) { + postList.order.splice(commentIndex, 1); + } + } + } + + this.postsInfo[channelId].postList = postList; + } + + getPendingPosts(channelId) { + if (this.postsInfo.hasOwnProperty(channelId)) { + return this.postsInfo[channelId].pendingPosts; + } + + return null; + } + + storePendingPost(post) { + const copyPost = JSON.parse(JSON.stringify(post)); + copyPost.state = Constants.POST_LOADING; + + const postList = makePostListNonNull(this.getPendingPosts(copyPost.channel_id)); + + postList.posts[copyPost.pending_post_id] = copyPost; + postList.order.unshift(copyPost.pending_post_id); + + this.makePostsInfo(copyPost.channel_id); + this.postsInfo[copyPost.channel_id].pendingPosts = postList; + this.emitChange(); + } + + removePendingPost(channelId, pendingPostId) { + const postList = makePostListNonNull(this.getPendingPosts(channelId)); + + Reflect.deleteProperty(postList.posts, pendingPostId); + const index = postList.order.indexOf(pendingPostId); + if (index === -1) { + return; + } + + postList.order.splice(index, 1); + + this.postsInfo[channelId].pendingPosts = postList; + this.emitChange(); + } + + clearPendingPosts(channelId) { + if (this.postsInfo.hasOwnProperty(channelId)) { + Reflect.deleteProperty(this.postsInfo[channelId], 'pendingPosts'); + } + } + + updatePendingPost(post) { + const copyPost = JSON.parse(JSON.stringify(post)); + const postList = makePostListNonNull(this.getPendingPosts(copyPost.channel_id)); + + if (postList.order.indexOf(copyPost.pending_post_id) === -1) { + return; + } + + postList.posts[copyPost.pending_post_id] = copyPost; + this.postsInfo[copyPost.channel_id].pendingPosts = postList; + this.emitChange(); + } + + storeSelectedPostId(postId) { + this.selectedPostId = postId; + } + + getSelectedPostId() { + return this.selectedPostId; + } + + getSelectedPost() { + if (this.selectedPostId == null) { + return null; + } + + for (const k in this.postsInfo) { + if (this.postsInfo[k].postList.posts.hasOwnProperty(this.selectedPostId)) { + return this.postsInfo[k].postList.posts[this.selectedPostId]; + } + } + + return null; + } + + getSelectedPostThread() { + if (this.selectedPostId == null) { + return null; + } + + let posts; + let pendingPosts; + for (const k in this.postsInfo) { + if (this.postsInfo[k].postList.posts.hasOwnProperty(this.selectedPostId)) { + posts = this.postsInfo[k].postList.posts; + if (this.postsInfo[k].pendingPosts != null) { + pendingPosts = this.postsInfo[k].pendingPosts.posts; + } + } + } + + const threadPosts = {}; + const rootId = this.selectedPostId; + for (const k in posts) { + if (posts[k].root_id === rootId) { + threadPosts[k] = JSON.parse(JSON.stringify(posts[k])); + } + } + + for (const k in pendingPosts) { + if (pendingPosts[k].root_id === rootId) { + threadPosts[k] = JSON.parse(JSON.stringify(pendingPosts[k])); + } + } + + return threadPosts; + } + + emitSelectedPostChange(fromSearch) { + this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch); + } + + addSelectedPostChangeListener(callback) { + this.on(SELECTED_POST_CHANGE_EVENT, callback); + } + + removeSelectedPostChangeListener(callback) { + this.removeListener(SELECTED_POST_CHANGE_EVENT, callback); + } + + getCurrentUsersLatestPost(channelId, rootId) { + const userId = UserStore.getCurrentId(); + var postList = makePostListNonNull(this.getAllPosts(channelId)); + var i = 0; + var len = postList.order.length; + var lastPost = null; + + for (i; i < len; i++) { + const post = postList.posts[postList.order[i]]; + if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) { + if (rootId) { + if (post.root_id === rootId || post.id === rootId) { + lastPost = post; + break; + } + } else { + lastPost = post; + break; + } + } + } + + return lastPost; + } + + getEmptyDraft() { + return {message: '', uploadsInProgress: [], previews: []}; + } + storeCurrentDraft(draft) { + var channelId = ChannelStore.getCurrentId(); + BrowserStore.setGlobalItem('draft_' + channelId, draft); + } + getCurrentDraft() { + var channelId = ChannelStore.getCurrentId(); + return this.getDraft(channelId); + } + storeDraft(channelId, draft) { + BrowserStore.setGlobalItem('draft_' + channelId, draft); + } + getDraft(channelId) { + return BrowserStore.getGlobalItem('draft_' + channelId, this.getEmptyDraft()); + } + storeCommentDraft(parentPostId, draft) { + BrowserStore.setGlobalItem('comment_draft_' + parentPostId, draft); + } + getCommentDraft(parentPostId) { + return BrowserStore.getGlobalItem('comment_draft_' + parentPostId, this.getEmptyDraft()); + } + clearDraftUploads() { + BrowserStore.actionOnGlobalItemsWithPrefix('draft_', (key, value) => { + if (value) { + value.uploadsInProgress = []; + BrowserStore.setItem(key, value); + } + }); + } + clearCommentDraftUploads() { + BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', (key, value) => { + if (value) { + value.uploadsInProgress = []; + BrowserStore.setItem(key, value); + } + }); + } + getCommentCount(post) { + const posts = this.getAllPosts(post.channel_id).posts; + + let commentCount = 0; + for (const id in posts) { + if (posts.hasOwnProperty(id)) { + if (posts[id].root_id === post.id) { + commentCount += 1; + } + } + } + + return commentCount; + } +} + +var PostStore = new PostStoreClass(); + +PostStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_POSTS: { + const id = PostStore.currentFocusedPostId == null ? action.id : PostStore.currentFocusedPostId; + PostStore.checkBounds(id, action.numRequested, makePostListNonNull(action.post_list), action.before); + PostStore.storePosts(id, makePostListNonNull(action.post_list)); + PostStore.emitChange(); + break; + } + case ActionTypes.RECEIVED_FOCUSED_POST: + PostStore.clearChannelVisibility(action.postId, false); + PostStore.storeFocusedPost(action.postId, makePostListNonNull(action.post_list)); + PostStore.emitChange(); + break; + case ActionTypes.RECEIVED_POST: + PostStore.storePost(action.post); + PostStore.emitChange(); + break; + case ActionTypes.RECEIVED_EDIT_POST: + PostStore.emitEditPost(action); + PostStore.emitChange(); + break; + case ActionTypes.CLICK_CHANNEL: + PostStore.clearFocusedPost(); + PostStore.clearChannelVisibility(action.id, true); + break; + case ActionTypes.CREATE_POST: + PostStore.storePendingPost(action.post); + PostStore.storeDraft(action.post.channel_id, null); + PostStore.jumpPostsViewToBottom(); + break; + case ActionTypes.POST_DELETED: + PostStore.deletePost(action.post); + PostStore.emitChange(); + break; + case ActionTypes.REMOVE_POST: + PostStore.removePost(action.post); + PostStore.emitChange(); + break; + case ActionTypes.RECEIVED_POST_SELECTED: + PostStore.storeSelectedPostId(action.postId); + PostStore.emitSelectedPostChange(action.from_search); + break; + default: + } +}); + +export default PostStore; + +function makePostListNonNull(pl) { + var postList = pl; + if (postList == null) { + postList = {order: [], posts: {}}; + } + + if (postList.order == null) { + postList.order = []; + } + + if (postList.posts == null) { + postList.posts = {}; + } + + return postList; +} + +function isPostListNull(pl) { + if (pl == null) { + return true; + } + + if (pl.posts == null) { + return true; + } + + if (pl.order == null) { + return true; + } + + return false; +} diff --git a/webapp/stores/preference_store.jsx b/webapp/stores/preference_store.jsx new file mode 100644 index 000000000..df77f0d51 --- /dev/null +++ b/webapp/stores/preference_store.jsx @@ -0,0 +1,178 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +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); + } + + getAllPreferences() { + return new Map(BrowserStore.getItem('preferences', [])); + } + + get(category, name, defaultValue = '') { + const preference = this.getAllPreferences().get(getPreferenceKey(category, name)); + + if (!preference) { + return defaultValue; + } + + return preference.value || defaultValue; + } + + getBool(category, name, defaultValue = false) { + const preference = this.getAllPreferences().get(getPreferenceKey(category, name)); + + if (!preference) { + 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; + } + + getInt(category, name, defaultValue = 0) { + const preference = this.getAllPreferences().get(getPreferenceKey(category, name)); + + if (!preference) { + 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; + } + + getCategory(category) { + return this.getPreferencesWhere((preference) => (preference.category === category)); + } + + getPreferencesWhere(pred) { + const all = this.getAllPreferences(); + const preferences = []; + + for (const [, preference] of all) { + if (pred(preference)) { + preferences.push(preference); + } + } + + 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; + } + + setPreferences(newPreferences) { + const preferences = this.getAllPreferences(); + + for (const preference of newPreferences) { + preferences.set(getPreferenceKeyForModel(preference), preference); + } + + this.setAllPreferences(preferences); + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + handleEventPayload(payload) { + const action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_PREFERENCE: { + const preference = action.preference; + this.setPreference(preference.category, preference.name, preference.value); + this.emitChange(); + break; + } + case ActionTypes.RECEIVED_PREFERENCES: + this.setPreferences(action.preferences); + this.emitChange(); + break; + } + } +} + +const PreferenceStore = new PreferenceStoreClass(); +export default PreferenceStore; +window.PreferenceStore = PreferenceStore; diff --git a/webapp/stores/search_store.jsx b/webapp/stores/search_store.jsx new file mode 100644 index 000000000..c7818a858 --- /dev/null +++ b/webapp/stores/search_store.jsx @@ -0,0 +1,137 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; + +import BrowserStore from 'stores/browser_store.jsx'; + +import Constants from 'utils/constants.jsx'; +var ActionTypes = Constants.ActionTypes; + +var CHANGE_EVENT = 'change'; +var SEARCH_CHANGE_EVENT = 'search_change'; +var SEARCH_TERM_CHANGE_EVENT = 'search_term_change'; +var SHOW_SEARCH_EVENT = 'show_search'; + +class SearchStoreClass extends EventEmitter { + constructor() { + super(); + + this.emitChange = this.emitChange.bind(this); + this.addChangeListener = this.addChangeListener.bind(this); + this.removeChangeListener = this.removeChangeListener.bind(this); + + this.emitSearchChange = this.emitSearchChange.bind(this); + this.addSearchChangeListener = this.addSearchChangeListener.bind(this); + this.removeSearchChangeListener = this.removeSearchChangeListener.bind(this); + + this.emitSearchTermChange = this.emitSearchTermChange.bind(this); + this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this); + this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this); + + this.emitShowSearch = this.emitShowSearch.bind(this); + this.addShowSearchListener = this.addShowSearchListener.bind(this); + this.removeShowSearchListener = this.removeShowSearchListener.bind(this); + + this.getSearchResults = this.getSearchResults.bind(this); + this.getIsMentionSearch = this.getIsMentionSearch.bind(this); + + this.storeSearchTerm = this.storeSearchTerm.bind(this); + this.getSearchTerm = this.getSearchTerm.bind(this); + + this.storeSearchResults = this.storeSearchResults.bind(this); + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + emitSearchChange() { + this.emit(SEARCH_CHANGE_EVENT); + } + + addSearchChangeListener(callback) { + this.on(SEARCH_CHANGE_EVENT, callback); + } + + removeSearchChangeListener(callback) { + this.removeListener(SEARCH_CHANGE_EVENT, callback); + } + + emitSearchTermChange(doSearch, isMentionSearch) { + this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch); + } + + addSearchTermChangeListener(callback) { + this.on(SEARCH_TERM_CHANGE_EVENT, callback); + } + + removeSearchTermChangeListener(callback) { + this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback); + } + + emitShowSearch() { + this.emit(SHOW_SEARCH_EVENT); + } + + addShowSearchListener(callback) { + this.on(SHOW_SEARCH_EVENT, callback); + } + + removeShowSearchListener(callback) { + this.removeListener(SHOW_SEARCH_EVENT, callback); + } + + getSearchResults() { + return BrowserStore.getItem('search_results'); + } + + getIsMentionSearch() { + return BrowserStore.getItem('is_mention_search'); + } + + storeSearchTerm(term) { + BrowserStore.setItem('search_term', term); + } + + getSearchTerm() { + return BrowserStore.getItem('search_term'); + } + + storeSearchResults(results, isMentionSearch) { + BrowserStore.setItem('search_results', results); + BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch)); + } +} + +var SearchStore = new SearchStoreClass(); + +SearchStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_SEARCH: + SearchStore.storeSearchResults(action.results, action.is_mention_search); + SearchStore.emitSearchChange(); + break; + case ActionTypes.RECEIVED_SEARCH_TERM: + SearchStore.storeSearchTerm(action.term); + SearchStore.emitSearchTermChange(action.do_search, action.is_mention_search); + break; + case ActionTypes.SHOW_SEARCH: + SearchStore.emitShowSearch(); + break; + default: + } +}); + +export default SearchStore; diff --git a/webapp/stores/socket_store.jsx b/webapp/stores/socket_store.jsx new file mode 100644 index 000000000..5d6302743 --- /dev/null +++ b/webapp/stores/socket_store.jsx @@ -0,0 +1,343 @@ +// 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/suggestion_store.jsx b/webapp/stores/suggestion_store.jsx new file mode 100644 index 000000000..eeb09fa9e --- /dev/null +++ b/webapp/stores/suggestion_store.jsx @@ -0,0 +1,261 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import Constants from 'utils/constants.jsx'; +import EventEmitter from 'events'; + +const ActionTypes = Constants.ActionTypes; + +const COMPLETE_WORD_EVENT = 'complete_word'; +const PRETEXT_CHANGED_EVENT = 'pretext_changed'; +const SUGGESTIONS_CHANGED_EVENT = 'suggestions_changed'; + +class SuggestionStore extends EventEmitter { + constructor() { + super(); + + this.addSuggestionsChangedListener = this.addSuggestionsChangedListener.bind(this); + this.removeSuggestionsChangedListener = this.removeSuggestionsChangedListener.bind(this); + this.emitSuggestionsChanged = this.emitSuggestionsChanged.bind(this); + + this.addPretextChangedListener = this.addPretextChangedListener.bind(this); + this.removePretextChangedListener = this.removePretextChangedListener.bind(this); + this.emitPretextChanged = this.emitPretextChanged.bind(this); + + this.addCompleteWordListener = this.addCompleteWordListener.bind(this); + this.removeCompleteWordListener = this.removeCompleteWordListener.bind(this); + this.emitCompleteWord = this.emitCompleteWord.bind(this); + + this.handleEventPayload = this.handleEventPayload.bind(this); + this.dispatchToken = AppDispatcher.register(this.handleEventPayload); + + // this.suggestions stores the state of all SuggestionBoxes by mapping their unique identifier to an + // object with the following fields: + // pretext: the text before the cursor + // matchedPretext: the text before the cursor that will be replaced if an autocomplete term is selected + // terms: a list of strings which the previously typed text may be replaced by + // items: a list of objects backing the terms which may be used in rendering + // components: a list of react components that can be used to render their corresponding item + // selection: the term currently selected by the keyboard + this.suggestions = new Map(); + } + + addSuggestionsChangedListener(id, callback) { + this.on(SUGGESTIONS_CHANGED_EVENT + id, callback); + } + removeSuggestionsChangedListener(id, callback) { + this.removeListener(SUGGESTIONS_CHANGED_EVENT + id, callback); + } + emitSuggestionsChanged(id) { + this.emit(SUGGESTIONS_CHANGED_EVENT + id); + } + + addPretextChangedListener(id, callback) { + this.on(PRETEXT_CHANGED_EVENT + id, callback); + } + removePretextChangedListener(id, callback) { + this.removeListener(PRETEXT_CHANGED_EVENT + id, callback); + } + emitPretextChanged(id, pretext) { + this.emit(PRETEXT_CHANGED_EVENT + id, pretext); + } + + addCompleteWordListener(id, callback) { + this.on(COMPLETE_WORD_EVENT + id, callback); + } + removeCompleteWordListener(id, callback) { + this.removeListener(COMPLETE_WORD_EVENT + id, callback); + } + emitCompleteWord(id, term) { + this.emit(COMPLETE_WORD_EVENT + id, term); + } + + registerSuggestionBox(id) { + this.suggestions.set(id, { + pretext: '', + matchedPretext: '', + terms: [], + items: [], + components: [], + selection: '' + }); + } + + unregisterSuggestionBox(id) { + this.suggestions.delete(id); + } + + clearSuggestions(id) { + const suggestion = this.suggestions.get(id); + + suggestion.matchedPretext = ''; + suggestion.terms = []; + suggestion.items = []; + suggestion.components = []; + } + + clearSelection(id) { + const suggestion = this.suggestions.get(id); + + suggestion.selection = ''; + } + + hasSuggestions(id) { + return this.suggestions.get(id).terms.length > 0; + } + + setPretext(id, pretext) { + const suggestion = this.suggestions.get(id); + + suggestion.pretext = pretext; + } + + setMatchedPretext(id, matchedPretext) { + const suggestion = this.suggestions.get(id); + + suggestion.matchedPretext = matchedPretext; + } + + addSuggestion(id, term, item, component) { + const suggestion = this.suggestions.get(id); + + suggestion.terms.push(term); + suggestion.items.push(item); + suggestion.components.push(component); + } + + addSuggestions(id, terms, items, component) { + const suggestion = this.suggestions.get(id); + + suggestion.terms.push(...terms); + suggestion.items.push(...items); + + for (let i = 0; i < terms.length; i++) { + suggestion.components.push(component); + } + } + + // make sure that if suggestions exist, then one of them is selected. return true if the selection changes. + ensureSelectionExists(id) { + const suggestion = this.suggestions.get(id); + + if (suggestion.terms.length > 0) { + // if the current selection is no longer in the map, select the first term in the list + if (!suggestion.selection || suggestion.terms.indexOf(suggestion.selection) === -1) { + suggestion.selection = suggestion.terms[0]; + + return true; + } + } else if (suggestion.selection) { + suggestion.selection = ''; + + return true; + } + + return false; + } + + getPretext(id) { + return this.suggestions.get(id).pretext; + } + + getMatchedPretext(id) { + return this.suggestions.get(id).matchedPretext; + } + + getItems(id) { + return this.suggestions.get(id).items; + } + + getTerms(id) { + return this.suggestions.get(id).terms; + } + + getComponents(id) { + return this.suggestions.get(id).components; + } + + getSelection(id) { + return this.suggestions.get(id).selection; + } + + selectNext(id) { + this.setSelectionByDelta(id, 1); + } + + selectPrevious(id) { + this.setSelectionByDelta(id, -1); + } + + setSelectionByDelta(id, delta) { + const suggestion = this.suggestions.get(id); + + let selectionIndex = suggestion.terms.indexOf(suggestion.selection); + + if (selectionIndex === -1) { + // this should never happen since selection should always be in terms + throw new Error('selection is not in terms'); + } + + selectionIndex += delta; + + if (selectionIndex < 0) { + selectionIndex = 0; + } else if (selectionIndex > suggestion.terms.length - 1) { + selectionIndex = suggestion.terms.length - 1; + } + + suggestion.selection = suggestion.terms[selectionIndex]; + } + + handleEventPayload(payload) { + const {type, id, ...other} = payload.action; // eslint-disable-line no-use-before-define + + switch (type) { + case ActionTypes.SUGGESTION_PRETEXT_CHANGED: + this.clearSuggestions(id); + + this.setPretext(id, other.pretext); + this.emitPretextChanged(id, other.pretext); + + this.ensureSelectionExists(id); + this.emitSuggestionsChanged(id); + break; + case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS: + if (this.getMatchedPretext(id) === '') { + this.setMatchedPretext(id, other.matchedPretext); + + // ensure the matched pretext hasn't changed so that we don't receive suggestions for outdated pretext + this.addSuggestions(id, other.terms, other.items, other.component); + + this.ensureSelectionExists(id); + this.emitSuggestionsChanged(id); + } + break; + case ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS: + this.clearSuggestions(id); + this.clearSelection(id); + this.emitSuggestionsChanged(id); + break; + case ActionTypes.SUGGESTION_SELECT_NEXT: + this.selectNext(id); + this.emitSuggestionsChanged(id); + break; + case ActionTypes.SUGGESTION_SELECT_PREVIOUS: + this.selectPrevious(id); + this.emitSuggestionsChanged(id); + break; + case ActionTypes.SUGGESTION_COMPLETE_WORD: + this.emitCompleteWord(id, other.term || this.getSelection(id), this.getMatchedPretext(id)); + + this.setPretext(id, ''); + this.clearSuggestions(id); + this.clearSelection(id); + this.emitSuggestionsChanged(id); + break; + } + } +} + +export default new SuggestionStore(); diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx new file mode 100644 index 000000000..e1fc9167d --- /dev/null +++ b/webapp/stores/team_store.jsx @@ -0,0 +1,142 @@ +// Copyright (c) 2015 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'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; + +var Utils; +function getWindowLocationOrigin() { + if (!Utils) { + Utils = require('utils/utils.jsx'); //eslint-disable-line global-require + } + return Utils.getWindowLocationOrigin(); +} + +class TeamStoreClass extends EventEmitter { + constructor() { + super(); + + this.emitChange = this.emitChange.bind(this); + this.addChangeListener = this.addChangeListener.bind(this); + this.removeChangeListener = this.removeChangeListener.bind(this); + this.get = this.get.bind(this); + this.getByName = this.getByName.bind(this); + this.getAll = this.getAll.bind(this); + this.getCurrentId = this.getCurrentId.bind(this); + this.getCurrent = this.getCurrent.bind(this); + this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this); + this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this); + this.saveTeam = this.saveTeam.bind(this); + + this.teams = {}; + this.currentTeamId = ''; + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + get(id) { + var c = this.getAll(); + return c[id]; + } + + getByName(name) { + var t = this.getAll(); + + for (var id in t) { + if (t[id].name === name) { + return t[id]; + } + } + + return null; + } + + getAll() { + return this.teams; + } + + getCurrentId() { + var team = this.get(this.currentTeamId); + + if (team) { + return team.id; + } + + return null; + } + + getCurrent() { + const team = this.teams[this.currentTeamId]; + + if (team) { + return team; + } + + return null; + } + + getCurrentTeamUrl() { + if (this.getCurrent()) { + return getWindowLocationOrigin() + '/' + this.getCurrent().name; + } + return null; + } + + getCurrentInviteLink() { + const current = this.getCurrent(); + + if (current) { + return getWindowLocationOrigin() + '/signup_user_complete/?id=' + current.invite_id; + } + + return ''; + } + + saveTeam(team) { + this.teams[team.id] = team; + } + + saveTeams(teams) { + this.teams = teams; + } + + saveMyTeam(team) { + this.saveTeam(team); + this.currentTeamId = team.id; + } +} + +var TeamStore = new TeamStoreClass(); + +TeamStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_MY_TEAM: + TeamStore.saveMyTeam(action.team); + TeamStore.emitChange(); + break; + case ActionTypes.RECEIVED_ALL_TEAMS: + TeamStore.saveTeams(action.teams); + TeamStore.emitChange(); + break; + default: + } +}); + +export default TeamStore; diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx new file mode 100644 index 000000000..98cc2f3f1 --- /dev/null +++ b/webapp/stores/user_store.jsx @@ -0,0 +1,305 @@ +// Copyright (c) 2015 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'; +const ActionTypes = Constants.ActionTypes; +import BrowserStore from './browser_store.jsx'; + +const CHANGE_EVENT = 'change'; +const CHANGE_EVENT_SESSIONS = 'change_sessions'; +const CHANGE_EVENT_AUDITS = 'change_audits'; +const CHANGE_EVENT_STATUSES = 'change_statuses'; + +class UserStoreClass extends EventEmitter { + constructor() { + super(); + this.profileCache = null; + this.currentUserId = ''; + } + + emitChange(userId) { + this.emit(CHANGE_EVENT, userId); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + emitSessionsChange() { + this.emit(CHANGE_EVENT_SESSIONS); + } + + addSessionsChangeListener(callback) { + this.on(CHANGE_EVENT_SESSIONS, callback); + } + + removeSessionsChangeListener(callback) { + this.removeListener(CHANGE_EVENT_SESSIONS, callback); + } + + emitAuditsChange() { + this.emit(CHANGE_EVENT_AUDITS); + } + + addAuditsChangeListener(callback) { + this.on(CHANGE_EVENT_AUDITS, callback); + } + + removeAuditsChangeListener(callback) { + this.removeListener(CHANGE_EVENT_AUDITS, callback); + } + + emitStatusesChange() { + this.emit(CHANGE_EVENT_STATUSES); + } + + addStatusesChangeListener(callback) { + this.on(CHANGE_EVENT_STATUSES, callback); + } + + removeStatusesChangeListener(callback) { + this.removeListener(CHANGE_EVENT_STATUSES, callback); + } + + getCurrentUser() { + return this.getProfiles()[this.currentUserId]; + } + + setCurrentUser(user) { + this.saveProfile(user); + this.currentUserId = user.id; + global.window.mm_current_user_id = this.currentUserId; + } + + getCurrentId() { + var user = this.getCurrentUser(); + + if (user) { + return user.id; + } + + return null; + } + + getLastEmail() { + return BrowserStore.getGlobalItem('last_email', ''); + } + + setLastEmail(email) { + BrowserStore.setGlobalItem('last_email', email); + } + + getLastUsername() { + return BrowserStore.getGlobalItem('last_username', ''); + } + + setLastUsername(username) { + BrowserStore.setGlobalItem('last_username', username); + } + + hasProfile(userId) { + return this.getProfiles()[userId] != null; + } + + getProfile(userId) { + if (userId === this.getCurrentId()) { + return this.getCurrentUser(); + } + + return this.getProfiles()[userId]; + } + + getProfileByUsername(username) { + return this.getProfilesUsernameMap()[username]; + } + + getProfilesUsernameMap() { + var profileUsernameMap = {}; + + var profiles = this.getProfiles(); + for (var key in profiles) { + if (profiles.hasOwnProperty(key)) { + var profile = profiles[key]; + profileUsernameMap[profile.username] = profile; + } + } + + return profileUsernameMap; + } + + getProfiles() { + if (this.profileCache !== null) { + return this.profileCache; + } + + return BrowserStore.getItem('profiles', {}); + } + + getActiveOnlyProfiles(skipCurrent) { + const active = {}; + const profiles = this.getProfiles(); + const currentId = this.getCurrentId(); + + for (var key in profiles) { + if (!(profiles[key].id === currentId && skipCurrent) && profiles[key].delete_at === 0) { + active[key] = profiles[key]; + } + } + + return active; + } + + getActiveOnlyProfileList() { + const profileMap = this.getActiveOnlyProfiles(); + const profiles = []; + const currentId = this.getCurrentId(); + + for (const id in profileMap) { + if (profileMap.hasOwnProperty(id) && id !== currentId) { + profiles.push(profileMap[id]); + } + } + + return profiles; + } + + saveProfile(profile) { + var ps = this.getProfiles(); + ps[profile.id] = profile; + this.profileCache = ps; + BrowserStore.setItem('profiles', ps); + } + + saveProfiles(profiles) { + const currentId = this.getCurrentId(); + if (this.profileCache) { + const currentUser = this.profileCache[currentId]; + if (currentUser) { + if (currentId in profiles) { + delete profiles[currentId]; + } + + this.profileCache = profiles; + this.profileCache[currentId] = currentUser; + } else { + this.profileCache = profiles; + } + } else { + this.profileCache = profiles; + } + + BrowserStore.setItem('profiles', profiles); + } + + setSessions(sessions) { + BrowserStore.setItem('sessions', sessions); + } + + getSessions() { + return BrowserStore.getItem('sessions', {loading: true}); + } + + setAudits(audits) { + BrowserStore.setItem('audits', audits); + } + + getAudits() { + return BrowserStore.getItem('audits', {loading: true}); + } + + getCurrentMentionKeys() { + return this.getMentionKeys(this.getCurrentId()); + } + + getMentionKeys(id) { + var user = this.getProfile(id); + + var keys = []; + + if (!user || !user.notify_props) { + return keys; + } + + if (user.notify_props.mention_keys) { + keys = keys.concat(user.notify_props.mention_keys.split(',')); + } + + if (user.notify_props.first_name === 'true' && user.first_name) { + keys.push(user.first_name); + } + + if (user.notify_props.all === 'true') { + keys.push('@all'); + } + + if (user.notify_props.channel === 'true') { + keys.push('@channel'); + } + + return keys; + } + + setStatuses(statuses) { + this.pSetStatuses(statuses); + this.emitStatusesChange(); + } + + pSetStatuses(statuses) { + BrowserStore.setItem('statuses', statuses); + } + + setStatus(userId, status) { + var statuses = this.getStatuses(); + statuses[userId] = status; + this.pSetStatuses(statuses); + this.emitStatusesChange(); + } + + getStatuses() { + return BrowserStore.getItem('statuses', {}); + } + + getStatus(id) { + return this.getStatuses()[id]; + } +} + +var UserStore = new UserStoreClass(); +UserStore.setMaxListeners(15); + +UserStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_PROFILES: + UserStore.saveProfiles(action.profiles); + UserStore.emitChange(); + break; + case ActionTypes.RECEIVED_ME: + UserStore.setCurrentUser(action.me); + UserStore.emitChange(action.me.id); + break; + case ActionTypes.RECEIVED_SESSIONS: + UserStore.setSessions(action.sessions); + UserStore.emitSessionsChange(); + break; + case ActionTypes.RECEIVED_AUDITS: + UserStore.setAudits(action.audits); + UserStore.emitAuditsChange(); + break; + case ActionTypes.RECEIVED_STATUSES: + UserStore.pSetStatuses(action.statuses); + UserStore.emitStatusesChange(); + break; + default: + } +}); + +export {UserStore as default}; -- cgit v1.2.3-1-g7c22