Commit 57e29072 authored by Eric Webster's avatar Eric Webster Committed by Joram Wilander

[PLT-7676] Refactor webapp search store and right hand sidebar view state (#287)

* added appropriate rhs reducers

* add rhs state to redux

* refactor search bar

* remove user store usage in sidebar_right

* fix import, refactor navbar

* moving to redux

* more actions

* put search terms in redux

* fixed mention search

* filter out deleted posts

* fix back button on mentions

* handle sidebar channel in sidebar

* remove resize event correctly

* isSearching rhs state

* show loading search results before search is finished

* search for term

* remove unused action types

* remove console.log

* bit of refactoring

* fix style issues

* PR comment fixes

* added reducer tests for rhs

* started on adding rhs action tests

* use channel selector

* clean up showMentions

* clean up batch action

* more unit tests

* more tests

* last of the tests

* previousRhsState feedback addressed

* emitCloseRightHandSide fix

* correctly merge @here mention search terms change

* fix header post logic to match

* flagged posts bug fix and improvements

* more consistent store fix

* added comment draft prefix
parent 2beb61c6
......@@ -9,17 +9,16 @@ import {removeUserFromTeam} from 'mattermost-redux/actions/teams';
import {Client4} from 'mattermost-redux/client';
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
import {trackEvent} from 'actions/diagnostics_actions.jsx';
import {handleNewPost} from 'actions/post_actions.jsx';
import {stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
import {loadNewDMIfNeeded, loadNewGMIfNeeded, loadProfilesForSidebar} from 'actions/user_actions.jsx';
import {closeRightHandSide} from 'actions/views/rhs';
import * as WebsocketActions from 'actions/websocket_actions.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import ErrorStore from 'stores/error_store.jsx';
import store from 'stores/redux_store.jsx';
import SearchStore from 'stores/search_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
......@@ -118,6 +117,10 @@ export async function doFocusPost(channelId, postId, data) {
getChannelStats(channelId)(dispatch, getState);
}
export function emitCloseRightHandSide() {
dispatch(closeRightHandSide());
}
export async function emitPostFocusEvent(postId, onSuccess) {
loadChannelsForCurrentUser();
const {data} = await getPostThread(postId)(dispatch, getState);
......@@ -141,36 +144,6 @@ export async function emitPostFocusEvent(postId, onSuccess) {
}
}
export function emitCloseRightHandSide() {
SearchStore.storeSearchResults(null, false, false);
SearchStore.emitSearchChange();
dispatch({
type: ActionTypes.SELECT_POST,
postId: '',
channelId: ''
});
}
export async function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) {
await getPostThread(post.id)(dispatch, getState);
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: Utils.getRootId(post),
channelId: post.channel_id,
from_search: SearchStore.getSearchTerm(),
from_flagged_posts: SearchStore.getIsFlaggedPosts(),
from_pinned_posts: SearchStore.getIsPinnedPosts()
});
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH,
results: null,
is_mention_search: isMentionSearch
});
}
export function emitLeaveTeam() {
removeUserFromTeam(TeamStore.getCurrentId(), UserStore.getCurrentId())(dispatch, getState);
}
......@@ -469,73 +442,8 @@ export function clientLogout(redirectTo = '/') {
window.location.href = redirectTo;
}
export function emitSearchMentionsEvent(user) {
let terms = '';
if (user.notify_props) {
const termKeys = UserStore.getMentionKeys(user.id);
const indexOfChannel = termKeys.indexOf('@channel');
if (indexOfChannel !== -1) {
termKeys.splice(indexOfChannel, 1);
}
const indexOfAll = termKeys.indexOf('@all');
if (indexOfAll !== -1) {
termKeys.splice(indexOfAll, 1);
}
const indexOfHere = termKeys.indexOf('@here');
if (indexOfHere !== -1) {
termKeys.splice(indexOfHere, 1);
}
terms = termKeys.join(' ');
}
trackEvent('api', 'api_posts_search_mention');
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH_TERM,
term: terms,
do_search: true,
is_mention_search: true
});
}
export function toggleSideBarAction(visible) {
if (!visible) {
//Array of actions resolving in the closing of the sidebar
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH,
results: null
});
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH_TERM,
term: null,
do_search: false,
is_mention_search: false
});
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: null,
channelId: null
});
}
}
export function toggleSideBarRightMenuAction() {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH,
results: null
});
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: null,
channelId: null
});
dispatch(closeRightHandSide());
document.querySelector('.app__body .inner-wrap').classList.remove('move--right', 'move--left', 'move--left-small');
document.querySelector('.app__body .sidebar--left').classList.remove('move--right');
......
......@@ -7,17 +7,18 @@ import {batchActions} from 'redux-batched-actions';
import {PostTypes} from 'mattermost-redux/action_types';
import {getMyChannelMember} from 'mattermost-redux/actions/channels';
import * as PostActions from 'mattermost-redux/actions/posts';
import {Client4} from 'mattermost-redux/client';
import * as Selectors from 'mattermost-redux/selectors/entities/posts';
import {sendDesktopNotification} from 'actions/notification_actions.jsx';
import {loadNewDMIfNeeded, loadNewGMIfNeeded} from 'actions/user_actions.jsx';
import * as RhsActions from 'actions/views/rhs';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import store from 'stores/redux_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {getSelectedPostId} from 'selectors/rhs';
import {ActionTypes, Constants} from 'utils/constants.jsx';
import {EMOJI_PATTERN} from 'utils/emoticons.jsx';
......@@ -102,54 +103,6 @@ export function unflagPost(postId) {
PostActions.unflagPost(postId)(dispatch, getState);
}
export function getFlaggedPosts() {
Client4.getFlaggedPosts(UserStore.getCurrentId(), '', TeamStore.getCurrentId()).then(
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH_TERM,
term: null,
do_search: false,
is_mention_search: false
});
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH,
results: data,
is_flagged_posts: true,
is_pinned_posts: false
});
PostActions.getProfilesAndStatusesForPosts(data.posts, dispatch, getState);
}
).catch(
() => {} //eslint-disable-line no-empty-function
);
}
export function getPinnedPosts(channelId = ChannelStore.getCurrentId()) {
Client4.getPinnedPosts(channelId).then(
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH_TERM,
term: null,
do_search: false,
is_mention_search: false
});
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH,
results: {...data, channelId},
is_flagged_posts: false,
is_pinned_posts: true
});
PostActions.getProfilesAndStatusesForPosts(data.posts, dispatch, getState);
}
).catch(
() => {} //eslint-disable-line no-empty-function
);
}
export function addReaction(channelId, postId, emojiName) {
PostActions.addReaction(postId, emojiName)(dispatch, getState);
}
......@@ -210,7 +163,7 @@ export async function deletePost(channelId, post, success) {
await PostActions.deletePost(post, hardDelete)(dispatch, getState);
if (post.id === getState().views.rhs.selectedPostId) {
if (post.id === getSelectedPostId(getState())) {
dispatch({
type: ActionTypes.SELECT_POST,
postId: '',
......@@ -240,30 +193,6 @@ export async function deletePost(channelId, post, success) {
}
}
export function performSearch(terms, isMentionSearch, success, error) {
Client4.searchPosts(TeamStore.getCurrentId(), terms, isMentionSearch).then(
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH,
results: data,
is_mention_search: isMentionSearch
});
PostActions.getProfilesAndStatusesForPosts(data.posts, dispatch, getState);
if (success) {
success(data);
}
}
).catch(
(err) => {
if (error) {
error(err);
}
}
);
}
const POST_INCREASE_AMOUNT = Constants.POST_CHUNK_SIZE / 2;
// Returns true if there are more posts to load
......@@ -313,11 +242,8 @@ export function increasePostVisibility(channelId, focusedPostId) {
}
export function searchForTerm(term) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH_TERM,
term,
do_search: true
});
dispatch(RhsActions.updateSearchTerms(term));
dispatch(RhsActions.showSearchResults());
}
export function pinPost(postId) {
......
......@@ -27,12 +27,12 @@ import {EmojiMap} from 'stores/emoji_store.jsx';
import {makeGetCommentDraft} from 'selectors/rhs';
import * as Utils from 'utils/utils.jsx';
import {Constants} from 'utils/constants.jsx';
import {Constants, StoragePrefixes} from 'utils/constants.jsx';
import {REACTION_PATTERN} from 'components/create_post.jsx';
export function clearCommentDraftUploads() {
return actionOnGlobalItemsWithPrefix('comment_draft_', (key, value) => {
return actionOnGlobalItemsWithPrefix(StoragePrefixes.COMMENT_DRAFT, (key, value) => {
if (value) {
return {...value, uploadsInProgress: []};
}
......@@ -41,7 +41,7 @@ export function clearCommentDraftUploads() {
}
export function updateCommentDraft(rootId, draft) {
return setGlobalItem(`comment_draft_${rootId}`, draft);
return setGlobalItem(`${StoragePrefixes.COMMENT_DRAFT}${rootId}`, draft);
}
export function makeOnMoveHistoryIndex(rootId, direction) {
......
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
import {SearchTypes} from 'mattermost-redux/action_types';
import {searchPosts} from 'mattermost-redux/actions/search';
import * as PostActions from 'mattermost-redux/actions/posts';
import {Client4} from 'mattermost-redux/client';
import {getCurrentUserId, getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {trackEvent} from 'actions/diagnostics_actions.jsx';
import {getSearchTerms, getRhsState} from 'selectors/rhs';
import {ActionTypes, RHSStates} from 'utils/constants';
import * as Utils from 'utils/utils';
export function updateRhsState(rhsState) {
return (dispatch, getState) => {
const action = {
type: ActionTypes.UPDATE_RHS_STATE,
state: rhsState
};
if (rhsState === RHSStates.PIN) {
action.channelId = getCurrentChannelId(getState());
}
dispatch(action);
};
}
export function selectPostFromRightHandSideSearch(post) {
return async (dispatch, getState) => {
await dispatch(PostActions.getPostThread(post.id));
dispatch({
type: ActionTypes.SELECT_POST,
postId: Utils.getRootId(post),
channelId: post.channel_id,
previousRhsState: getRhsState(getState())
});
};
}
export function updateSearchTerms(terms) {
return {
type: ActionTypes.UPDATE_RHS_SEARCH_TERMS,
terms
};
}
export function performSearch(terms, isMentionSearch) {
return (dispatch, getState) => {
const teamId = getCurrentTeamId(getState());
return dispatch(searchPosts(teamId, terms, isMentionSearch));
};
}
export function showSearchResults() {
return (dispatch, getState) => {
const searchTerms = getSearchTerms(getState());
dispatch(updateRhsState(RHSStates.SEARCH));
return dispatch(performSearch(searchTerms));
};
}
function receivedSearchPosts(teamId, result) {
return batchActions([
{
type: SearchTypes.RECEIVED_SEARCH_POSTS,
data: result
},
{
type: SearchTypes.RECEIVED_SEARCH_TERM,
data: {
teamId,
terms: null,
isOrSearch: false
}
},
{
type: SearchTypes.SEARCH_POSTS_SUCCESS
}
], 'SEARCH_POST_BATCH');
}
export function getFlaggedPosts() {
return async (dispatch, getState) => {
const state = getState();
const userId = getCurrentUserId(state);
const teamId = getCurrentTeamId(state);
const result = await Client4.getFlaggedPosts(userId, '', teamId);
await PostActions.getProfilesAndStatusesForPosts(result.posts, dispatch, getState);
dispatch(receivedSearchPosts(teamId, result));
};
}
export function showFlaggedPosts() {
return (dispatch) => {
dispatch(getFlaggedPosts());
dispatch(updateSearchTerms(''));
dispatch(updateRhsState(RHSStates.FLAG));
};
}
export function getPinnedPosts(channelId) {
return async (dispatch, getState) => {
const currentChannelId = getCurrentChannelId(getState());
const result = await Client4.getPinnedPosts(channelId || currentChannelId);
await PostActions.getProfilesAndStatusesForPosts(result.posts, dispatch, getState);
const teamId = getCurrentTeamId(getState());
dispatch(receivedSearchPosts(teamId, result));
};
}
export function showPinnedPosts(channelId) {
return (dispatch) => {
dispatch(getPinnedPosts(channelId));
dispatch(updateSearchTerms(''));
dispatch(updateRhsState(RHSStates.PIN));
};
}
export function showMentions() {
return (dispatch, getState) => {
const termKeys = [...getCurrentUserMentionKeys(getState())];
const indexOfChannel = termKeys.indexOf('@channel');
if (indexOfChannel !== -1) {
termKeys.splice(indexOfChannel, 1);
}
const indexOfAll = termKeys.indexOf('@all');
if (indexOfAll !== -1) {
termKeys.splice(indexOfAll, 1);
}
const indexOfHere = termKeys.indexOf('@here');
if (indexOfHere !== -1) {
termKeys.splice(indexOfHere, 1);
}
const terms = termKeys.join(' ').trim() + ' ';
trackEvent('api', 'api_posts_search_mention');
dispatch(updateSearchTerms(terms));
dispatch(performSearch(terms, true));
dispatch(updateRhsState(RHSStates.MENTION));
};
}
export function closeRightHandSide() {
return (dispatch) => {
dispatch(updateRhsState(null));
dispatch({
type: ActionTypes.SELECT_POST,
postId: '',
channelId: ''
});
};
}
\ No newline at end of file
......@@ -383,7 +383,7 @@ function handleUserRemovedEvent(msg) {
$('#removed_from_channel').modal('show');
}
GlobalActions.toggleSideBarAction(false);
GlobalActions.emitCloseRightHandSide();
const townsquare = ChannelStore.getByName('town-square');
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
......
......@@ -9,10 +9,8 @@ import {FormattedMessage} from 'react-intl';
import 'bootstrap';
import * as GlobalActions from 'actions/global_actions.jsx';
import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx';
import * as WebrtcActions from 'actions/webrtc_actions.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import SearchStore from 'stores/search_store.jsx';
import WebrtcStore from 'stores/webrtc_store.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
......@@ -31,7 +29,7 @@ import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
import MessageWrapper from 'components/message_wrapper.jsx';
import PopoverListMembers from 'components/popover_list_members';
import RenameChannelModal from 'components/rename_channel_modal';
import NavbarSearchBox from 'components/search_bar.jsx';
import NavbarSearchBox from 'components/search_bar';
import StatusIcon from 'components/status_icon.jsx';
import ToggleModalButton from 'components/toggle_modal_button.jsx';
......@@ -49,10 +47,17 @@ export default class ChannelHeader extends React.Component {
dmUserStatus: PropTypes.object,
dmUserIsInCall: PropTypes.bool,
enableFormatting: PropTypes.bool.isRequired,
rhsState: PropTypes.oneOf(
Object.values(RHSStates)
),
actions: PropTypes.shape({
leaveChannel: PropTypes.func.isRequired,
favoriteChannel: PropTypes.func.isRequired,
unfavoriteChannel: PropTypes.func.isRequired
unfavoriteChannel: PropTypes.func.isRequired,
showFlaggedPosts: PropTypes.func.isRequired,
showPinnedPosts: PropTypes.func.isRequired,
showMentions: PropTypes.func.isRequired,
closeRightHandSide: PropTypes.func.isRequired
}).isRequired
}
......@@ -69,7 +74,6 @@ export default class ChannelHeader extends React.Component {
showEditChannelPurposeModal: false,
showMembersModal: false,
showRenameChannelModal: false,
rhsState: '',
isBusy: WebrtcStore.isBusy()
};
}
......@@ -77,29 +81,15 @@ export default class ChannelHeader extends React.Component {
componentDidMount() {
WebrtcStore.addChangedListener(this.onWebrtcChange);
WebrtcStore.addBusyListener(this.onBusy);
SearchStore.addSearchChangeListener(this.onSearchChange);
document.addEventListener('keydown', this.handleShortcut);
}
componentWillUnmount() {
WebrtcStore.removeChangedListener(this.onWebrtcChange);
WebrtcStore.removeBusyListener(this.onBusy);
SearchStore.removeSearchChangeListener(this.onSearchChange);
document.removeEventListener('keydown', this.handleShortcut);
}
onSearchChange = () => {
let rhsState = '';
if (SearchStore.isPinnedPosts) {
rhsState = RHSStates.PIN;
} else if (SearchStore.isFlaggedPosts) {
rhsState = RHSStates.FLAG;
} else if (SearchStore.isMentionSearch) {
rhsState = RHSStates.MENTION;
}
this.setState({rhsState});
}
onWebrtcChange = () => {
this.setState({isBusy: WebrtcStore.isBusy()});
}
......@@ -126,28 +116,28 @@ export default class ChannelHeader extends React.Component {
searchMentions = (e) => {
e.preventDefault();
if (this.state.rhsState === RHSStates.MENTION) {
GlobalActions.toggleSideBarAction(false);
if (this.props.rhsState === RHSStates.MENTION) {
this.props.actions.closeRightHandSide();
} else {
GlobalActions.emitSearchMentionsEvent(this.props.currentUser);
this.props.actions.showMentions();
}
}
getPinnedPosts = (e) => {
e.preventDefault();
if (this.state.rhsState === RHSStates.PIN) {
GlobalActions.toggleSideBarAction(false);
if (this.props.rhsState === RHSStates.PIN) {
this.props.actions.closeRightHandSide();
} else {
getPinnedPosts(this.props.channel.id);
this.props.actions.showPinnedPosts();
}
}
getFlagged = (e) => {
e.preventDefault();
if (this.state.rhsState === RHSStates.FLAG) {
GlobalActions.toggleSideBarAction(false);
if (this.props.rhsState === RHSStates.FLAG) {
this.props.actions.closeRightHandSide();
} else {
getFlaggedPosts();
this.props.actions.showFlaggedPosts();
}
}
......@@ -176,7 +166,7 @@ export default class ChannelHeader extends React.Component {
initWebrtc = (contactId, isOnline) => {
if (isOnline && !this.state.isBusy) {
GlobalActions.emitCloseRightHandSide();
this.props.actions.closeRightHandSide();
WebrtcActions.initWebrtc(contactId, true);
}
}
......@@ -840,7 +830,7 @@ export default class ChannelHeader extends React.Component {
}
let pinnedIconClass = 'channel-header__icon';
if (this.state.rhsState === RHSStates.PIN) {
if (this.props.rhsState === RHSStates.PIN) {
pinnedIconClass += ' active';
}
......
......@@ -12,6 +12,10 @@ import {getMyTeamMember} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser, getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import {getUserIdFromChannelName, isDefault, isFavoriteChannel} from 'mattermost-redux/utils/channel_utils';
import {showFlaggedPosts, showPinnedPosts, showMentions, closeRightHandSide} from 'actions/views/rhs';
import {getRhsState} from 'selectors/rhs';
import ChannelHeader from './channel_header.jsx';
function mapStateToProps(state, ownProps) {
......@@ -36,7 +40,8 @@ function mapStateToProps(state, ownProps) {
currentUser: user,
dmUser,
dmUserStatus,
enableFormatting: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true)
enableFormatting: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
rhsState: getRhsState(state)
};
}
......@@ -45,7 +50,11 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
leaveChannel,
favoriteChannel,
unfavoriteChannel
unfavoriteChannel,
showFlaggedPosts,
showPinnedPosts,
showMentions,
closeRightHandSide
}, dispatch)
};
}
......
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {closeRightHandSide} from 'actions/views/rhs';
import {getRhsState} from 'selectors/rhs';
import {RHSStates} from 'utils/constants.jsx';
import Navbar from './navbar.jsx';
function mapStateToProps(state) {
const rhsState = getRhsState(state);
return {
isPinnedPosts: rhsState === RHSStates.PIN
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
closeRightHandSide
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Navbar);
......@@ -9,15 +9,11 @@ import {browserHistory} from 'react-router';
import iNoBounce from 'inobounce';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
import {loadProfilesForSidebar} from 'actions/user_actions.jsx';
import {startPeriodicSync, stopPeriodicSync} from 'actions/websocket_actions.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import store from 'stores/redux_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
......@@ -41,7 +37,7 @@ import RemovedFromChannelModal from 'components/removed_from_channel_modal.jsx';
import ResetStatusModal from 'components/reset_status_modal';
import ShortcutsModal from 'components/shortcuts_modal.jsx';
import SidebarRight from 'components/sidebar_right';
import SidebarRightMenu from 'components/sidebar_right_menu.jsx';
import SidebarRightMenu from 'components/sidebar_right_menu';
import TeamSettingsModal from 'components/team_settings_modal.jsx';
import ImportThemeModal from 'components/user_settings/import_theme_modal.jsx';
import UserSettingsModal from 'components/user_settings/user_settings_modal.jsx';
......@@ -63,7 +59,6 @@ export default class NeedsTeam extends React.Component {
sidebar: PropTypes.element,
team_sidebar: PropTypes.element,
center: PropTypes.element,