diff options
Diffstat (limited to 'web/react/stores')
-rw-r--r-- | web/react/stores/admin_store.jsx | 16 | ||||
-rw-r--r-- | web/react/stores/browser_store.jsx | 23 | ||||
-rw-r--r-- | web/react/stores/channel_store.jsx | 164 | ||||
-rw-r--r-- | web/react/stores/error_store.jsx | 12 | ||||
-rw-r--r-- | web/react/stores/modal_store.jsx | 16 | ||||
-rw-r--r-- | web/react/stores/post_store.jsx | 552 | ||||
-rw-r--r-- | web/react/stores/preference_store.jsx | 15 | ||||
-rw-r--r-- | web/react/stores/search_store.jsx | 60 | ||||
-rw-r--r-- | web/react/stores/socket_store.jsx | 68 | ||||
-rw-r--r-- | web/react/stores/suggestion_store.jsx | 259 | ||||
-rw-r--r-- | web/react/stores/team_store.jsx | 23 | ||||
-rw-r--r-- | web/react/stores/user_store.jsx | 27 |
12 files changed, 886 insertions, 349 deletions
diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index cf16d031c..704e2ced4 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -1,17 +1,17 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var EventEmitter = require('events').EventEmitter; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; -var BrowserStore = require('../stores/browser_store.jsx'); +import BrowserStore from '../stores/browser_store.jsx'; -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; -var LOG_CHANGE_EVENT = 'log_change'; -var CONFIG_CHANGE_EVENT = 'config_change'; -var ALL_TEAMS_EVENT = 'all_team_change'; +const LOG_CHANGE_EVENT = 'log_change'; +const CONFIG_CHANGE_EVENT = 'config_change'; +const ALL_TEAMS_EVENT = 'all_team_change'; class AdminStoreClass extends EventEmitter { constructor() { diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 8e86ce32f..ff6ae45ea 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -1,6 +1,8 @@ // 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_user) { return global.window.mm_user.id + '_'; @@ -26,6 +28,7 @@ class BrowserStoreClass { this.clearAll = this.clearAll.bind(this); this.checkedLocalStorageSupported = ''; this.signalLogout = this.signalLogout.bind(this); + this.isSignallingLogout = this.isSignallingLogout.bind(this); var currentVersion = sessionStorage.getItem('storage_version'); if (currentVersion !== global.window.mm_config.Version) { @@ -72,7 +75,7 @@ class BrowserStoreClass { console.log('An error occurred while setting local storage, clearing all props'); //eslint-disable-line no-console localStorage.clear(); sessionStorage.clear(); - window.location.href = window.location.href; + window.location.reload(true); } } @@ -113,11 +116,19 @@ class BrowserStoreClass { signalLogout() { if (this.isLocalStorageSupported()) { - localStorage.setItem('__logout__', 'yes'); + // 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__'); + } + /** * Preforms the given action on each item that has the given prefix * Signature for action is action(key, value) @@ -151,7 +162,14 @@ class BrowserStoreClass { } 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() { @@ -185,3 +203,4 @@ class BrowserStoreClass { var BrowserStore = new BrowserStoreClass(); export default BrowserStore; +window.BrowserStore = BrowserStore; diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index cc0d0d14b..0bfde77b4 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -1,19 +1,18 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var EventEmitter = require('events').EventEmitter; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; var Utils; -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; +const NotificationPrefs = Constants.NotificationPrefs; -var BrowserStore = require('../stores/browser_store.jsx'); - -var CHANGE_EVENT = 'change'; -var LEAVE_EVENT = 'leave'; -var MORE_CHANGE_EVENT = 'change'; -var EXTRA_INFO_EVENT = 'extra_info'; +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) { @@ -21,7 +20,43 @@ class ChannelStoreClass extends EventEmitter { this.setMaxListeners(11); + 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.pSetPostMode = this.pSetPostMode.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); @@ -90,29 +125,19 @@ class ChannelStoreClass extends EventEmitter { setCurrentId(id) { this.currentId = id; } - setLastVisitedName(name) { - if (name == null) { - BrowserStore.removeItem('last_visited_name'); - } else { - BrowserStore.setItem('last_visited_name', name); - } - } - getLastVisitedName() { - return BrowserStore.getItem('last_visited_name'); - } resetCounts(id) { - var cm = this.pGetChannelMembers(); + 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; } } - this.pStoreChannelMembers(cm); } getCurrentId() { return this.currentId; @@ -142,18 +167,7 @@ class ChannelStoreClass extends EventEmitter { this.emitChange(); } getCurrentExtraInfo() { - var currentId = this.getCurrentId(); - var extra = null; - - if (currentId) { - extra = this.pGetExtraInfos()[currentId]; - } - - if (extra == null) { - extra = {members: []}; - } - - return extra; + return this.getExtraInfo(this.getCurrentId()); } getExtraInfo(channelId) { var extra = null; @@ -162,7 +176,10 @@ class ChannelStoreClass extends EventEmitter { extra = this.pGetExtraInfos()[channelId]; } - if (extra == null) { + if (extra) { + // create a defensive copy + extra = JSON.parse(JSON.stringify(extra)); + } else { extra = {members: []}; } @@ -192,10 +209,10 @@ class ChannelStoreClass extends EventEmitter { this.pStoreChannels(channels); } pStoreChannels(channels) { - BrowserStore.setItem('channels', channels); + this.channels = channels; } pGetChannels() { - return BrowserStore.getItem('channels', []); + return this.channels; } pStoreChannelMember(channelMember) { var members = this.pGetChannelMembers(); @@ -203,49 +220,90 @@ class ChannelStoreClass extends EventEmitter { this.pStoreChannelMembers(members); } pStoreChannelMembers(channelMembers) { - BrowserStore.setItem('channel_members', channelMembers); + this.channelMembers = channelMembers; } pGetChannelMembers() { - return BrowserStore.getItem('channel_members', {}); + return this.channelMembers; } pStoreMoreChannels(channels) { - BrowserStore.setItem('more_channels', channels); + this.moreChannels = channels; } pGetMoreChannels() { - var channels = BrowserStore.getItem('more_channels'); - - if (channels == null) { - channels = {}; - channels.loading = true; - } - - return channels; + return this.moreChannels; } pStoreExtraInfos(extraInfos) { - BrowserStore.setItem('extra_infos', extraInfos); + this.extraInfos = extraInfos; } pGetExtraInfos() { - return BrowserStore.getItem('extra_infos', {}); + return this.extraInfos; } isDefault(channel) { return channel.name === Constants.DEFAULT_CHANNEL; } + + pSetPostMode(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(function handleAction(payload) { +ChannelStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; var currentId; switch (action.type) { case ActionTypes.CLICK_CHANNEL: ChannelStore.setCurrentId(action.id); - ChannelStore.setLastVisitedName(action.name); ChannelStore.resetCounts(action.id); + ChannelStore.pSetPostMode(ChannelStore.POST_MODE_CHANNEL); ChannelStore.emitChange(); break; + case ActionTypes.RECIEVED_FOCUSED_POST: { + const post = action.post_list.posts[action.postId]; + ChannelStore.setCurrentId(post.channel_id); + ChannelStore.pSetPostMode(ChannelStore.POST_MODE_FOCUS); + ChannelStore.emitChange(); + break; + } + case ActionTypes.RECIEVED_CHANNELS: ChannelStore.pStoreChannels(action.channels); ChannelStore.pStoreChannelMembers(action.members); @@ -253,6 +311,7 @@ ChannelStore.dispatchToken = AppDispatcher.register(function handleAction(payloa if (currentId) { ChannelStore.resetCounts(currentId); } + ChannelStore.setUnreadCounts(); ChannelStore.emitChange(); break; @@ -263,6 +322,7 @@ ChannelStore.dispatchToken = AppDispatcher.register(function handleAction(payloa if (currentId) { ChannelStore.resetCounts(currentId); } + ChannelStore.setUnreadCount(action.channel.id); ChannelStore.emitChange(); break; diff --git a/web/react/stores/error_store.jsx b/web/react/stores/error_store.jsx index 775b8e006..8fb051138 100644 --- a/web/react/stores/error_store.jsx +++ b/web/react/stores/error_store.jsx @@ -1,15 +1,15 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var EventEmitter = require('events').EventEmitter; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; -var BrowserStore = require('../stores/browser_store.jsx'); +import BrowserStore from '../stores/browser_store.jsx'; -var CHANGE_EVENT = 'change'; +const CHANGE_EVENT = 'change'; class ErrorStoreClass extends EventEmitter { constructor() { diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx index dc65d48da..9f33cf022 100644 --- a/web/react/stores/modal_store.jsx +++ b/web/react/stores/modal_store.jsx @@ -1,10 +1,10 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -const EventEmitter = require('events').EventEmitter; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; -const Constants = require('../utils/constants.jsx'); +import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; class ModalStoreClass extends EventEmitter { @@ -27,12 +27,16 @@ class ModalStoreClass extends EventEmitter { } handleEventPayload(payload) { - const action = payload.action; + // 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-redeclare - switch (action.type) { + switch (type) { case ActionTypes.TOGGLE_IMPORT_THEME_MODAL: case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL: - this.emit(action.type, action.value); + case ActionTypes.TOGGLE_DELETE_POST_MODAL: + case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL: + case ActionTypes.TOGGLE_REGISTER_APP_MODAL: + this.emit(type, value, args); break; } } diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 0fe253310..2212edadb 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -1,20 +1,21 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var EventEmitter = require('events').EventEmitter; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; -var ChannelStore = require('../stores/channel_store.jsx'); -var BrowserStore = require('../stores/browser_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); +import ChannelStore from '../stores/channel_store.jsx'; +import BrowserStore from '../stores/browser_store.jsx'; +import UserStore from '../stores/user_store.jsx'; -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; -var CHANGE_EVENT = 'change'; -var SELECTED_POST_CHANGE_EVENT = 'selected_post_change'; -var EDIT_POST_EVENT = 'edit_post'; -var POSTS_VIEW_JUMP_EVENT = 'post_list_jump'; +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() { @@ -24,10 +25,6 @@ class PostStoreClass extends EventEmitter { this.addChangeListener = this.addChangeListener.bind(this); this.removeChangeListener = this.removeChangeListener.bind(this); - this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this); - this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this); - this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this); - this.emitEditPost = this.emitEditPost.bind(this); this.addEditPostListener = this.addEditPostListener.bind(this); this.removeEditPostListener = this.removeEditPostListner.bind(this); @@ -36,26 +33,50 @@ class PostStoreClass extends EventEmitter { this.addPostsViewJumpListener = this.addPostsViewJumpListener.bind(this); this.removePostsViewJumpListener = this.removePostsViewJumpListener.bind(this); - this.getCurrentPosts = this.getCurrentPosts.bind(this); + this.emitPostFocused = this.emitPostFocused.bind(this); + this.addPostFocusedListener = this.addPostFocusedListener.bind(this); + this.removePostFocusedListener = this.removePostFocusedListener.bind(this); + + this.makePostsInfo = this.makePostsInfo.bind(this); + + this.getPost = this.getPost.bind(this); + this.getAllPosts = this.getAllPosts.bind(this); + this.getEarliestPost = this.getEarliestPost.bind(this); + this.getLatestPost = this.getLatestPost.bind(this); + this.getVisiblePosts = this.getVisiblePosts.bind(this); + this.getVisibilityAtTop = this.getVisibilityAtTop.bind(this); + this.getVisibilityAtBottom = this.getVisibilityAtBottom.bind(this); + this.requestVisibilityIncrease = this.requestVisibilityIncrease.bind(this); + this.getFocusedPostId = this.getFocusedPostId.bind(this); + this.storePosts = this.storePosts.bind(this); - this.pStorePosts = this.pStorePosts.bind(this); - this.getPosts = this.getPosts.bind(this); this.storePost = this.storePost.bind(this); - this.pStorePost = this.pStorePost.bind(this); + this.storeFocusedPost = this.storeFocusedPost.bind(this); + this.checkBounds = this.checkBounds.bind(this); + + this.clearFocusedPost = this.clearFocusedPost.bind(this); + this.clearChannelVisibility = this.clearChannelVisibility.bind(this); + this.removePost = this.removePost.bind(this); - this.storePendingPost = this.storePendingPost.bind(this); - this.pStorePendingPosts = this.pStorePendingPosts.bind(this); + this.getPendingPosts = this.getPendingPosts.bind(this); - this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this); - this.storeUnseenDeletedPosts = this.storeUnseenDeletedPosts.bind(this); - this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this); - this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this); + this.storePendingPost = this.storePendingPost.bind(this); this.removePendingPost = this.removePendingPost.bind(this); - this.pRemovePendingPost = this.pRemovePendingPost.bind(this); this.clearPendingPosts = this.clearPendingPosts.bind(this); this.updatePendingPost = this.updatePendingPost.bind(this); + + this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this); + this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this); + this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this); + + // These functions are bad and work should be done to remove this system when the RHS dies this.storeSelectedPost = this.storeSelectedPost.bind(this); this.getSelectedPost = this.getSelectedPost.bind(this); + this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this); + this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this); + this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this); + this.selectedPost = null; + this.getEmptyDraft = this.getEmptyDraft.bind(this); this.storeCurrentDraft = this.storeCurrentDraft.bind(this); this.getCurrentDraft = this.getCurrentDraft.bind(this); @@ -68,6 +89,10 @@ class PostStoreClass extends EventEmitter { this.storeLatestUpdate = this.storeLatestUpdate.bind(this); this.getLatestUpdate = this.getLatestUpdate.bind(this); this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this); + this.getCommentCount = this.getCommentCount.bind(this); + + this.postsInfo = {}; + this.currentFocusedPostId = null; } emitChange() { this.emit(CHANGE_EVENT); @@ -81,16 +106,16 @@ class PostStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT, callback); } - emitSelectedPostChange(fromSearch) { - this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch); + emitPostFocused() { + this.emit(FOCUSED_POST_CHANGE); } - addSelectedPostChangeListener(callback) { - this.on(SELECTED_POST_CHANGE_EVENT, callback); + addPostFocusedListener(callback) { + this.on(FOCUSED_POST_CHANGE, callback); } - removeSelectedPostChangeListener(callback) { - this.removeListener(SELECTED_POST_CHANGE_EVENT, callback); + removePostFocusedListener(callback) { + this.removeListener(FOCUSED_POST_CHANGE, callback); } emitEditPost(post) { @@ -129,101 +154,168 @@ class PostStoreClass extends EventEmitter { this.emitPostsViewJump(Constants.PostsViewJumpTypes.SIDEBAR_OPEN, null); } - getCurrentPosts() { - var currentId = ChannelStore.getCurrentId(); + // 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; + } - if (currentId != null) { - return this.getPosts(currentId); + getLatestPost(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[0]]; } + return null; } - storePosts(channelId, newPostsView) { - if (isPostListNull(newPostsView)) { + + 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); + } + + // Add deleted posts + if (this.postsInfo[id].hasOwnProperty('deletedPosts')) { + Object.assign(postList.posts, this.postsInfo[id].deletedPosts); + + for (const postID in this.postsInfo[id].deletedPosts) { + if (this.postsInfo[id].deletedPosts.hasOwnProperty(postID)) { + postList.order.push(postID); + } + } + + // Merge would be faster + postList.order.sort((a, b) => { + if (postList.posts[a].create_at > postList.posts[b].create_at) { + return -1; + } + if (postList.posts[a].create_at < postList.posts[b].create_at) { + return 1; + } + return 0; + }); + } + + 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; } - var postList = makePostListNonNull(this.getPosts(channelId)); + const combinedPosts = makePostListNonNull(this.getAllPosts(id)); - for (const pid in newPostsView.posts) { - if (newPostsView.posts.hasOwnProperty(pid)) { - const np = newPostsView.posts[pid]; + for (const pid in newPosts.posts) { + if (newPosts.posts.hasOwnProperty(pid)) { + const np = newPosts.posts[pid]; if (np.delete_at === 0) { - postList.posts[pid] = np; - if (postList.order.indexOf(pid) === -1) { - postList.order.push(pid); + combinedPosts.posts[pid] = np; + if (combinedPosts.order.indexOf(pid) === -1) { + combinedPosts.order.push(pid); } } else { - if (pid in postList.posts) { - delete postList.posts[pid]; + if (pid in combinedPosts.posts) { + Reflect.deleteProperty(combinedPosts.posts, pid); } - const index = postList.order.indexOf(pid); + const index = combinedPosts.order.indexOf(pid); if (index !== -1) { - postList.order.splice(index, 1); + combinedPosts.order.splice(index, 1); } } } } - postList.order.sort((a, b) => { - if (postList.posts[a].create_at > postList.posts[b].create_at) { + combinedPosts.order.sort((a, b) => { + if (combinedPosts.posts[a].create_at > combinedPosts.posts[b].create_at) { return -1; } - if (postList.posts[a].create_at < postList.posts[b].create_at) { + if (combinedPosts.posts[a].create_at < combinedPosts.posts[b].create_at) { return 1; } return 0; }); - var latestUpdate = 0; - for (var pid in postList.posts) { - if (postList.posts[pid].update_at > latestUpdate) { - latestUpdate = postList.posts[pid].update_at; - } - } - - this.storeLatestUpdate(channelId, latestUpdate); - this.pStorePosts(channelId, postList); - this.emitChange(); - } - pStorePosts(channelId, posts) { - BrowserStore.setItem('posts_' + channelId, posts); - } - getPosts(channelId) { - return BrowserStore.getItem('posts_' + channelId); + this.makePostsInfo(id); + this.postsInfo[id].postList = combinedPosts; } - getCurrentUsersLatestPost(channelId, rootId) { - const userId = UserStore.getCurrentId(); - var postList = makePostListNonNull(this.getPosts(channelId)); - var i = 0; - var len = postList.order.length; - var lastPost = null; - - for (i; i < len; i++) { - let 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; - } storePost(post) { - this.pStorePost(post); - this.emitChange(); - } - pStorePost(post) { - var postList = this.getPosts(post.channel_id); - postList = makePostListNonNull(postList); + const postList = makePostListNonNull(this.getAllPosts(post.channel_id)); if (post.pending_post_id !== '') { this.removePendingPost(post.channel_id, post.pending_post_id); @@ -236,65 +328,119 @@ class PostStoreClass extends EventEmitter { postList.order.unshift(post.id); } - this.pStorePosts(post.channel_id, postList); + this.makePostsInfo(post.channel_id); + this.postsInfo[post.channel_id].postList = postList; } - removePost(postId, channelId) { - var postList = this.getPosts(channelId); + + 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; + } + + removePost(post) { + const channelId = post.channel_id; + this.makePostsInfo(channelId); + const postList = this.postsInfo[channelId].postList; if (isPostListNull(postList)) { return; } - if (postId in postList.posts) { - delete postList.posts[postId]; + if (post.id in postList.posts) { + Reflect.deleteProperty(postList.posts, post.id); } - var index = postList.order.indexOf(postId); + const index = postList.order.indexOf(post.id); if (index !== -1) { postList.order.splice(index, 1); } - this.pStorePosts(channelId, postList); + this.postsInfo[channelId].postList = postList; } + + getPendingPosts(channelId) { + if (this.postsInfo.hasOwnProperty(channelId)) { + return this.postsInfo[channelId].pendingPosts; + } + + return null; + } + storePendingPost(post) { post.state = Constants.POST_LOADING; - var postList = this.getPendingPosts(post.channel_id); - postList = makePostListNonNull(postList); + const postList = makePostListNonNull(this.getPendingPosts(post.channel_id)); postList.posts[post.pending_post_id] = post; postList.order.unshift(post.pending_post_id); - this.pStorePendingPosts(post.channel_id, postList); + + this.makePostsInfo(post.channel_id); + this.postsInfo[post.channel_id].pendingPosts = postList; this.emitChange(); } - pStorePendingPosts(channelId, postList) { - var posts = postList.posts; - // sort failed posts to the bottom - postList.order.sort((a, b) => { - if (posts[a].state === Constants.POST_LOADING && posts[b].state === Constants.POST_FAILED) { - return 1; - } - if (posts[a].state === Constants.POST_FAILED && posts[b].state === Constants.POST_LOADING) { - return -1; - } + removePendingPost(channelId, pendingPostId) { + const postList = makePostListNonNull(this.getPendingPosts(channelId)); - if (posts[a].create_at > posts[b].create_at) { - return -1; - } - if (posts[a].create_at < posts[b].create_at) { - return 1; - } + Reflect.deleteProperty(postList.posts, pendingPostId); + const index = postList.order.indexOf(pendingPostId); + if (index === -1) { + return; + } - return 0; - }); + postList.order.splice(index, 1); - BrowserStore.setGlobalItem('pending_posts_' + channelId, postList); + this.postsInfo[channelId].pendingPosts = postList; + this.emitChange(); } - getPendingPosts(channelId) { - return BrowserStore.getGlobalItem('pending_posts_' + channelId); + + clearPendingPosts(channelId) { + if (this.postsInfo.hasOwnProperty(channelId)) { + Reflect.deleteProperty(this.postsInfo[channelId], 'pendingPosts'); + } + } + + updatePendingPost(post) { + const postList = makePostListNonNull(this.getPendingPosts(post.channel_id)); + + if (postList.order.indexOf(post.pending_post_id) === -1) { + return; + } + + postList.posts[post.pending_post_id] = post; + this.postsInfo[post.channel_id].pendingPosts = postList; + this.emitChange(); } + storeUnseenDeletedPost(post) { - var posts = this.getUnseenDeletedPosts(post.channel_id); + let posts = this.getUnseenDeletedPosts(post.channel_id); if (!posts) { posts = {}; @@ -305,58 +451,68 @@ class PostStoreClass extends EventEmitter { post.filenames = []; posts[post.id] = post; - this.storeUnseenDeletedPosts(post.channel_id, posts); - } - storeUnseenDeletedPosts(channelId, posts) { - BrowserStore.setItem('deleted_posts_' + channelId, posts); + this.postsInfo[post.channel_id].deletedPosts = posts; } + getUnseenDeletedPosts(channelId) { - return BrowserStore.getItem('deleted_posts_' + channelId); + if (this.postsInfo.hasOwnProperty(channelId)) { + return this.postsInfo[channelId].deletedPosts; + } + + return null; } + clearUnseenDeletedPosts(channelId) { - BrowserStore.setItem('deleted_posts_' + channelId, {}); + if (this.postsInfo.hasOwnProperty(channelId)) { + Reflect.deleteProperty(this.postsInfo[channelId], 'deletedPosts'); + } } - removePendingPost(channelId, pendingPostId) { - this.pRemovePendingPost(channelId, pendingPostId); - this.emitChange(); + + storeSelectedPost(postList) { + this.selectedPost = postList; } - pRemovePendingPost(channelId, pendingPostId) { - var postList = this.getPendingPosts(channelId); - postList = makePostListNonNull(postList); - if (pendingPostId in postList.posts) { - delete postList.posts[pendingPostId]; - } - var index = postList.order.indexOf(pendingPostId); - if (index !== -1) { - postList.order.splice(index, 1); - } + getSelectedPost() { + return this.selectedPost; + } - this.pStorePendingPosts(channelId, postList); + emitSelectedPostChange(fromSearch) { + this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch); } - clearPendingPosts() { - BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', (key) => { - BrowserStore.removeItem(key); - }); + + addSelectedPostChangeListener(callback) { + this.on(SELECTED_POST_CHANGE_EVENT, callback); } - updatePendingPost(post) { - var postList = this.getPendingPosts(post.channel_id); - postList = makePostListNonNull(postList); - if (postList.order.indexOf(post.pending_post_id) === -1) { - return; + 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; + } + } } - postList.posts[post.pending_post_id] = post; - this.pStorePendingPosts(post.channel_id, postList); - this.emitChange(); - } - storeSelectedPost(postList) { - BrowserStore.setItem('select_post', postList); - } - getSelectedPost() { - return BrowserStore.getItem('select_post'); + return lastPost; } + getEmptyDraft() { return {message: '', uploadsInProgress: [], previews: []}; } @@ -397,10 +553,31 @@ class PostStoreClass extends EventEmitter { }); } storeLatestUpdate(channelId, time) { - BrowserStore.setItem('latest_post_' + channelId, time); + if (!this.postsInfo.hasOwnProperty(channelId)) { + this.postsInfo[channelId] = {}; + } + this.postsInfo[channelId].latestPost = time; } getLatestUpdate(channelId) { - return BrowserStore.getItem('latest_post_' + channelId, 0); + if (this.postsInfo.hasOwnProperty(channelId) && this.postsInfo[channelId].hasOwnProperty('latestPost')) { + return this.postsInfo[channelId].latestPost; + } + + return 0; + } + 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; } } @@ -410,20 +587,45 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.RECIEVED_POSTS: - PostStore.storePosts(action.id, makePostListNonNull(action.post_list)); + case ActionTypes.RECIEVED_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.RECIEVED_FOCUSED_POST: + PostStore.clearChannelVisibility(action.postId, false); + PostStore.storeFocusedPost(action.postId, makePostListNonNull(action.post_list)); + PostStore.emitChange(); break; case ActionTypes.RECIEVED_POST: - PostStore.pStorePost(action.post); + PostStore.storePost(action.post); + PostStore.emitChange(); + break; + case ActionTypes.RECIEVED_EDIT_POST: + PostStore.emitEditPost(action); + PostStore.emitChange(); + break; + case ActionTypes.CLICK_CHANNEL: + PostStore.clearFocusedPost(); + PostStore.clearChannelVisibility(action.id, true); + PostStore.clearUnseenDeletedPosts(action.id); + break; + case ActionTypes.CREATE_POST: + PostStore.storePendingPost(action.post); + PostStore.storeDraft(action.post.channel_id, null); + PostStore.jumpPostsViewToBottom(); + break; + case ActionTypes.POST_DELETED: + PostStore.storeUnseenDeletedPost(action.post); + PostStore.removePost(action.post); PostStore.emitChange(); break; case ActionTypes.RECIEVED_POST_SELECTED: PostStore.storeSelectedPost(action.post_list); PostStore.emitSelectedPostChange(action.from_search); break; - case ActionTypes.RECIEVED_EDIT_POST: - PostStore.emitEditPost(action); - break; default: } }); diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx index f630d150d..068bc29c2 100644 --- a/web/react/stores/preference_store.jsx +++ b/web/react/stores/preference_store.jsx @@ -1,11 +1,12 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -const ActionTypes = require('../utils/constants.jsx').ActionTypes; -const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -const BrowserStore = require('./browser_store.jsx'); -const EventEmitter = require('events').EventEmitter; -const UserStore = require('../stores/user_store.jsx'); +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'; @@ -105,7 +106,7 @@ class PreferenceStoreClass extends EventEmitter { const action = payload.action; switch (action.type) { - case ActionTypes.RECIEVED_PREFERENCES: + case ActionTypes.RECIEVED_PREFERENCES: { const preferences = this.getAllPreferences(); for (const preference of action.preferences) { @@ -114,6 +115,8 @@ class PreferenceStoreClass extends EventEmitter { this.setAllPreferences(preferences); this.emitChange(preferences); + break; + } } } } diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx index 95f0ea845..f932c379a 100644 --- a/web/react/stores/search_store.jsx +++ b/web/react/stores/search_store.jsx @@ -1,19 +1,18 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var EventEmitter = require('events').EventEmitter; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; -var BrowserStore = require('../stores/browser_store.jsx'); +import BrowserStore from '../stores/browser_store.jsx'; -var Constants = require('../utils/constants.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 MENTION_DATA_CHANGE_EVENT = 'mention_data_change'; -var ADD_MENTION_EVENT = 'add_mention'; +var SHOW_SEARCH_EVENT = 'show_search'; class SearchStoreClass extends EventEmitter { constructor() { @@ -31,9 +30,9 @@ class SearchStoreClass extends EventEmitter { this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this); this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this); - this.emitMentionDataChange = this.emitMentionDataChange.bind(this); - this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this); - this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.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); @@ -80,6 +79,18 @@ class SearchStoreClass extends EventEmitter { 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'); } @@ -96,30 +107,6 @@ class SearchStoreClass extends EventEmitter { return BrowserStore.getItem('search_term'); } - emitMentionDataChange(id, mentionText) { - this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText); - } - - addMentionDataChangeListener(callback) { - this.on(MENTION_DATA_CHANGE_EVENT, callback); - } - - removeMentionDataChangeListener(callback) { - this.removeListener(MENTION_DATA_CHANGE_EVENT, callback); - } - - emitAddMention(id, username) { - this.emit(ADD_MENTION_EVENT, id, username); - } - - addAddMentionListener(callback) { - this.on(ADD_MENTION_EVENT, callback); - } - - removeAddMentionListener(callback) { - this.removeListener(ADD_MENTION_EVENT, callback); - } - storeSearchResults(results, isMentionSearch) { BrowserStore.setItem('search_results', results); BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch)); @@ -140,11 +127,8 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => { SearchStore.storeSearchTerm(action.term); SearchStore.emitSearchTermChange(action.do_search, action.is_mention_search); break; - case ActionTypes.RECIEVED_MENTION_DATA: - SearchStore.emitMentionDataChange(action.id, action.mention_text); - break; - case ActionTypes.RECIEVED_ADD_MENTION: - SearchStore.emitAddMention(action.id, action.username); + case ActionTypes.SHOW_SEARCH: + SearchStore.emitShowSearch(); break; default: } diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 4efeb7c8f..d5aed40cf 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -1,19 +1,18 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -const UserStore = require('./user_store.jsx'); -const PostStore = require('./post_store.jsx'); -const ChannelStore = require('./channel_store.jsx'); -const BrowserStore = require('./browser_store.jsx'); -const ErrorStore = require('./error_store.jsx'); -const EventEmitter = require('events').EventEmitter; - -const Utils = require('../utils/utils.jsx'); -const AsyncClient = require('../utils/async_client.jsx'); - -const Constants = require('../utils/constants.jsx'); -const ActionTypes = Constants.ActionTypes; +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 EventHelpers from '../dispatcher/event_helpers.jsx'; + +import Constants from '../utils/constants.jsx'; const SocketEvents = Constants.SocketEvents; const CHANGE_EVENT = 'change'; @@ -60,13 +59,14 @@ class SocketStoreClass extends EventEmitter { conn.onopen = () => { if (this.failCount > 0) { console.log('websocket re-established connection'); //eslint-disable-line no-console + + if (ErrorStore.getLastError()) { + ErrorStore.storeLastError(null); + ErrorStore.emitChange(); + } } this.failCount = 0; - if (ErrorStore.getLastError()) { - ErrorStore.storeLastError(null); - ErrorStore.emitChange(); - } }; conn.onclose = () => { @@ -91,10 +91,9 @@ class SocketStoreClass extends EventEmitter { }; conn.onmessage = (evt) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_MSG, - msg: JSON.parse(evt.data) - }); + const msg = JSON.parse(evt.data); + this.handleMessage(msg); + this.emitChange(msg); }; } } @@ -153,19 +152,19 @@ class SocketStoreClass extends EventEmitter { function handleNewPostEvent(msg) { // Store post const post = JSON.parse(msg.props.post); - PostStore.storePost(post); + EventHelpers.emitPostRecievedEvent(post); // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { if (window.isActive) { - AsyncClient.updateLastViewedAt(true); + AsyncClient.updateLastViewedAt(); } } 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') { + if ((UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') && !Utils.isSystemMessage(post)) { const msgProps = msg.props; let mentions = []; @@ -226,6 +225,7 @@ 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) { @@ -237,20 +237,17 @@ function handlePostEditEvent(msg) { function handlePostDeleteEvent(msg) { const post = JSON.parse(msg.props.post); - - PostStore.storeUnseenDeletedPost(post); - PostStore.removePost(post, true); - PostStore.emitChange(); + EventHelpers.emitPostDeletedEvent(post); } function handleNewUserEvent() { AsyncClient.getProfiles(); - AsyncClient.getChannelExtraInfo(true); + AsyncClient.getChannelExtraInfo(); } function handleUserAddedEvent(msg) { if (ChannelStore.getCurrentId() === msg.channel_id) { - AsyncClient.getChannelExtraInfo(true); + AsyncClient.getChannelExtraInfo(); } if (UserStore.getCurrentId() === msg.user_id) { @@ -273,7 +270,7 @@ function handleUserRemovedEvent(msg) { $('#removed_from_channel').modal('show'); } } else if (ChannelStore.getCurrentId() === msg.channel_id) { - AsyncClient.getChannelExtraInfo(true); + AsyncClient.getChannelExtraInfo(); } } @@ -286,17 +283,12 @@ function handleChannelViewedEvent(msg) { var SocketStore = new SocketStoreClass(); -SocketStore.dispatchToken = AppDispatcher.register((payload) => { +/*SocketStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.RECIEVED_MSG: - SocketStore.handleMessage(action.msg); - SocketStore.emitChange(action.msg); - break; - default: } -}); + });*/ export default SocketStore; diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx new file mode 100644 index 000000000..9cd566c22 --- /dev/null +++ b/web/react/stores/suggestion_store.jsx @@ -0,0 +1,259 @@ +// 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-redeclare + + 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 (other.matchedPretext === this.getMatchedPretext(id)) { + // 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/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index 22114ae85..2d518d9e7 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -1,14 +1,14 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var EventEmitter = require('events').EventEmitter; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; -var BrowserStore = require('../stores/browser_store.jsx'); +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; +import BrowserStore from '../stores/browser_store.jsx'; -var CHANGE_EVENT = 'change'; +const CHANGE_EVENT = 'change'; var Utils; function getWindowLocationOrigin() { @@ -31,6 +31,7 @@ class TeamStoreClass extends EventEmitter { 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); } @@ -92,6 +93,16 @@ class TeamStoreClass extends EventEmitter { return null; } + getCurrentInviteLink() { + const current = this.getCurrent(); + + if (current) { + return getWindowLocationOrigin() + '/signup_user_complete/?id=' + current.invite_id; + } + + return ''; + } + saveTeam(team) { var teams = this.getAll(); teams[team.id] = team; diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index b173c9ca0..3e1871180 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -1,18 +1,18 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var EventEmitter = require('events').EventEmitter; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; -var BrowserStore = require('./browser_store.jsx'); +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; +import BrowserStore from './browser_store.jsx'; -var CHANGE_EVENT = 'change'; -var CHANGE_EVENT_SESSIONS = 'change_sessions'; -var CHANGE_EVENT_AUDITS = 'change_audits'; -var CHANGE_EVENT_TEAMS = 'change_teams'; -var CHANGE_EVENT_STATUSES = 'change_statuses'; +const CHANGE_EVENT = 'change'; +const CHANGE_EVENT_SESSIONS = 'change_sessions'; +const CHANGE_EVENT_AUDITS = 'change_audits'; +const CHANGE_EVENT_TEAMS = 'change_teams'; +const CHANGE_EVENT_STATUSES = 'change_statuses'; class UserStoreClass extends EventEmitter { constructor() { @@ -164,6 +164,10 @@ class UserStoreClass extends EventEmitter { } getProfile(userId) { + if (userId === this.getCurrentId()) { + return this.getCurrentUser(); + } + return this.getProfiles()[userId]; } @@ -350,5 +354,4 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { } }); -global.window.UserStore = UserStore; -export default UserStore; +export {UserStore as default}; |