summaryrefslogtreecommitdiffstats
path: root/web/react/stores
diff options
context:
space:
mode:
Diffstat (limited to 'web/react/stores')
-rw-r--r--web/react/stores/browser_store.jsx21
-rw-r--r--web/react/stores/channel_store.jsx62
-rw-r--r--web/react/stores/search_store.jsx36
-rw-r--r--web/react/stores/socket_store.jsx2
-rw-r--r--web/react/stores/suggestion_store.jsx272
5 files changed, 340 insertions, 53 deletions
diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx
index 2e3a26cff..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) {
@@ -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 dec4926f5..0bfde77b4 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -7,6 +7,7 @@ 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';
@@ -37,6 +38,10 @@ class ChannelStoreClass extends EventEmitter {
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;
@@ -45,6 +50,7 @@ class ChannelStoreClass extends EventEmitter {
this.moreChannels = {};
this.moreChannels.loading = true;
this.extraInfos = {};
+ this.unreadCounts = {};
}
get POST_MODE_CHANNEL() {
return 1;
@@ -120,18 +126,18 @@ class ChannelStoreClass extends EventEmitter {
this.currentId = id;
}
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;
@@ -161,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;
@@ -181,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: []};
}
@@ -250,6 +248,38 @@ class ChannelStoreClass extends EventEmitter {
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();
@@ -281,6 +311,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
if (currentId) {
ChannelStore.resetCounts(currentId);
}
+ ChannelStore.setUnreadCounts();
ChannelStore.emitChange();
break;
@@ -291,6 +322,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
if (currentId) {
ChannelStore.resetCounts(currentId);
}
+ ChannelStore.setUnreadCount(action.channel.id);
ChannelStore.emitChange();
break;
diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx
index e8ab6a2ae..f932c379a 100644
--- a/web/react/stores/search_store.jsx
+++ b/web/react/stores/search_store.jsx
@@ -12,8 +12,6 @@ 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 {
@@ -32,10 +30,6 @@ 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);
@@ -113,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));
@@ -157,12 +127,6 @@ 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);
- break;
case ActionTypes.SHOW_SEARCH:
SearchStore.emitShowSearch();
break;
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index 2e0769cc4..29aa32a08 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -163,7 +163,7 @@ function handleNewPostEvent(msg) {
}
// 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 = [];
diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx
new file mode 100644
index 000000000..2250ec234
--- /dev/null
+++ b/web/react/stores/suggestion_store.jsx
@@ -0,0 +1,272 @@
+// 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
+ // completeOnSpace: whether or not space will trigger the term to be autocompleted
+ 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: '',
+ completeOnSpace: true
+ });
+ }
+
+ unregisterSuggestionBox(id) {
+ this.suggestions.delete(id);
+ }
+
+ clearSuggestions(id) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.matchedPretext = '';
+ suggestion.terms = [];
+ suggestion.items = [];
+ suggestion.components = [];
+ suggestion.completeOnSpace = true;
+ }
+
+ 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;
+ }
+
+ setCompleteOnSpace(id, completeOnSpace) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.completeOnSpace = completeOnSpace;
+ }
+
+ 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;
+ }
+
+ shouldCompleteOnSpace(id) {
+ return this.suggestions.get(id).completeOnSpace;
+ }
+
+ 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();