* Mark category as read

* Fix lint and test

* Fix tests

* Fix test and remove wrong aria

* Address server issues and add mark as read for unreads

* Missing changes

* Fix tests

* fix tests

* Add confirmation popup to mark as read category

* Always use viewMultipleChannels and other fixes

* Remove unneeded code

* Fix test

* Address feedback

* Address feedback

* Fix tests

* Fix test

* Fix tests

* Update aria-haspopup depending on the number of channels to mark as viewed

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Daniel Espino García
2023-08-14 10:01:02 +02:00
коммит произвёл GitHub
родитель c1c07ba1bb
Коммит e9b3afecc2
60 изменённых файлов: 1151 добавлений и 1557 удалений

Просмотреть файл

@@ -174,7 +174,7 @@ describe('actions/new_post', () => {
},
},
{
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
data: {
channel_id: channelId,
},
@@ -377,7 +377,7 @@ describe('actions/new_post', () => {
},
},
{
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
data: {
channel_id: channelId,
},

Просмотреть файл

@@ -9,7 +9,6 @@ import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/pre
import {
actionsToMarkChannelAsRead,
actionsToMarkChannelAsUnread,
actionsToMarkChannelAsViewed,
markChannelAsViewedOnServer,
} from 'mattermost-redux/actions/channels';
import * as PostActions from 'mattermost-redux/actions/posts';
@@ -123,10 +122,7 @@ export function setChannelReadAndViewed(dispatch: DispatchFunc, getState: GetSta
dispatch(markChannelAsViewedOnServer(post.channel_id));
}
return [
...actionsToMarkChannelAsRead(getState, post.channel_id),
...actionsToMarkChannelAsViewed(getState, post.channel_id),
];
return actionsToMarkChannelAsRead(getState, post.channel_id);
}
return actionsToMarkChannelAsUnread(getState, websocketMessageProps.team_id, post.channel_id, websocketMessageProps.mentions, fetchedChannelMember, post.root_id === '', post?.metadata?.priority?.priority);

Просмотреть файл

@@ -27,8 +27,7 @@ import {
getChannelAndMyMember,
getMyChannelMember,
getChannelStats,
viewChannel,
markChannelAsRead,
markMultipleChannelsAsRead,
getChannelMemberCountsByGroup,
} from 'mattermost-redux/actions/channels';
import {getCloudSubscription} from 'mattermost-redux/actions/cloud';
@@ -456,8 +455,8 @@ export function handleEvent(msg) {
handleAddEmoji(msg);
break;
case SocketEvents.CHANNEL_VIEWED:
handleChannelViewedEvent(msg);
case SocketEvents.MULTIPLE_CHANNELS_VIEWED:
handleMultipleChannelsViewedEvent(msg);
break;
case SocketEvents.PLUGIN_ENABLED:
@@ -741,15 +740,6 @@ export function handlePostEditEvent(msg) {
dispatch(receivedPost(post, crtEnabled));
getProfilesAndStatusesForPosts([post], dispatch, getState);
const currentChannelId = getCurrentChannelId(getState());
// Update channel state
if (currentChannelId === msg.broadcast.channel_id) {
dispatch(getChannelStats(currentChannelId));
if (window.isActive) {
dispatch(viewChannel(currentChannelId));
}
}
}
async function handlePostDeleteEvent(msg) {
@@ -1291,11 +1281,9 @@ function handleReactionRemovedEvent(msg) {
});
}
function handleChannelViewedEvent(msg) {
// Useful for when multiple devices have the app open to different channels
if ((!window.isActive || getCurrentChannelId(getState()) !== msg.data.channel_id) &&
getCurrentUserId(getState()) === msg.broadcast.user_id) {
dispatch(markChannelAsRead(msg.data.channel_id, '', false));
function handleMultipleChannelsViewedEvent(msg) {
if (getCurrentUserId(getState()) === msg.broadcast.user_id) {
dispatch(markMultipleChannelsAsRead(msg.data.channel_times));
}
}

Просмотреть файл

@@ -11,8 +11,8 @@ import {Channel} from '@mattermost/types/channels';
import {DispatchFunc, GenericAction} from 'mattermost-redux/types/actions';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {viewChannel} from 'mattermost-redux/actions/channels';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {markChannelAsViewedOnServer, updateApproximateViewTime} from 'mattermost-redux/actions/channels';
import {getCurrentChannelId, isManuallyUnread} from 'mattermost-redux/selectors/entities/channels';
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUser, shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/users';
@@ -34,10 +34,12 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
const license = getLicense(state);
const config = getConfig(state);
const showTermsOfService = shouldShowTermsOfService(state);
const currentChannelId = getCurrentChannelId(state);
return {
currentUser: getCurrentUser(state),
currentChannelId: getCurrentChannelId(state),
currentChannelId,
isCurrentChannelManuallyUnread: isManuallyUnread(state, currentChannelId),
mfaRequired: checkIfMFARequired(getCurrentUser(state), license, config, ownProps.match.url),
enableTimezone: config.ExperimentalTimezone === 'true',
showTermsOfService,
@@ -60,7 +62,8 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
actions: bindActionCreators({
autoUpdateTimezone,
getChannelURLAction,
viewChannel,
markChannelAsViewedOnServer,
updateApproximateViewTime,
}, dispatch),
};
}

Просмотреть файл

@@ -24,8 +24,10 @@ describe('components/logged_in/LoggedIn', () => {
actions: {
autoUpdateTimezone: jest.fn(),
getChannelURLAction: jest.fn(),
viewChannel: jest.fn(),
markChannelAsViewedOnServer: jest.fn(),
updateApproximateViewTime: jest.fn(),
},
isCurrentChannelManuallyUnread: false,
showTermsOfService: false,
location: {
pathname: '/',

Просмотреть файл

@@ -29,13 +29,15 @@ declare global {
export type Props = {
currentUser?: UserProfile;
currentChannelId?: string;
isCurrentChannelManuallyUnread: boolean;
children?: React.ReactNode;
mfaRequired: boolean;
enableTimezone: boolean;
actions: {
autoUpdateTimezone: (deviceTimezone: string) => void;
getChannelURLAction: (channel: Channel, teamId: string, url: string) => void;
viewChannel: (channelId: string, prevChannelId?: string) => void;
markChannelAsViewedOnServer: (channelId: string) => void;
updateApproximateViewTime: (channelId: string) => void;
};
showTermsOfService: boolean;
location: {
@@ -226,8 +228,9 @@ export default class LoggedIn extends React.PureComponent<Props> {
private handleBeforeUnload = (): void => {
// remove the event listener to prevent getting stuck in a loop
window.removeEventListener('beforeunload', this.handleBeforeUnload);
if (document.cookie.indexOf('MMUSERID=') > -1) {
this.props.actions.viewChannel('', this.props.currentChannelId || '');
if (document.cookie.indexOf('MMUSERID=') > -1 && this.props.currentChannelId && !this.props.isCurrentChannelManuallyUnread) {
this.props.actions.updateApproximateViewTime(this.props.currentChannelId);
this.props.actions.markChannelAsViewedOnServer(this.props.currentChannelId);
}
WebSocketActions.close();
};

Просмотреть файл

@@ -7,7 +7,7 @@ import {Dispatch, bindActionCreators, ActionCreatorsMapObject} from 'redux';
import {getRecentPostsChunkInChannel, makeGetPostsChunkAroundPost, getUnreadPostsChunk, getPost, isPostsChunkIncludingUnreadsPosts, getLimitedViews} from 'mattermost-redux/selectors/entities/posts';
import {memoizeResult} from 'mattermost-redux/utils/helpers';
import {Action} from 'mattermost-redux/types/actions';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
import {makePreparePostIdsForPostList} from 'mattermost-redux/utils/post_list';
import {RequestStatus} from 'mattermost-redux/constants';
@@ -111,7 +111,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
loadPostsAround,
checkAndSetMobileView,
syncPostsInChannel,
markChannelAsViewed,
markChannelAsRead,
updateNewMessagesAtInChannel,
}, dispatch),

Просмотреть файл

@@ -17,7 +17,6 @@ const actionsProp = {
syncPostsInChannel: jest.fn().mockResolvedValue({}),
loadLatestPosts: jest.fn().mockImplementation(() => Promise.resolve({atLatestMessage: true, atOldestmessage: true})),
checkAndSetMobileView: jest.fn(),
markChannelAsViewed: jest.fn(),
markChannelAsRead: jest.fn(),
updateNewMessagesAtInChannel: jest.fn(),
toggleShouldStartFromBottomWhenUnread: jest.fn(),
@@ -250,7 +249,6 @@ describe('components/post_view/post_list', () => {
await wrapper.instance().postsOnLoad('undefined');
expect(actionsProp.markChannelAsRead).toHaveBeenCalledWith(baseProps.channelId);
expect(actionsProp.markChannelAsViewed).toHaveBeenCalledWith(baseProps.channelId);
});
test('Should not call markChannelAsReadAndViewed as it is a permalink', async () => {
const emptyPostList: string[] = [];
@@ -261,7 +259,6 @@ describe('components/post_view/post_list', () => {
await actionsProp.loadPostsAround();
expect(actionsProp.markChannelAsRead).not.toHaveBeenCalled();
expect(actionsProp.markChannelAsViewed).not.toHaveBeenCalled();
});
});
});

Просмотреть файл

@@ -141,8 +141,6 @@ export interface Props {
*/
loadLatestPosts: (channelId: string) => Promise<void>;
markChannelAsViewed: (channelId: string) => void;
markChannelAsRead: (channelId: string) => void;
updateNewMessagesAtInChannel: typeof updateNewMessagesAtInChannel;
};
@@ -230,7 +228,9 @@ export default class PostList extends React.PureComponent<Props, State> {
}
if (!focusedPostId) {
this.markChannelAsReadAndViewed(channelId);
// Posts are marked as read from here to not cause a race when loading posts
// marking channel as read and viewed after calling for posts in channel
this.props.actions.markChannelAsRead(channelId);
}
if (this.mounted) {
@@ -274,13 +274,6 @@ export default class PostList extends React.PureComponent<Props, State> {
return {error};
};
markChannelAsReadAndViewed = (channelId: string) => {
// Posts are marked as read from here to not cause a race when loading posts
// marking channel as read and viewed after calling for posts in channel
this.props.actions.markChannelAsViewed(channelId);
this.props.actions.markChannelAsRead(channelId);
};
getOldestVisiblePostId = () => {
return getOldestPostId(this.props.postListIds || []);
};

Просмотреть файл

@@ -27,18 +27,7 @@ exports[`components/sidebar should match snapshot 1`] = `
id="lhsNavigator"
role="application"
>
<Connect(ChannelNavigator)
canCreateChannel={true}
canJoinPublicChannel={true}
handleOpenDirectMessagesModal={[Function]}
invitePeopleModal={[Function]}
showCreateCategoryModal={[Function]}
showCreateUserGroupModal={[Function]}
showMoreChannelsModal={[Function]}
showNewChannelModal={[Function]}
unreadFilterEnabled={false}
userGroupsEnabled={false}
/>
<Connect(ChannelNavigator) />
</div>
<div
className="sidebar--left__icons"
@@ -81,18 +70,7 @@ exports[`components/sidebar should match snapshot when direct channels modal is
id="lhsNavigator"
role="application"
>
<Connect(ChannelNavigator)
canCreateChannel={true}
canJoinPublicChannel={true}
handleOpenDirectMessagesModal={[Function]}
invitePeopleModal={[Function]}
showCreateCategoryModal={[Function]}
showCreateUserGroupModal={[Function]}
showMoreChannelsModal={[Function]}
showNewChannelModal={[Function]}
unreadFilterEnabled={false}
userGroupsEnabled={false}
/>
<Connect(ChannelNavigator) />
</div>
<div
className="sidebar--left__icons"
@@ -139,18 +117,7 @@ exports[`components/sidebar should match snapshot when more channels modal is op
id="lhsNavigator"
role="application"
>
<Connect(ChannelNavigator)
canCreateChannel={true}
canJoinPublicChannel={true}
handleOpenDirectMessagesModal={[Function]}
invitePeopleModal={[Function]}
showCreateCategoryModal={[Function]}
showCreateUserGroupModal={[Function]}
showMoreChannelsModal={[Function]}
showNewChannelModal={[Function]}
unreadFilterEnabled={false}
userGroupsEnabled={false}
/>
<Connect(ChannelNavigator) />
</div>
<div
className="sidebar--left__icons"

Просмотреть файл

@@ -13,26 +13,11 @@ let props: Props;
describe('Components/ChannelNavigator', () => {
beforeEach(() => {
props = {
canGoForward: true,
canGoBack: true,
canJoinPublicChannel: true,
showMoreChannelsModal: jest.fn(),
showCreateUserGroupModal: jest.fn(),
invitePeopleModal: jest.fn(),
showNewChannelModal: jest.fn(),
showCreateCategoryModal: jest.fn(),
handleOpenDirectMessagesModal: jest.fn(),
unreadFilterEnabled: true,
canCreateChannel: true,
showUnreadsCategory: true,
isQuickSwitcherOpen: false,
userGroupsEnabled: false,
canCreateCustomGroups: true,
actions: {
openModal: jest.fn(),
closeModal: jest.fn(),
goBack: jest.fn(),
goForward: jest.fn(),
},
};
});

Просмотреть файл

@@ -18,26 +18,11 @@ import * as Utils from 'utils/utils';
import ChannelFilter from '../channel_filter';
export type Props = {
canGoForward: boolean;
canGoBack: boolean;
canJoinPublicChannel: boolean;
showMoreChannelsModal: () => void;
showCreateUserGroupModal: () => void;
invitePeopleModal: () => void;
showNewChannelModal: () => void;
showCreateCategoryModal: () => void;
handleOpenDirectMessagesModal: (e: Event) => void;
unreadFilterEnabled: boolean;
canCreateChannel: boolean;
showUnreadsCategory: boolean;
isQuickSwitcherOpen: boolean;
userGroupsEnabled: boolean;
canCreateCustomGroups: boolean;
actions: {
openModal: <P>(modalData: ModalData<P>) => void;
closeModal: (modalId: string) => void;
goBack: () => void;
goForward: () => void;
};
};
@@ -100,16 +85,6 @@ export default class ChannelNavigator extends React.PureComponent<Props> {
}
};
goBack = () => {
trackEvent('ui', 'ui_history_back');
this.props.actions.goBack();
};
goForward = () => {
trackEvent('ui', 'ui_history_forward');
this.props.actions.goForward();
};
render() {
return (
<div className={'SidebarChannelNavigator webapp'}>

Просмотреть файл

@@ -5,12 +5,9 @@ import {connect} from 'react-redux';
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
import {Action} from 'mattermost-redux/types/actions';
import {shouldShowUnreadsCategory, isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
import Permissions from 'mattermost-redux/constants/permissions';
import {shouldShowUnreadsCategory} from 'mattermost-redux/selectors/entities/preferences';
import {openModal, closeModal} from 'actions/views/modals';
import {getHistory} from 'utils/browser_history';
import {ModalIdentifiers} from 'utils/constants';
import {isModalOpen} from 'selectors/views/modals';
@@ -19,37 +16,16 @@ import {GlobalState} from 'types/store';
import ChannelNavigator from './channel_navigator';
// TODO: For Phase 1. Will revisit history in Phase 2
function goBack() {
return () => {
getHistory().goBack();
return {data: null};
};
}
function goForward() {
return () => {
getHistory().goForward();
return {data: null};
};
}
function mapStateToProps(state: GlobalState) {
const canCreateCustomGroups = haveISystemPermission(state, {permission: Permissions.CREATE_CUSTOM_GROUP}) && isCustomGroupsEnabled(state);
return {
canGoBack: true, // TODO: Phase 1 only
canGoForward: true,
showUnreadsCategory: shouldShowUnreadsCategory(state),
isQuickSwitcherOpen: isModalOpen(state, ModalIdentifiers.QUICK_SWITCH),
canCreateCustomGroups,
};
}
type Actions = {
openModal: <P>(modalData: ModalData<P>) => void;
closeModal: (modalId: string) => void;
goBack: () => void;
goForward: () => void;
}
function mapDispatchToProps(dispatch: Dispatch) {
@@ -57,8 +33,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
openModal,
closeModal,
goBack,
goForward,
}, dispatch),
};
}

Просмотреть файл

@@ -5,14 +5,12 @@ import {connect} from 'react-redux';
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
import {fetchMyCategories} from 'mattermost-redux/actions/channel_categories';
import {Preferences} from 'mattermost-redux/constants';
import Permissions from 'mattermost-redux/constants/permissions';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {getBool, isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {haveICurrentChannelPermission, haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {GenericAction} from 'mattermost-redux/types/actions';
import {createCategory, clearChannelSelection} from 'actions/views/channel_sidebar';
import {clearChannelSelection} from 'actions/views/channel_sidebar';
import {isUnreadFilterEnabled} from 'selectors/views/channel_sidebar';
import {closeModal, openModal} from 'actions/views/modals';
import {closeRightHandSide} from 'actions/views/rhs';
@@ -49,13 +47,6 @@ function mapStateToProps(state: GlobalState) {
canCreatePublicChannel,
canJoinPublicChannel,
isOpen: getIsLhsOpen(state),
hasSeenModal: getBool(
state,
Preferences.CATEGORY_WHATS_NEW_MODAL,
Preferences.HAS_SEEN_SIDEBAR_WHATS_NEW_MODAL,
false,
),
isCloud: getLicense(state).Cloud === 'true',
unreadFilterEnabled,
isMobileView: getIsMobileView(state),
isKeyBoardShortcutModalOpen: isModalOpen(state, ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL),
@@ -68,7 +59,6 @@ function mapStateToProps(state: GlobalState) {
type Actions = {
fetchMyCategories: (teamId: string) => {data: boolean};
createCategory: (teamId: string, categoryName: string) => {data: string};
openModal: <P>(modalData: ModalData<P>) => void;
clearChannelSelection: () => void;
closeModal: (modalId: string) => void;
@@ -79,7 +69,6 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return {
actions: bindActionCreators<ActionCreatorsMapObject, Actions>({
clearChannelSelection,
createCategory,
fetchMyCategories,
openModal,
closeModal,

Просмотреть файл

@@ -36,16 +36,13 @@ type Props = {
canCreatePrivateChannel: boolean;
canJoinPublicChannel: boolean;
isOpen: boolean;
hasSeenModal: boolean;
actions: {
fetchMyCategories: (teamId: string) => {data: boolean};
createCategory: (teamId: string, categoryName: string) => {data: string};
openModal: <P>(modalData: ModalData<P>) => void;
closeModal: (modalId: string) => void;
clearChannelSelection: () => void;
closeRightHandSide: () => void;
};
isCloud: boolean;
unreadFilterEnabled: boolean;
isMobileView: boolean;
isKeyBoardShortcutModalOpen: boolean;
@@ -148,10 +145,6 @@ export default class Sidebar extends React.PureComponent<Props, State> {
trackEvent('ui', 'ui_sidebar_menu_createCategory');
};
handleCreateCategory = (categoryName: string) => {
this.props.actions.createCategory(this.props.teamId, categoryName);
};
showMoreChannelsModal = () => {
this.props.actions.openModal({
modalId: ModalIdentifiers.MORE_CHANNELS,
@@ -265,18 +258,7 @@ export default class Sidebar extends React.PureComponent<Props, State> {
className='a11y__region'
data-a11y-sort-order='6'
>
<ChannelNavigator
showNewChannelModal={this.showNewChannelModal}
showMoreChannelsModal={this.showMoreChannelsModal}
showCreateUserGroupModal={this.showCreateUserGroupModal}
invitePeopleModal={this.invitePeopleModal}
showCreateCategoryModal={this.showCreateCategoryModal}
canCreateChannel={this.props.canCreatePrivateChannel || this.props.canCreatePublicChannel}
canJoinPublicChannel={this.props.canJoinPublicChannel}
handleOpenDirectMessagesModal={this.handleOpenMoreDirectChannelsModal}
unreadFilterEnabled={this.props.unreadFilterEnabled}
userGroupsEnabled={this.props.userGroupsEnabled}
/>
<ChannelNavigator/>
</div>
<div className='sidebar--left__icons'>
<Pluggable pluggableName='LeftSidebarHeader'/>

Просмотреть файл

@@ -33,7 +33,7 @@ exports[`components/sidebar/sidebar_category should match snapshot 2`] = `
muted={false}
onClick={[Function]}
>
<Connect(Component)
<Memo(SidebarCategoryMenu)
category={
Object {
"channel_ids": Array [
@@ -106,7 +106,7 @@ exports[`components/sidebar/sidebar_category should match snapshot when collapse
muted={false}
onClick={[Function]}
>
<Connect(Component)
<Memo(SidebarCategoryMenu)
category={
Object {
"channel_ids": Array [
@@ -187,7 +187,7 @@ exports[`components/sidebar/sidebar_category should match snapshot when isNewCat
id="sidebar_left.sidebar_category.newLabel"
/>
</div>
<Connect(Component)
<Memo(SidebarCategoryMenu)
category={
Object {
"channel_ids": Array [],
@@ -279,7 +279,7 @@ exports[`components/sidebar/sidebar_category should match snapshot when sorting
muted={false}
onClick={[Function]}
>
<Connect(Component)
<Memo(SidebarCategorySortingMenu)
category={
Object {
"channel_ids": Array [
@@ -402,7 +402,7 @@ exports[`components/sidebar/sidebar_category should match snapshot when the cate
muted={false}
onClick={[Function]}
>
<Connect(Component)
<Memo(SidebarCategorySortingMenu)
category={
Object {
"channel_ids": Array [

Просмотреть файл

@@ -0,0 +1,125 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/sidebar/sidebar_category/sidebar_category_menu should match snapshot and contain correct buttons 1`] = `
<Memo(SidebarCategoryGenericMenu)
id="test_category_id"
>
<MarkAsUnreadItem
handleViewCategory={[Function]}
id="test_category_id"
numChannels={0}
/>
<MenuItemSeparator />
<MenuItem
id="mute-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Mute Category"
id="sidebar_left.sidebar_category_menu.muteCategory"
/>
}
leadingElement={
<BellOutlineIcon
size={18}
/>
}
onClick={[Function]}
/>
<MenuItem
aria-haspopup={true}
id="rename-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Rename Category"
id="sidebar_left.sidebar_category_menu.renameCategory"
/>
}
leadingElement={
<PencilOutlineIcon
size={18}
/>
}
onClick={[Function]}
/>
<MenuItem
aria-haspopup={true}
id="delete-test_category_id"
isDestructive={true}
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Delete Category"
id="sidebar_left.sidebar_category_menu.deleteCategory"
/>
}
leadingElement={
<TrashCanOutlineIcon
size={18}
/>
}
onClick={[Function]}
/>
<MenuItemSeparator />
<SubMenu
id="sortChannels-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Sort"
id="sidebar.sort"
/>
}
leadingElement={
<SortAlphabeticalAscendingIcon
size={18}
/>
}
menuAriaLabel="Sort submenu"
menuId="sortChannels-test_category_id-menu"
trailingElements={
<React.Fragment>
<Memo(MemoizedFormattedMessage)
defaultMessage="Alphabetically"
id="user.settings.sidebar.sortAlpha"
/>
<ChevronRightIcon
size={16}
/>
</React.Fragment>
}
>
<MenuItem
id="sortAplhabetical-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Alphabetically"
id="user.settings.sidebar.sortAlpha"
/>
}
onClick={[Function]}
/>
<MenuItem
id="sortByMostRecent-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Recent Activity"
id="sidebar.sortedByRecencyLabel"
/>
}
onClick={[Function]}
/>
<MenuItem
id="sortManual-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Manually"
id="sidebar.sortedManually"
/>
}
onClick={[Function]}
/>
</SubMenu>
<MenuItemSeparator />
<CreateNewCategoryMenuItem
id="test_category_id"
/>
</Memo(SidebarCategoryGenericMenu)>
`;

Просмотреть файл

@@ -1,159 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/sidebar/sidebar_category/sidebar_category_menu should match snapshot and contain correct buttons 1`] = `
<div
className="SidebarMenu MenuWrapper"
>
<Menu
menu={
Object {
"aria-label": "Edit category menu",
"id": "SidebarChannelMenu-MenuList-test_category_id",
"onToggle": [Function],
}
}
menuButton={
Object {
"aria-label": "Category options",
"children": <DotsVerticalIcon
size={16}
/>,
"class": "SidebarMenu_menuButton",
"id": "SidebarCategoryMenu-Button-test_category_id",
}
}
menuButtonTooltip={
Object {
"class": "hidden-xs",
"id": "SidebarCategoryMenu-ButtonTooltip-test_category_id",
"text": "Category options",
}
}
>
<MenuItem
id="mute-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Mute Category"
id="sidebar_left.sidebar_category_menu.muteCategory"
/>
}
leadingElement={
<BellOutlineIcon
size={18}
/>
}
onClick={[Function]}
/>
<MenuItem
aria-haspopup={true}
id="rename-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Rename Category"
id="sidebar_left.sidebar_category_menu.renameCategory"
/>
}
leadingElement={
<PencilOutlineIcon
size={18}
/>
}
onClick={[Function]}
/>
<MenuItem
aria-haspopup={true}
id="delete-test_category_id"
isDestructive={true}
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Delete Category"
id="sidebar_left.sidebar_category_menu.deleteCategory"
/>
}
leadingElement={
<TrashCanOutlineIcon
size={18}
/>
}
onClick={[Function]}
/>
<MenuItemSeparator />
<SubMenu
id="sortChannels-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Sort"
id="sidebar.sort"
/>
}
leadingElement={
<SortAlphabeticalAscendingIcon
size={18}
/>
}
menuAriaLabel="Sort submenu"
menuId="sortChannels-test_category_id-menu"
trailingElements={
<React.Fragment>
<Memo(MemoizedFormattedMessage)
defaultMessage="Alphabetically"
id="user.settings.sidebar.sortAlpha"
/>
<ChevronRightIcon
size={16}
/>
</React.Fragment>
}
>
<MenuItem
id="sortAplhabetical-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Alphabetically"
id="user.settings.sidebar.sortAlpha"
/>
}
onClick={[Function]}
/>
<MenuItem
id="sortByMostRecent-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Recent Activity"
id="sidebar.sortedByRecencyLabel"
/>
}
onClick={[Function]}
/>
<MenuItem
id="sortManual-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Manually"
id="sidebar.sortedManually"
/>
}
onClick={[Function]}
/>
</SubMenu>
<MenuItemSeparator />
<MenuItem
aria-haspopup={true}
id="create-test_category_id"
labels={
<Memo(MemoizedFormattedMessage)
defaultMessage="Create New Category"
id="sidebar_left.sidebar_category_menu.createCategory"
/>
}
leadingElement={
<FolderPlusOutlineIcon
size={18}
/>
}
onClick={[Function]}
/>
</Menu>
</div>
`;

Просмотреть файл

@@ -0,0 +1,48 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import * as Menu from 'components/menu';
import {FolderPlusOutlineIcon} from '@mattermost/compass-icons/components';
import {FormattedMessage} from 'react-intl';
import {trackEvent} from 'actions/telemetry_actions';
import {ModalIdentifiers} from 'utils/constants';
import EditCategoryModal from 'components/edit_category_modal';
import {useDispatch} from 'react-redux';
import {openModal} from 'actions/views/modals';
type Props = {
id: string;
}
const CreateNewCategoryMenuItem = ({
id,
}: Props) => {
const dispatch = useDispatch();
const handleCreateCategory = useCallback(() => {
dispatch(openModal({
modalId: ModalIdentifiers.EDIT_CATEGORY,
dialogType: EditCategoryModal,
}));
trackEvent('ui', 'ui_sidebar_category_menu_createCategory');
}, [dispatch]);
return (
<Menu.Item
id={`create-${id}`}
onClick={handleCreateCategory}
aria-haspopup={true}
leadingElement={<FolderPlusOutlineIcon size={18}/>}
labels={(
<FormattedMessage
id='sidebar_left.sidebar_category_menu.createCategory'
defaultMessage='Create New Category'
/>
)}
/>
);
};
export default CreateNewCategoryMenuItem;

Просмотреть файл

@@ -8,7 +8,41 @@ import {CategorySorting} from '@mattermost/types/channel_categories';
import {CategoryTypes} from 'mattermost-redux/constants/channel_categories';
import SidebarCategoryMenu from './sidebar_category_menu';
import SidebarCategoryMenu from '.';
import * as redux from 'react-redux';
import CreateNewCategoryMenuItem from './create_new_category_menu_item';
const initialState = {
entities: {
preferences: {
myPreferences: {},
},
channels: {
channels: {},
channelsInTeam: {},
},
users: {
currentUserId: '',
profiles: {},
},
teams: {
currentTeamId: '',
},
general: {
config: {
ExperimentalGroupUnreadChannels: 'default_off',
},
},
},
views: {
channel: {
lastUnreadChannel: '',
},
},
};
jest.spyOn(redux, 'useSelector').mockImplementation((cb) => cb(initialState));
jest.spyOn(redux, 'useDispatch').mockReturnValue((t) => t);
describe('components/sidebar/sidebar_category/sidebar_category_menu', () => {
const categoryId = 'test_category_id';
@@ -24,9 +58,6 @@ describe('components/sidebar/sidebar_category/sidebar_category_menu', () => {
muted: false,
collapsed: false,
},
openModal: jest.fn(),
setCategoryMuted: jest.fn(),
setCategorySorting: jest.fn(),
};
test('should match snapshot and contain correct buttons', () => {
@@ -35,7 +66,7 @@ describe('components/sidebar/sidebar_category/sidebar_category_menu', () => {
);
expect(wrapper.find(`#rename-${categoryId}`)).toHaveLength(1);
expect(wrapper.find(`#create-${categoryId}`)).toHaveLength(1);
expect(wrapper.find(CreateNewCategoryMenuItem)).toHaveLength(1);
expect(wrapper.find(`#delete-${categoryId}`)).toHaveLength(1);
expect(wrapper).toMatchSnapshot();

Просмотреть файл

@@ -1,22 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect, ConnectedProps} from 'react-redux';
import {setCategoryMuted, setCategorySorting} from 'mattermost-redux/actions/channel_categories';
import {openModal} from 'actions/views/modals';
import SidebarCategoryMenu from './sidebar_category_menu';
const mapDispatchToProps = {
openModal,
setCategoryMuted,
setCategorySorting,
};
const connector = connect(null, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
export default connector(SidebarCategoryMenu);

Просмотреть файл

@@ -1,9 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState} from 'react';
import React, {memo, useCallback} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import classNames from 'classnames';
import {
BellOutlineIcon,
@@ -12,8 +11,6 @@ import {
FormatListBulletedIcon,
SortAlphabeticalAscendingIcon,
ClockOutlineIcon,
FolderPlusOutlineIcon,
DotsVerticalIcon,
ChevronRightIcon,
} from '@mattermost/compass-icons/components';
@@ -29,32 +26,46 @@ import DeleteCategoryModal from 'components/delete_category_modal';
import EditCategoryModal from 'components/edit_category_modal';
import * as Menu from 'components/menu';
import type {PropsFromRedux} from './index';
import SidebarCategoryGenericMenu from './sidebar_category_generic_menu';
import MarkAsReadMenuItem from './mark_as_read_menu_item';
import CreateNewCategoryMenuItem from './create_new_category_menu_item';
import {useDispatch, useSelector} from 'react-redux';
import {shouldShowUnreadsCategory} from 'mattermost-redux/selectors/entities/preferences';
import {setCategoryMuted, setCategorySorting} from 'mattermost-redux/actions/channel_categories';
type OwnProps = {
import {openModal} from 'actions/views/modals';
import {makeGetUnreadIdsForCategory} from 'selectors/views/channel_sidebar';
import {GlobalState} from 'types/store';
import {readMultipleChannels} from 'mattermost-redux/actions/channels';
type Props = {
category: ChannelCategory;
};
type Props = OwnProps & PropsFromRedux;
const getUnreadsIdsForCategory = makeGetUnreadIdsForCategory();
const SidebarCategoryMenu = (props: Props) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const SidebarCategoryMenu = ({
category,
}: Props) => {
const dispatch = useDispatch();
const showUnreadsCategory = useSelector(shouldShowUnreadsCategory);
const unreadsIds = useSelector((state: GlobalState) => getUnreadsIdsForCategory(state, category));
const {formatMessage} = useIntl();
let muteUnmuteCategoryMenuItem: JSX.Element | null = null;
if (props.category.type !== CategoryTypes.DIRECT_MESSAGES) {
if (category.type !== CategoryTypes.DIRECT_MESSAGES) {
function toggleCategoryMute() {
props.setCategoryMuted(props.category.id, !props.category.muted);
dispatch(setCategoryMuted(category.id, !category.muted));
}
muteUnmuteCategoryMenuItem = (
<Menu.Item
id={`mute-${props.category.id}`}
id={`mute-${category.id}`}
onClick={toggleCategoryMute}
leadingElement={<BellOutlineIcon size={18}/>}
labels={
props.category.muted ? (
category.muted ? (
<FormattedMessage
id='sidebar_left.sidebar_category_menu.unmuteCategory'
defaultMessage='Unmute Category'
@@ -72,20 +83,20 @@ const SidebarCategoryMenu = (props: Props) => {
let deleteCategoryMenuItem: JSX.Element | null = null;
let renameCategoryMenuItem: JSX.Element | null = null;
if (props.category.type === CategoryTypes.CUSTOM) {
if (category.type === CategoryTypes.CUSTOM) {
function handleDeleteCategory() {
props.openModal({
dispatch(openModal({
modalId: ModalIdentifiers.DELETE_CATEGORY,
dialogType: DeleteCategoryModal,
dialogProps: {
category: props.category,
category,
},
});
}));
}
deleteCategoryMenuItem = (
<Menu.Item
id={`delete-${props.category.id}`}
id={`delete-${category.id}`}
isDestructive={true}
aria-haspopup={true}
onClick={handleDeleteCategory}
@@ -100,19 +111,19 @@ const SidebarCategoryMenu = (props: Props) => {
);
function handleRenameCategory() {
props.openModal({
dispatch(openModal({
modalId: ModalIdentifiers.EDIT_CATEGORY,
dialogType: EditCategoryModal,
dialogProps: {
categoryId: props.category.id,
initialCategoryName: props.category.display_name,
categoryId: category.id,
initialCategoryName: category.display_name,
},
});
}));
}
renameCategoryMenuItem = (
<Menu.Item
id={`rename-${props.category.id}`}
id={`rename-${category.id}`}
onClick={handleRenameCategory}
aria-haspopup={true}
leadingElement={<PencilOutlineIcon size={18}/>}
@@ -127,7 +138,7 @@ const SidebarCategoryMenu = (props: Props) => {
}
function handleSortChannels(sorting: CategorySorting) {
props.setCategorySorting(props.category.id, sorting);
dispatch(setCategorySorting(category.id, sorting));
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
}
@@ -138,7 +149,7 @@ const SidebarCategoryMenu = (props: Props) => {
/>
);
let sortChannelsIcon = <FormatListBulletedIcon size={18}/>;
if (props.category.sorting === CategorySorting.Alphabetical) {
if (category.sorting === CategorySorting.Alphabetical) {
sortChannelsSelectedValue = (
<FormattedMessage
id='user.settings.sidebar.sortAlpha'
@@ -146,7 +157,7 @@ const SidebarCategoryMenu = (props: Props) => {
/>
);
sortChannelsIcon = <SortAlphabeticalAscendingIcon size={18}/>;
} else if (props.category.sorting === CategorySorting.Recency) {
} else if (category.sorting === CategorySorting.Recency) {
sortChannelsSelectedValue = (
<FormattedMessage
id='user.settings.sidebar.recent'
@@ -158,7 +169,7 @@ const SidebarCategoryMenu = (props: Props) => {
const sortChannelsMenuItem = (
<Menu.SubMenu
id={`sortChannels-${props.category.id}`}
id={`sortChannels-${category.id}`}
leadingElement={sortChannelsIcon}
labels={(
<FormattedMessage
@@ -172,11 +183,11 @@ const SidebarCategoryMenu = (props: Props) => {
<ChevronRightIcon size={16}/>
</>
)}
menuId={`sortChannels-${props.category.id}-menu`}
menuId={`sortChannels-${category.id}-menu`}
menuAriaLabel={formatMessage({id: 'sidebar_left.sidebar_category_menu.sort.dropdownAriaLabel', defaultMessage: 'Sort submenu'})}
>
<Menu.Item
id={`sortAplhabetical-${props.category.id}`}
id={`sortAplhabetical-${category.id}`}
labels={(
<FormattedMessage
id='user.settings.sidebar.sortAlpha'
@@ -186,7 +197,7 @@ const SidebarCategoryMenu = (props: Props) => {
onClick={() => handleSortChannels(CategorySorting.Alphabetical)}
/>
<Menu.Item
id={`sortByMostRecent-${props.category.id}`}
id={`sortByMostRecent-${category.id}`}
labels={(
<FormattedMessage
id='sidebar.sortedByRecencyLabel'
@@ -196,7 +207,7 @@ const SidebarCategoryMenu = (props: Props) => {
onClick={() => handleSortChannels(CategorySorting.Recency)}
/>
<Menu.Item
id={`sortManual-${props.category.id}`}
id={`sortManual-${category.id}`}
labels={(
<FormattedMessage
id='sidebar.sortedManually'
@@ -208,71 +219,33 @@ const SidebarCategoryMenu = (props: Props) => {
</Menu.SubMenu>
);
function handleCreateCategory() {
props.openModal({
modalId: ModalIdentifiers.EDIT_CATEGORY,
dialogType: EditCategoryModal,
});
trackEvent('ui', 'ui_sidebar_category_menu_createCategory');
}
const handleViewCategory = useCallback(() => {
dispatch(readMultipleChannels(unreadsIds));
trackEvent('ui', 'ui_sidebar_category_menu_viewCategory');
}, [dispatch, unreadsIds]);
const createNewCategoryMenuItem = (
<Menu.Item
id={`create-${props.category.id}`}
onClick={handleCreateCategory}
aria-haspopup={true}
leadingElement={<FolderPlusOutlineIcon size={18}/>}
labels={(
<FormattedMessage
id='sidebar_left.sidebar_category_menu.createCategory'
defaultMessage='Create New Category'
/>
)}
/>
);
function handleMenuToggle(isOpen: boolean) {
setIsMenuOpen(isOpen);
}
const markAsReadMenuItem = showUnreadsCategory ?
null :
(
<MarkAsReadMenuItem
id={category.id}
handleViewCategory={handleViewCategory}
numChannels={unreadsIds.length}
/>
);
return (
<div
className={classNames(
'SidebarMenu',
'MenuWrapper',
{
'MenuWrapper--open': isMenuOpen,
menuOpen: isMenuOpen,
},
)}
>
<Menu.Container
menuButton={{
id: `SidebarCategoryMenu-Button-${props.category.id}`,
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
class: 'SidebarMenu_menuButton',
children: <DotsVerticalIcon size={16}/>,
}}
menuButtonTooltip={{
id: `SidebarCategoryMenu-ButtonTooltip-${props.category.id}`,
text: formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
class: 'hidden-xs',
}}
menu={{
id: `SidebarChannelMenu-MenuList-${props.category.id}`,
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.dropdownAriaLabel', defaultMessage: 'Edit category menu'}),
onToggle: handleMenuToggle,
}}
>
{muteUnmuteCategoryMenuItem}
{renameCategoryMenuItem}
{deleteCategoryMenuItem}
<Menu.Separator/>
{sortChannelsMenuItem}
<Menu.Separator/>
{createNewCategoryMenuItem}
</Menu.Container>
</div>
<SidebarCategoryGenericMenu id={category.id}>
{markAsReadMenuItem}
{markAsReadMenuItem && <Menu.Separator/>}
{muteUnmuteCategoryMenuItem}
{renameCategoryMenuItem}
{deleteCategoryMenuItem}
<Menu.Separator/>
{sortChannelsMenuItem}
<Menu.Separator/>
<CreateNewCategoryMenuItem id={category.id}/>
</SidebarCategoryGenericMenu>
);
};

Просмотреть файл

@@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import {GenericModal} from '@mattermost/components';
import 'components/category_modal.scss';
type Props = {
handleConfirm: () => void;
numChannels: number;
onExited: () => void;
};
const handleCancel = () => null;
const MarkAsReadConfirmModal = ({
handleConfirm,
numChannels,
onExited,
}: Props) => {
const intl = useIntl();
const header = intl.formatMessage({id: 'mark_as_read_confirm_modal.header', defaultMessage: 'Mark as read'});
const body = intl.formatMessage({id: 'mark_as_read_confirm_modal.body', defaultMessage: 'Are you sure you want to mark {numChannels} channels as read?'}, {numChannels});
const confirm = intl.formatMessage({id: 'mark_as_read_confirm_modal.confirm', defaultMessage: 'Mark as read'});
return (
<GenericModal
ariaLabel={header}
modalHeaderText={header}
handleConfirm={handleConfirm}
handleCancel={handleCancel}
onExited={onExited}
confirmButtonText={confirm}
>
<span className='mark-as-read__helpText'>
{body}
</span>
</GenericModal>
);
};
export default MarkAsReadConfirmModal;

Просмотреть файл

@@ -0,0 +1,63 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import * as Menu from 'components/menu';
import {MarkAsUnreadIcon} from '@mattermost/compass-icons/components';
import {FormattedMessage} from 'react-intl';
import {useDispatch} from 'react-redux';
import {openModal} from 'actions/views/modals';
import {ModalIdentifiers} from 'utils/constants';
import MarkAsReadConfirmModal from './mark_as_read_confirm_modal';
type Props = ({
id: string;
handleViewCategory: () => void;
numChannels: number;
})
const MarkAsUnreadItem = ({
id,
handleViewCategory,
numChannels,
}: Props) => {
const dispatch = useDispatch();
const onClick = useCallback(() => {
if (numChannels <= 0) {
return;
}
if (numChannels === 1) {
handleViewCategory();
return;
}
dispatch(openModal({
modalId: ModalIdentifiers.DELETE_CATEGORY,
dialogType: MarkAsReadConfirmModal,
dialogProps: {
handleConfirm: handleViewCategory,
numChannels,
},
}));
}, [dispatch, handleViewCategory, numChannels]);
return (
<Menu.Item
id={`view-${id}`}
onClick={onClick}
aria-haspopup={numChannels > 1}
leadingElement={<MarkAsUnreadIcon size={18}/>}
labels={(
<FormattedMessage
id='sidebar_left.sidebar_category_menu.viewCategory'
defaultMessage='Mark category as read'
/>
)}
/>
);
};
export default MarkAsUnreadItem;

Просмотреть файл

@@ -0,0 +1,66 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState} from 'react';
import {useIntl} from 'react-intl';
import classNames from 'classnames';
import {
DotsVerticalIcon,
} from '@mattermost/compass-icons/components';
import * as Menu from 'components/menu';
type Props = {
id: string;
children: React.ReactNode[];
};
const SidebarCategoryGenericMenu = ({
id,
children,
}: Props) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const {formatMessage} = useIntl();
function handleMenuToggle(isOpen: boolean) {
setIsMenuOpen(isOpen);
}
return (
<div
className={classNames(
'SidebarMenu',
'MenuWrapper',
{
'MenuWrapper--open': isMenuOpen,
menuOpen: isMenuOpen,
},
)}
>
<Menu.Container
menuButton={{
id: `SidebarCategoryMenu-Button-${id}`,
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
class: 'SidebarMenu_menuButton',
children: <DotsVerticalIcon size={16}/>,
}}
menuButtonTooltip={{
id: `SidebarCategoryMenu-ButtonTooltip-${id}`,
text: formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
class: 'hidden-xs',
}}
menu={{
id: `SidebarChannelMenu-MenuList-${id}`,
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.dropdownAriaLabel', defaultMessage: 'Edit category menu'}),
onToggle: handleMenuToggle,
}}
>
{children}
</Menu.Container>
</div>
);
};
export default memo(SidebarCategoryGenericMenu);

Просмотреть файл

@@ -2,21 +2,35 @@
// See LICENSE.txt for license information.
import React from 'react';
import * as redux from 'react-redux';
import {shallow} from 'enzyme';
import {TestHelper} from 'utils/test_helper';
import Constants from 'utils/constants';
import SidebarCategorySortingMenu from './sidebar_category_sorting_menu';
const initialState = {
entities: {
users: {
currentUserId: 'user_id',
},
preferences: {
myPreferences: {
'sidebar_settings--limit_visible_dms_gms': {
value: '10',
},
},
},
},
};
jest.spyOn(redux, 'useSelector').mockImplementation((cb) => cb(initialState));
jest.spyOn(redux, 'useDispatch').mockReturnValue((t) => t);
describe('components/sidebar/sidebar_category/sidebar_category_sorting_menu', () => {
const baseProps = {
category: TestHelper.getCategoryMock(),
handleOpenDirectMessagesModal: jest.fn(),
selectedDmNumber: Constants.DM_AND_GM_SHOW_COUNTS[0],
currentUserId: TestHelper.getUserMock().id,
setCategorySorting: jest.fn(),
savePreferences: jest.fn(),
};
test('should match snapshot', () => {

Просмотреть файл

@@ -24,21 +24,30 @@ import {trackEvent} from 'actions/telemetry_actions';
import * as Menu from 'components/menu';
import type {PropsFromRedux} from './index';
import {useDispatch, useSelector} from 'react-redux';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getVisibleDmGmLimit} from 'mattermost-redux/selectors/entities/preferences';
import {setCategorySorting} from 'mattermost-redux/actions/channel_categories';
import {savePreferences} from 'mattermost-redux/actions/preferences';
type OwnProps = {
type Props = {
category: ChannelCategory;
handleOpenDirectMessagesModal: (e: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) => void;
};
type Props = OwnProps & PropsFromRedux;
const SidebarCategorySortingMenu = (props: Props) => {
const SidebarCategorySortingMenu = ({
category,
handleOpenDirectMessagesModal,
}: Props) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const {formatMessage} = useIntl();
const dispatch = useDispatch();
const selectedDmNumber = useSelector(getVisibleDmGmLimit);
const currentUserId = useSelector(getCurrentUserId);
function handleSortDirectMessages(sorting: CategorySorting) {
props.setCategorySorting(props.category.id, sorting);
dispatch(setCategorySorting(category.id, sorting));
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
}
@@ -49,7 +58,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
defaultMessage='Recent Activity'
/>
);
if (props.category.sorting === CategorySorting.Alphabetical) {
if (category.sorting === CategorySorting.Alphabetical) {
sortDirectMessagesSelectedValue = (
<FormattedMessage
id='user.settings.sidebar.sortAlpha'
@@ -61,7 +70,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
const sortDirectMessagesMenuItem = (
<Menu.SubMenu
id={`sortDirectMessages-${props.category.id}`}
id={`sortDirectMessages-${category.id}`}
leadingElement={sortDirectMessagesIcon}
labels={(
<FormattedMessage
@@ -75,10 +84,10 @@ const SidebarCategorySortingMenu = (props: Props) => {
<ChevronRightIcon size={16}/>
</>
}
menuId={`sortDirectMessages-${props.category.id}-menu`}
menuId={`sortDirectMessages-${category.id}-menu`}
>
<Menu.Item
id={`sortAlphabetical-${props.category.id}`}
id={`sortAlphabetical-${category.id}`}
labels={(
<FormattedMessage
id='user.settings.sidebar.sortAlpha'
@@ -88,7 +97,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
onClick={() => handleSortDirectMessages(CategorySorting.Alphabetical)}
/>
<Menu.Item
id={`sortByMostRecent-${props.category.id}`}
id={`sortByMostRecent-${category.id}`}
labels={(
<FormattedMessage
id='sidebar.sortedByRecencyLabel'
@@ -102,16 +111,16 @@ const SidebarCategorySortingMenu = (props: Props) => {
);
function handlelimitVisibleDMsGMs(number: number) {
props.savePreferences(props.currentUserId, [{
user_id: props.currentUserId,
dispatch(savePreferences(currentUserId, [{
user_id: currentUserId,
category: Constants.Preferences.CATEGORY_SIDEBAR_SETTINGS,
name: Preferences.LIMIT_VISIBLE_DMS_GMS,
value: number.toString(),
}]);
}]));
}
let showMessagesCountSelectedValue = <span>{props.selectedDmNumber}</span>;
if (props.selectedDmNumber === 10000) {
let showMessagesCountSelectedValue = <span>{selectedDmNumber}</span>;
if (selectedDmNumber === 10000) {
showMessagesCountSelectedValue = (
<FormattedMessage
id='channel_notifications.levels.all'
@@ -122,7 +131,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
const showMessagesCountMenuItem = (
<Menu.SubMenu
id={`showMessagesCount-${props.category.id}`}
id={`showMessagesCount-${category.id}`}
leadingElement={<AccountMultipleOutlineIcon size={18}/>}
labels={(
<FormattedMessage
@@ -136,10 +145,10 @@ const SidebarCategorySortingMenu = (props: Props) => {
<ChevronRightIcon size={16}/>
</>
)}
menuId={`showMessagesCount-${props.category.id}-menu`}
menuId={`showMessagesCount-${category.id}-menu`}
>
<Menu.Item
id={`showAllDms-${props.category.id}`}
id={`showAllDms-${category.id}`}
labels={(
<FormattedMessage
id='sidebar.allDirectMessages'
@@ -151,8 +160,8 @@ const SidebarCategorySortingMenu = (props: Props) => {
<Menu.Separator/>
{Constants.DM_AND_GM_SHOW_COUNTS.map((dmGmShowCount) => (
<Menu.Item
id={`showDmCount-${props.category.id}-${dmGmShowCount}`}
key={`showDmCount-${props.category.id}-${dmGmShowCount}`}
id={`showDmCount-${category.id}-${dmGmShowCount}`}
key={`showDmCount-${category.id}-${dmGmShowCount}`}
labels={<span>{dmGmShowCount}</span>}
onClick={() => handlelimitVisibleDMsGMs(dmGmShowCount)}
/>
@@ -163,8 +172,8 @@ const SidebarCategorySortingMenu = (props: Props) => {
const openDirectMessageMenuItem = (
<Menu.Item
id={`openDirectMessage-${props.category.id}`}
onClick={props.handleOpenDirectMessagesModal}
id={`openDirectMessage-${category.id}`}
onClick={handleOpenDirectMessagesModal}
leadingElement={<AccountPlusOutlineIcon size={18}/>}
labels={(
<FormattedMessage
@@ -190,18 +199,18 @@ const SidebarCategorySortingMenu = (props: Props) => {
>
<Menu.Container
menuButton={{
id: `SidebarCategorySortingMenu-Button-${props.category.id}`,
id: `SidebarCategorySortingMenu-Button-${category.id}`,
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
class: 'SidebarMenu_menuButton sortingMenu',
children: <DotsVerticalIcon size={16}/>,
}}
menuButtonTooltip={{
id: `SidebarCategorySortingMenu-ButtonTooltip-${props.category.id}`,
id: `SidebarCategorySortingMenu-ButtonTooltip-${category.id}`,
text: formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
class: 'hidden-xs',
}}
menu={{
id: `SidebarCategorySortingMenu-MenuList-${props.category.id}`,
id: `SidebarCategorySortingMenu-MenuList-${category.id}`,
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.dropdownAriaLabel', defaultMessage: 'Edit category menu'}),
onToggle: handleMenuToggle,
}}

Просмотреть файл

@@ -1,31 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect, ConnectedProps} from 'react-redux';
import {setCategorySorting} from 'mattermost-redux/actions/channel_categories';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getVisibleDmGmLimit} from 'mattermost-redux/selectors/entities/preferences';
import {GlobalState} from 'types/store';
import SidebarCategorySortingMenu from './sidebar_category_sorting_menu';
function mapStateToProps(state: GlobalState) {
return {
selectedDmNumber: getVisibleDmGmLimit(state),
currentUserId: getCurrentUserId(state),
};
}
const mapDispatchToProps = {
setCategorySorting,
savePreferences,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
export default connector(SidebarCategorySortingMenu);

Просмотреть файл

@@ -37,7 +37,7 @@ const SidebarChannelMenu = (props: Props) => {
let markAsReadUnreadMenuItem: JSX.Element | null = null;
if (props.isUnread) {
function handleMarkAsRead() {
props.markChannelAsRead(props.channel.id);
props.markChannelAsRead(props.channel.id, true);
trackEvent('ui', 'ui_sidebar_channel_menu_markAsRead');
}

Просмотреть файл

@@ -17,7 +17,6 @@ import {
setDraggingState,
stopDragging,
clearChannelSelection,
multiSelectChannelAdd,
} from 'actions/views/channel_sidebar';
import {close, switchToLhsStaticPage} from 'actions/views/lhs';
import {
@@ -68,7 +67,6 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
setDraggingState,
stopDragging,
clearChannelSelection,
multiSelectChannelAdd,
switchToLhsStaticPage,
}, dispatch),
};

Просмотреть файл

@@ -99,7 +99,6 @@ type Props = {
setDraggingState: (data: DraggingState) => void;
stopDragging: () => void;
clearChannelSelection: () => void;
multiSelectChannelAdd: (channelId: string) => void;
};
};

Просмотреть файл

@@ -0,0 +1,74 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {SidebarCategoryHeaderStatic} from './sidebar_category_header';
import SidebarChannel from './sidebar_channel';
import SidebarCategoryGenericMenu from './sidebar_category/sidebar_category_menu/sidebar_category_generic_menu';
import MarkAsReadMenuItem from './sidebar_category/sidebar_category_menu/mark_as_read_menu_item';
import * as Menu from 'components/menu';
import CreateNewCategoryMenuItem from './sidebar_category/sidebar_category_menu/create_new_category_menu_item';
import {trackEvent} from 'actions/telemetry_actions';
import {useDispatch, useSelector} from 'react-redux';
import {getUnreadChannels} from 'selectors/views/channel_sidebar';
import {readMultipleChannels} from 'mattermost-redux/actions/channels';
type Props = {
setChannelRef: (channelId: string, ref: HTMLLIElement) => void;
};
export default function UnreadChannels({
setChannelRef,
}: Props) {
const intl = useIntl();
const unreadChannels = useSelector(getUnreadChannels);
const dispatch = useDispatch();
const handleViewCategory = useCallback(() => {
if (!unreadChannels.length) {
return;
}
dispatch(readMultipleChannels(unreadChannels.map((v) => v.id)));
trackEvent('ui', 'ui_sidebar_category_menu_viewUnreadCategory');
}, [unreadChannels, dispatch]);
return (
<div className='SidebarChannelGroup dropDisabled a11y__section'>
<SidebarCategoryHeaderStatic displayName={intl.formatMessage({id: 'sidebar.types.unreads', defaultMessage: 'UNREADS'})}>
<SidebarCategoryGenericMenu id='unreads'>
<MarkAsReadMenuItem
id={'unreads'}
handleViewCategory={handleViewCategory}
numChannels={unreadChannels.length}
/>
<Menu.Separator/>
<CreateNewCategoryMenuItem id={'unreads'}/>
</SidebarCategoryGenericMenu>
</SidebarCategoryHeaderStatic>
<div className='SidebarChannelGroup_content'>
<ul
role='list'
className='NavGroupContent'
>
{unreadChannels.map((channel, index) => {
return (
<SidebarChannel
key={channel.id}
channelIndex={index}
channelId={channel.id}
setChannelRef={setChannelRef}
isCategoryCollapsed={false}
isCategoryDragged={false}
isDraggable={false}
isAutoSortedCategory={true}
/>
);
})}
</ul>
</div>
</div>
);
}

Просмотреть файл

@@ -1,18 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getUnreadChannels} from 'selectors/views/channel_sidebar';
import {GlobalState} from 'types/store';
import UnreadChannels from './unread_channels';
function mapStateToProps(state: GlobalState) {
return {
unreadChannels: getUnreadChannels(state),
};
}
export default connect(mapStateToProps)(UnreadChannels);

Просмотреть файл

@@ -1,50 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import {Channel} from '@mattermost/types/channels';
import {SidebarCategoryHeaderStatic} from '../sidebar_category_header';
import SidebarChannel from '../sidebar_channel';
type Props = {
setChannelRef: (channelId: string, ref: HTMLLIElement) => void;
unreadChannels: Channel[];
};
export default function UnreadChannels(props: Props) {
const intl = useIntl();
if (props.unreadChannels.length === 0) {
return null;
}
return (
<div className='SidebarChannelGroup dropDisabled a11y__section'>
<SidebarCategoryHeaderStatic displayName={intl.formatMessage({id: 'sidebar.types.unreads', defaultMessage: 'UNREADS'})}/>
<div className='SidebarChannelGroup_content'>
<ul
role='list'
className='NavGroupContent'
>
{props.unreadChannels.map((channel, index) => {
return (
<SidebarChannel
key={channel.id}
channelIndex={index}
channelId={channel.id}
setChannelRef={props.setChannelRef}
isCategoryCollapsed={false}
isCategoryDragged={false}
isDraggable={false}
isAutoSortedCategory={true}
/>
);
})}
</ul>
</div>
</div>
);
}

Просмотреть файл

@@ -4,7 +4,7 @@
import {connect, ConnectedProps} from 'react-redux';
import {RouteComponentProps} from 'react-router-dom';
import {fetchAllMyTeamsChannelsAndChannelMembersREST, fetchMyChannelsAndMembersREST, viewChannel} from 'mattermost-redux/actions/channels';
import {fetchAllMyTeamsChannelsAndChannelMembersREST, fetchMyChannelsAndMembersREST} from 'mattermost-redux/actions/channels';
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeamId, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
@@ -39,7 +39,6 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
const disableRefetchingOnBrowserFocus = config.DisableRefetchingOnBrowserFocus === 'true';
return {
currentUser,
currentTeamId: getCurrentTeamId(state),
currentChannelId: getCurrentChannelId(state),
teamsList: getMyTeams(state),
@@ -55,7 +54,6 @@ const mapDispatchToProps = {
fetchChannelsAndMembers,
fetchMyChannelsAndMembersREST,
fetchAllMyTeamsChannelsAndChannelMembersREST,
viewChannel,
markChannelAsReadOnFocus,
initializeTeam,
joinTeam,

Просмотреть файл

@@ -108,10 +108,6 @@ function TeamController(props: Props) {
function handleBlur() {
window.isActive = false;
blurTime.current = Date.now();
if (props.currentUser) {
props.viewChannel('');
}
}
function handleKeydown(event: KeyboardEvent) {
@@ -137,7 +133,7 @@ function TeamController(props: Props) {
window.removeEventListener('blur', handleBlur);
window.removeEventListener('keydown', handleKeydown);
};
}, [props.selectedThreadId, props.graphQLEnabled, props.currentChannelId, props.currentTeamId, props.currentUser.id]);
}, [props.selectedThreadId, props.graphQLEnabled, props.currentChannelId, props.currentTeamId]);
// Effect runs on mount, adds active state to window
useEffect(() => {

Просмотреть файл

@@ -3922,6 +3922,9 @@
"mark_all_threads_as_read_modal.confirm": "Mark all as read",
"mark_all_threads_as_read_modal.description": "This will clear the unread state and mention badges on all your threads. Are you sure?",
"mark_all_threads_as_read_modal.title": "Mark all your threads as read?",
"mark_as_read_confirm_modal.body": "Are you sure you want to mark {numChannels} channels as read?",
"mark_as_read_confirm_modal.confirm": "Mark as read",
"mark_as_read_confirm_modal.header": "Mark as read",
"marketplace_command.disabled": "The marketplace is disabled. Please contact your System Administrator for details.",
"marketplace_command.no_permission": "You do not have the appropriate permissions to access the marketplace.",
"marketplace_modal_list.no_plugins_filter": "No results for \"{filter}\"",
@@ -4798,6 +4801,7 @@
"sidebar_left.sidebar_category_menu.renameCategory": "Rename Category",
"sidebar_left.sidebar_category_menu.sort.dropdownAriaLabel": "Sort submenu",
"sidebar_left.sidebar_category_menu.unmuteCategory": "Unmute Category",
"sidebar_left.sidebar_category_menu.viewCategory": "Mark category as read",
"sidebar_left.sidebar_category.newDropBoxLabel": "Drag channels here...",
"sidebar_left.sidebar_category.newLabel": "new",
"sidebar_left.sidebar_channel_menu.addMembers": "Add Members",

Просмотреть файл

@@ -637,364 +637,6 @@ describe('Actions.Channels', () => {
}
});
describe('viewChannel', () => {
test('should contact server and update last_viewed_at of both channels', async () => {
const channelId = TestHelper.generateId();
const prevChannelId = TestHelper.generateId();
const currentUserId = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
myMembers: {
[channelId]: {
channel_id: channelId,
last_viewed_at: 1000,
roles: '',
},
[prevChannelId]: {
channel_id: prevChannelId,
last_viewed_at: 1000,
roles: '',
},
},
},
users: {
currentUserId,
},
},
});
nock(Client4.getBaseRoute()).
post('/channels/members/me/view', {channel_id: channelId, prev_channel_id: prevChannelId, collapsed_threads_supported: true}).
reply(200, OK_RESPONSE);
const now = Date.now();
const result = await store.dispatch(Actions.viewChannel(channelId, prevChannelId));
expect(result).toEqual({data: true});
const state = store.getState();
expect(state.entities.channels.myMembers[channelId].last_viewed_at).toBeGreaterThan(now);
expect(state.entities.channels.myMembers[prevChannelId].last_viewed_at).toBeGreaterThan(now);
});
test('should clear manually unread state from current channel', async () => {
const channelId = TestHelper.generateId();
const currentUserId = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
manuallyUnread: {
[channelId]: true,
},
myMembers: {
[channelId]: {
channel_id: channelId,
last_viewed_at: 1000,
roles: '',
},
},
},
users: {
currentUserId,
},
},
});
nock(Client4.getBaseRoute()).
post('/channels/members/me/view', {channel_id: channelId, prev_channel_id: '', collapsed_threads_supported: true}).
reply(200, OK_RESPONSE);
const result = await store.dispatch(Actions.viewChannel(channelId));
expect(result).toEqual({data: true});
const state = store.getState();
expect(state.entities.channels.manuallyUnread[channelId]).not.toBe(true);
});
test('should not update last_viewed_at of previous channel if it is manually marked as unread', async () => {
const channelId = TestHelper.generateId();
const prevChannelId = TestHelper.generateId();
const currentUserId = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
manuallyUnread: {
[prevChannelId]: true,
},
myMembers: {
[channelId]: {
channel_id: channelId,
last_viewed_at: 1000,
roles: '',
},
[prevChannelId]: {
channel_id: prevChannelId,
last_viewed_at: 1000,
roles: '',
},
},
},
users: {
currentUserId,
},
},
});
nock(Client4.getBaseRoute()).
post('/channels/members/me/view', {channel_id: channelId, prev_channel_id: '', collapsed_threads_supported: true}).
reply(200, OK_RESPONSE);
const now = Date.now();
const result = await store.dispatch(Actions.viewChannel(channelId, prevChannelId));
expect(result).toEqual({data: true});
const state = store.getState();
expect(state.entities.channels.myMembers[channelId].last_viewed_at).toBeGreaterThan(now);
expect(state.entities.channels.myMembers[prevChannelId].last_viewed_at).toBe(1000);
});
});
it('markChannelAsViewed', async () => {
nock(Client4.getBaseRoute()).
post('/channels').
reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam!.id));
const userChannel = await Client4.createChannel(
TestHelper.fakeChannel(TestHelper.basicTeam!.id),
);
nock(Client4.getBaseRoute()).
get(`/users/me/teams/${TestHelper.basicTeam!.id}/channels`).
query(true).
reply(200, [userChannel, TestHelper.basicChannel]);
nock(Client4.getBaseRoute()).
get(`/users/me/teams/${TestHelper.basicTeam!.id}/channels/members`).
reply(200, [{user_id: TestHelper.basicUser!.id, roles: 'channel_user', channel_id: userChannel.id}, TestHelper.basicChannelMember]);
await store.dispatch(Actions.fetchMyChannelsAndMembersREST(TestHelper.basicTeam!.id));
const timestamp = Date.now();
let members = store.getState().entities.channels.myMembers;
let member = members[TestHelper.basicChannel!.id];
const otherMember = members[userChannel.id];
expect(member).toBeTruthy();
expect(otherMember).toBeTruthy();
await TestHelper.wait(50);
await store.dispatch(Actions.markChannelAsViewed(TestHelper.basicChannel!.id));
members = store.getState().entities.channels.myMembers;
member = members[TestHelper.basicChannel!.id];
expect(member.last_viewed_at > timestamp).toBeTruthy();
});
describe('markChannelAsUnread', () => {
it('plain message', () => {
const teamId = TestHelper.generateId();
const channelId = TestHelper.generateId();
const userId = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
channels: {
[channelId]: {team_id: teamId},
},
messageCounts: {
[channelId]: {total: 10},
},
myMembers: {
[channelId]: {msg_count: 10, mention_count: 0},
},
},
teams: {
myMembers: {
[teamId]: {msg_count: 0, mention_count: 0},
},
},
users: {
currentUserId: userId,
},
},
});
store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [TestHelper.generateId()], false));
const state = store.getState();
expect(state.entities.channels.messageCounts[channelId].total).toEqual(11);
expect(state.entities.channels.myMembers[channelId].msg_count).toEqual(10);
expect(state.entities.channels.myMembers[channelId].mention_count).toEqual(0);
expect(state.entities.teams.myMembers[teamId].msg_count).toEqual(1);
expect(state.entities.teams.myMembers[teamId].mention_count).toEqual(0);
});
it('message mentioning current user', () => {
const teamId = TestHelper.generateId();
const channelId = TestHelper.generateId();
const userId = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
channels: {
[channelId]: {team_id: teamId},
},
messageCounts: {
[channelId]: {total: 10},
},
myMembers: {
[channelId]: {msg_count: 10, mention_count: 0},
},
},
teams: {
myMembers: {
[teamId]: {msg_count: 0, mention_count: 0},
},
},
users: {
currentUserId: userId,
},
},
});
store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [userId], false));
const state = store.getState();
expect(state.entities.channels.messageCounts[channelId].total).toEqual(11);
expect(state.entities.channels.myMembers[channelId].msg_count).toEqual(10);
expect(state.entities.channels.myMembers[channelId].mention_count).toEqual(1);
expect(state.entities.teams.myMembers[teamId].msg_count).toEqual(1);
expect(state.entities.teams.myMembers[teamId].mention_count).toEqual(1);
});
it('plain message with mark_unread="mention"', () => {
const teamId = TestHelper.generateId();
const channelId = TestHelper.generateId();
const userId = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
channels: {
[channelId]: {team_id: teamId},
},
messageCounts: {
[channelId]: {total: 10},
},
myMembers: {
[channelId]: {msg_count: 10, mention_count: 0, notify_props: {mark_unread: MarkUnread.MENTION}},
},
},
teams: {
myMembers: {
[teamId]: {msg_count: 0, mention_count: 0},
},
},
users: {
currentUserId: userId,
},
},
});
store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [TestHelper.generateId()], false));
const state = store.getState();
expect(state.entities.channels.messageCounts[channelId].total).toEqual(11);
expect(state.entities.channels.myMembers[channelId].msg_count).toEqual(11);
expect(state.entities.channels.myMembers[channelId].mention_count).toEqual(0);
expect(state.entities.teams.myMembers[teamId].msg_count).toEqual(0);
expect(state.entities.teams.myMembers[teamId].mention_count).toEqual(0);
});
it('message mentioning current user with mark_unread="mention"', () => {
const teamId = TestHelper.generateId();
const channelId = TestHelper.generateId();
const userId = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
channels: {
[channelId]: {team_id: teamId},
},
messageCounts: {
[channelId]: {total: 10},
},
myMembers: {
[channelId]: {msg_count: 10, mention_count: 0, notify_props: {mark_unread: MarkUnread.MENTION}},
},
},
teams: {
myMembers: {
[teamId]: {msg_count: 0, mention_count: 0},
},
},
users: {
currentUserId: userId,
},
},
});
store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [userId], false));
const state = store.getState();
expect(state.entities.channels.messageCounts[channelId].total).toEqual(11);
expect(state.entities.channels.myMembers[channelId].msg_count).toEqual(11);
expect(state.entities.channels.myMembers[channelId].mention_count).toEqual(1);
expect(state.entities.teams.myMembers[teamId].msg_count).toEqual(0);
expect(state.entities.teams.myMembers[teamId].mention_count).toEqual(1);
});
it('channel member should not be updated if it has already been fetched', () => {
const teamId = TestHelper.generateId();
const channelId = TestHelper.generateId();
const userId = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
channels: {
[channelId]: {team_id: teamId},
},
messageCounts: {
[channelId]: {total: 8},
},
myMembers: {
[channelId]: {msg_count: 5, mention_count: 2},
},
},
teams: {
myMembers: {
[teamId]: {msg_count: 2, mention_count: 1},
},
},
users: {
currentUserId: userId,
},
},
});
store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [userId], true));
const state = store.getState();
expect(state.entities.channels.messageCounts[channelId].total).toEqual(8);
expect(state.entities.channels.myMembers[channelId].msg_count).toEqual(5);
expect(state.entities.channels.myMembers[channelId].mention_count).toEqual(2);
expect(state.entities.teams.myMembers[teamId].msg_count).toEqual(3);
expect(state.entities.teams.myMembers[teamId].mention_count).toEqual(2);
});
});
describe('markChannelAsRead', () => {
it('one read channel', async () => {
const channelId = TestHelper.generateId();
@@ -1199,136 +841,6 @@ describe('Actions.Channels', () => {
expect(state.entities.teams.myMembers[teamId].msg_count).toBe(3);
});
it('two unread channels, same team, reading both', async () => {
const channelId1 = TestHelper.generateId();
const channelId2 = TestHelper.generateId();
const teamId = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
channels: {
[channelId1]: {
id: channelId1,
team_id: teamId,
},
[channelId2]: {
id: channelId2,
team_id: teamId,
},
},
messageCounts: {
[channelId1]: {total: 10},
[channelId2]: {total: 12},
},
myMembers: {
[channelId1]: {
channel_id: channelId1,
mention_count: 2,
msg_count: 5,
last_viewed_at: 1000,
},
[channelId2]: {
channel_id: channelId2,
mention_count: 4,
msg_count: 9,
last_viewed_at: 2000,
},
},
},
teams: {
myMembers: {
[teamId]: {
id: teamId,
mention_count: 6,
msg_count: 8,
},
},
},
},
});
await store.dispatch(Actions.markChannelAsRead(channelId1, channelId2));
const state = store.getState();
expect(state.entities.channels.myMembers[channelId1].mention_count).toBe(0);
expect(state.entities.channels.myMembers[channelId1].msg_count).toBe(state.entities.channels.messageCounts[channelId1].total);
expect(state.entities.channels.myMembers[channelId1].last_viewed_at).toBeGreaterThan(1000);
expect(state.entities.channels.myMembers[channelId2].mention_count).toBe(0);
expect(state.entities.channels.myMembers[channelId2].msg_count).toBe(state.entities.channels.messageCounts[channelId2].total);
expect(state.entities.channels.myMembers[channelId2].last_viewed_at).toBeGreaterThan(2000);
expect(state.entities.teams.myMembers[teamId].mention_count).toBe(0);
expect(state.entities.teams.myMembers[teamId].msg_count).toBe(0);
});
it('two unread channels, same team, reading both (opposite order)', async () => {
const channelId1 = TestHelper.generateId();
const channelId2 = TestHelper.generateId();
const teamId = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
channels: {
[channelId1]: {
id: channelId1,
team_id: teamId,
},
[channelId2]: {
id: channelId2,
team_id: teamId,
},
},
messageCounts: {
[channelId1]: {total: 10},
[channelId2]: {total: 12},
},
myMembers: {
[channelId1]: {
channel_id: channelId1,
mention_count: 2,
msg_count: 5,
last_viewed_at: 1000,
},
[channelId2]: {
channel_id: channelId2,
mention_count: 4,
msg_count: 9,
last_viewed_at: 1000,
},
},
},
teams: {
myMembers: {
[teamId]: {
id: teamId,
mention_count: 6,
msg_count: 8,
},
},
},
},
});
await store.dispatch(Actions.markChannelAsRead(channelId2, channelId1));
const state = store.getState();
expect(state.entities.channels.myMembers[channelId1].mention_count).toBe(0);
expect(state.entities.channels.myMembers[channelId1].msg_count).toBe(state.entities.channels.messageCounts[channelId1].total);
expect(state.entities.channels.myMembers[channelId1].last_viewed_at).toBeGreaterThan(1000);
expect(state.entities.channels.myMembers[channelId2].mention_count).toBe(0);
expect(state.entities.channels.myMembers[channelId2].msg_count).toBe(state.entities.channels.messageCounts[channelId2].total);
expect(state.entities.channels.myMembers[channelId2].last_viewed_at).toBeGreaterThan(2000);
expect(state.entities.teams.myMembers[teamId].mention_count).toBe(0);
expect(state.entities.teams.myMembers[teamId].msg_count).toBe(0);
});
it('two unread channels, different teams, reading one', async () => {
const channelId1 = TestHelper.generateId();
const channelId2 = TestHelper.generateId();
@@ -1402,154 +914,6 @@ describe('Actions.Channels', () => {
expect(state.entities.teams.myMembers[teamId2].mention_count).toBe(4);
expect(state.entities.teams.myMembers[teamId2].msg_count).toBe(3);
});
it('two unread channels, different teams, reading both', async () => {
const channelId1 = TestHelper.generateId();
const channelId2 = TestHelper.generateId();
const teamId1 = TestHelper.generateId();
const teamId2 = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
channels: {
[channelId1]: {
id: channelId1,
team_id: teamId1,
},
[channelId2]: {
id: channelId2,
team_id: teamId2,
},
},
messageCounts: {
[channelId1]: {total: 10},
[channelId2]: {total: 12},
},
myMembers: {
[channelId1]: {
channel_id: channelId1,
mention_count: 2,
msg_count: 5,
last_viewed_at: 1000,
},
[channelId2]: {
channel_id: channelId2,
mention_count: 4,
msg_count: 9,
last_viewed_at: 2000,
},
},
},
teams: {
myMembers: {
[teamId1]: {
id: teamId1,
mention_count: 2,
msg_count: 5,
},
[teamId2]: {
id: teamId2,
mention_count: 4,
msg_count: 3,
},
},
},
},
});
await store.dispatch(Actions.markChannelAsRead(channelId1, channelId2));
const state = store.getState();
expect(state.entities.channels.myMembers[channelId1].mention_count).toBe(0);
expect(state.entities.channels.myMembers[channelId1].msg_count).toBe(state.entities.channels.messageCounts[channelId1].total);
expect(state.entities.channels.myMembers[channelId1].last_viewed_at).toBeGreaterThan(1000);
expect(state.entities.channels.myMembers[channelId2].mention_count).toBe(0);
expect(state.entities.channels.myMembers[channelId2].msg_count).toBe(state.entities.channels.messageCounts[channelId2].total);
expect(state.entities.channels.myMembers[channelId2].last_viewed_at).toBeGreaterThan(2000);
expect(state.entities.teams.myMembers[teamId1].mention_count).toBe(0);
expect(state.entities.teams.myMembers[teamId1].msg_count).toBe(0);
expect(state.entities.teams.myMembers[teamId2].mention_count).toBe(0);
expect(state.entities.teams.myMembers[teamId2].msg_count).toBe(0);
});
it('two unread channels, different teams, reading both (opposite order)', async () => {
const channelId1 = TestHelper.generateId();
const channelId2 = TestHelper.generateId();
const teamId1 = TestHelper.generateId();
const teamId2 = TestHelper.generateId();
store = configureStore({
entities: {
channels: {
channels: {
[channelId1]: {
id: channelId1,
team_id: teamId1,
},
[channelId2]: {
id: channelId2,
team_id: teamId2,
},
},
messageCounts: {
[channelId1]: {total: 10},
[channelId2]: {total: 12},
},
myMembers: {
[channelId1]: {
channel_id: channelId1,
mention_count: 2,
msg_count: 5,
last_viewed_at: 1000,
},
[channelId2]: {
channel_id: channelId2,
mention_count: 4,
msg_count: 9,
last_viewed_at: 2000,
},
},
},
teams: {
myMembers: {
[teamId1]: {
id: teamId1,
mention_count: 2,
msg_count: 5,
},
[teamId2]: {
id: teamId2,
mention_count: 4,
msg_count: 3,
},
},
},
},
});
await store.dispatch(Actions.markChannelAsRead(channelId1, channelId2));
const state = store.getState();
expect(state.entities.channels.myMembers[channelId1].mention_count).toBe(0);
expect(state.entities.channels.myMembers[channelId1].msg_count).toBe(state.entities.channels.messageCounts[channelId1].total);
expect(state.entities.channels.myMembers[channelId1].last_viewed_at).toBeGreaterThan(1000);
expect(state.entities.channels.myMembers[channelId2].mention_count).toBe(0);
expect(state.entities.channels.myMembers[channelId2].msg_count).toBe(state.entities.channels.messageCounts[channelId2].total);
expect(state.entities.channels.myMembers[channelId2].last_viewed_at).toBeGreaterThan(2000);
expect(state.entities.teams.myMembers[teamId1].mention_count).toBe(0);
expect(state.entities.teams.myMembers[teamId1].msg_count).toBe(0);
expect(state.entities.teams.myMembers[teamId2].mention_count).toBe(0);
expect(state.entities.teams.myMembers[teamId2].msg_count).toBe(0);
});
});
it('getChannels', async () => {

Просмотреть файл

@@ -728,102 +728,39 @@ export function unarchiveChannel(channelId: string): ActionFunc {
};
}
export function viewChannel(channelId: string, prevChannelId = ''): ActionFunc {
export function updateApproximateViewTime(channelId: string): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const {currentUserId} = getState().entities.users;
const {myPreferences} = getState().entities.preferences;
const viewTimePref = myPreferences[`${Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME}--${channelId}`];
const viewTime = viewTimePref ? parseInt(viewTimePref.value!, 10) : 0;
const prevChanManuallyUnread = isManuallyUnread(getState(), prevChannelId);
if (viewTime < new Date().getTime() - (3 * 60 * 60 * 1000)) {
const preferences = [
{user_id: currentUserId, category: Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, name: channelId, value: new Date().getTime().toString()},
];
savePreferences(currentUserId, preferences)(dispatch);
}
try {
await Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
const actions: AnyAction[] = [];
const {myMembers} = getState().entities.channels;
const member = myMembers[channelId];
if (member) {
if (isManuallyUnread(getState(), channelId)) {
actions.push({
type: ChannelTypes.REMOVE_MANUALLY_UNREAD,
data: {channelId},
});
}
actions.push({
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
data: {...member, last_viewed_at: new Date().getTime()},
});
dispatch(loadRolesIfNeeded(member.roles.split(' ')));
}
const prevMember = myMembers[prevChannelId];
if (!prevChanManuallyUnread && prevMember) {
actions.push({
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
data: {...prevMember, last_viewed_at: new Date().getTime()},
});
dispatch(loadRolesIfNeeded(prevMember.roles.split(' ')));
}
dispatch(batchActions(actions));
return {data: true};
};
}
export function markChannelAsViewed(channelId: string, prevChannelId?: string): ActionFunc {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
const actions = actionsToMarkChannelAsViewed(getState, channelId, prevChannelId);
return dispatch(batchActions(actions));
};
}
export function actionsToMarkChannelAsViewed(getState: GetStateFunc, channelId: string, prevChannelId = '') {
const actions: AnyAction[] = [];
const state = getState();
const {myMembers} = state.entities.channels;
const member = myMembers[channelId];
if (member) {
actions.push({
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
data: {...member, last_viewed_at: Date.now()},
});
if (isManuallyUnread(state, channelId)) {
actions.push({
type: ChannelTypes.REMOVE_MANUALLY_UNREAD,
data: {channelId},
});
export function readMultipleChannels(channelIds: string[]): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let response;
try {
response = await Client4.readMultipleChannels(channelIds);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
}
const prevMember = myMembers[prevChannelId];
if (prevMember && !isManuallyUnread(state, prevChannelId)) {
actions.push({
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
data: {...prevMember, last_viewed_at: Date.now()},
});
}
dispatch(markMultipleChannelsAsRead(response.last_viewed_at_times));
return actions;
return {data: true};
};
}
export function getChannels(teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc {
@@ -1217,38 +1154,27 @@ export function updateChannelPurpose(channelId: string, purpose: string): Action
};
}
export function markChannelAsRead(channelId: string, prevChannelId?: string, updateLastViewedAt = true): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
const prevChanManuallyUnread = isManuallyUnread(state, prevChannelId);
export function markChannelAsRead(channelId: string, skipUpdateViewTime = false): ActionFunc {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
if (skipUpdateViewTime) {
dispatch(updateApproximateViewTime(channelId));
}
dispatch(markChannelAsViewedOnServer(channelId));
const actions = actionsToMarkChannelAsRead(getState, channelId, prevChannelId);
const actions = actionsToMarkChannelAsRead(getState, channelId);
if (actions.length > 0) {
dispatch(batchActions(actions));
}
// Send channel last viewed at to the server
if (updateLastViewedAt) {
dispatch(markChannelAsViewedOnServer(channelId, prevChanManuallyUnread ? '' : prevChannelId));
return {data: true};
};
}
const now = Date.now();
// Don't use actionsToMarkChannelAsViewed here because that overwrites fields modified by
// actionsToMarkChannelAsRead
actions.push({
type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
data: {
channel_id: channelId,
last_viewed_at: now,
},
});
if (prevChannelId && !prevChanManuallyUnread) {
actions.push({
type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
data: {
channel_id: prevChannelId,
last_viewed_at: now,
},
});
}
export function markMultipleChannelsAsRead(channelTimes: Record<string, number>): ActionFunc {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
const actions: AnyAction[] = [];
for (const id of Object.keys(channelTimes)) {
actions.push(...actionsToMarkChannelAsRead(getState, id, channelTimes[id]));
}
if (actions.length > 0) {
@@ -1259,33 +1185,41 @@ export function markChannelAsRead(channelId: string, prevChannelId?: string, upd
};
}
export function markChannelAsViewedOnServer(channelId: string, prevChannelId?: string): ActionFunc {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
Client4.viewMyChannel(channelId, prevChannelId).then().catch((error) => {
export function markChannelAsViewedOnServer(channelId: string): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
try {
await Client4.viewMyChannel(channelId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
});
}
// const actions: AnyAction[] = [];
// for (const id of Object.keys(response.last_viewed_at_times)) {
// actions.push({
// type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
// data: {
// channel_id: channelId,
// last_viewed_at: response.last_viewed_at_times[id],
// },
// });
// }
return {data: true};
};
}
export function actionsToMarkChannelAsRead(getState: GetStateFunc, channelId: string, prevChannelId?: string) {
export function actionsToMarkChannelAsRead(getState: GetStateFunc, channelId: string, viewedAt = Date.now()) {
const state = getState();
const {channels, messageCounts, myMembers} = state.entities.channels;
const prevChanManuallyUnread = isManuallyUnread(state, prevChannelId);
// Update channel member objects to set all mentions and posts as viewed
const channel = channels[channelId];
const messageCount = messageCounts[channelId];
const prevChannel = (!prevChanManuallyUnread && prevChannelId) ? channels[prevChannelId] : null; // May be null since prevChannelId is optional
const prevMessageCount = (!prevChanManuallyUnread && prevChannelId) ? messageCounts[prevChannelId] : null;
// Update team member objects to set mentions and posts in channel as viewed
const channelMember = myMembers[channelId];
const prevChannelMember = (!prevChanManuallyUnread && prevChannelId) ? myMembers[prevChannelId] : null; // May also be null
const actions: AnyAction[] = [];
@@ -1310,6 +1244,14 @@ export function actionsToMarkChannelAsRead(getState: GetStateFunc, channelId: st
amountUrgent: channelMember.urgent_mention_count,
},
});
actions.push({
type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
data: {
channel_id: channelId,
last_viewed_at: viewedAt,
},
});
}
if (channel && isManuallyUnread(getState(), channelId)) {
@@ -1319,82 +1261,9 @@ export function actionsToMarkChannelAsRead(getState: GetStateFunc, channelId: st
});
}
if (prevChannel && prevChannelMember && prevMessageCount) {
actions.push({
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
data: {
teamId: prevChannel.team_id,
channelId: prevChannelId,
amount: prevMessageCount.total - prevChannelMember.msg_count,
amountRoot: prevMessageCount.root - prevChannelMember.msg_count_root,
},
});
actions.push({
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
data: {
teamId: prevChannel.team_id,
channelId: prevChannelId,
amount: prevChannelMember.mention_count,
amountRoot: prevChannelMember.mention_count_root,
amountUrgent: prevChannelMember.urgent_mention_count,
},
});
}
return actions;
}
// Increments the number of posts in the channel by 1 and marks it as unread if necessary
export function markChannelAsUnread(teamId: string, channelId: string, mentions: string[], fetchedChannelMember = false, isRoot = false): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
const {myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
const actions: AnyAction[] = [{
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
data: {
teamId,
channelId,
amount: 1,
amountRoot: isRoot ? 1 : 0,
onlyMentions: myMembers[channelId] && myMembers[channelId].notify_props &&
myMembers[channelId].notify_props.mark_unread === MarkUnread.MENTION,
fetchedChannelMember,
},
}];
if (!fetchedChannelMember) {
actions.push({
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
data: {
channelId,
amountRoot: isRoot ? 1 : 0,
amount: 1,
},
});
}
if (mentions && mentions.indexOf(currentUserId) !== -1) {
actions.push({
type: ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT,
data: {
teamId,
channelId,
amountRoot: isRoot ? 1 : 0,
amount: 1,
fetchedChannelMember,
},
});
}
dispatch(batchActions(actions));
return {data: true};
};
}
export function actionsToMarkChannelAsUnread(getState: GetStateFunc, teamId: string, channelId: string, mentions: string[], fetchedChannelMember = false, isRoot = false, priority = '') {
const state = getState();
const {myMembers} = state.entities.channels;
@@ -1609,8 +1478,6 @@ export default {
joinChannel,
deleteChannel,
unarchiveChannel,
viewChannel,
markChannelAsViewed,
getChannels,
autocompleteChannels,
autocompleteChannelsForSearch,
@@ -1622,7 +1489,6 @@ export default {
updateChannelHeader,
updateChannelPurpose,
markChannelAsRead,
markChannelAsUnread,
favoriteChannel,
unfavoriteChannel,
membersMinusGroupMembers,

Просмотреть файл

@@ -11,7 +11,7 @@ const WebsocketEvents = {
CHANNEL_DELETED: 'channel_deleted',
CHANNEL_UNARCHIVED: 'channel_restored',
CHANNEL_UPDATED: 'channel_updated',
CHANNEL_VIEWED: 'channel_viewed',
MULTIPLE_CHANNELS_VIEWED: 'multiple_channels_viewed',
CHANNEL_MEMBER_UPDATED: 'channel_member_updated',
CHANNEL_SCHEME_UPDATED: 'channel_scheme_updated',
DIRECT_ADDED: 'direct_added',

Просмотреть файл

@@ -208,6 +208,33 @@ export function makeGetFilteredChannelIdsForCategory(): (state: GlobalState, cat
);
}
// Returns a selector that, given a category, returns the ids of channels visible in that category. The returned channels do not
// include unread channels when the Unreads category is enabled.
export function makeGetUnreadIdsForCategory(): (state: GlobalState, category: ChannelCategory) => string[] {
const getChannelIdsForCategory = makeGetChannelIdsForCategory();
const emptyList: string[] = [];
return createSelector(
'makeGetFilteredChannelIdsForCategory',
getChannelIdsForCategory,
getUnreadChannelIdsSet,
shouldShowUnreadsCategory,
(channelIds, unreadChannelIdsSet, showUnreadsCategory) => {
if (showUnreadsCategory) {
return emptyList;
}
const filtered = channelIds.filter((id) => unreadChannelIdsSet.has(id));
if (filtered.length === 0) {
return emptyList;
}
return filtered.length === channelIds.length ? channelIds : filtered;
},
);
}
export function getDraggingState(state: GlobalState): DraggingState {
return state.views.channelSidebar.draggingState;
}

Просмотреть файл

@@ -570,7 +570,7 @@ export const SocketEvents = {
CHANNEL_DELETED: 'channel_deleted',
CHANNEL_UNARCHIVED: 'channel_restored',
CHANNEL_UPDATED: 'channel_updated',
CHANNEL_VIEWED: 'channel_viewed',
MULTIPLE_CHANNELS_VIEWED: 'multiple_channels_viewed',
CHANNEL_MEMBER_UPDATED: 'channel_member_updated',
CHANNEL_SCHEME_UPDATED: 'channel_scheme_updated',
DIRECT_ADDED: 'direct_added',

Просмотреть файл

@@ -1775,14 +1775,21 @@ export default class Client4 {
);
};
viewMyChannel = (channelId: string, prevChannelId?: string) => {
const data = {channel_id: channelId, prev_channel_id: prevChannelId, collapsed_threads_supported: true};
viewMyChannel = (channelId: string) => {
const data = {channel_id: channelId, collapsed_threads_supported: true};
return this.doFetch<ChannelViewResponse>(
`${this.getChannelsRoute()}/members/me/view`,
{method: 'post', body: JSON.stringify(data)},
);
};
readMultipleChannels = (channelIds: string[]) => {
return this.doFetch<ChannelViewResponse>(
`${this.getChannelsRoute()}/members/me/mark_read`,
{method: 'post', body: JSON.stringify(channelIds)},
);
};
autocompleteChannels = (teamId: string, name: string) => {
return this.doFetch<Channel[]>(
`${this.getTeamRoute(teamId)}/channels/autocomplete${buildQueryString({name})}`,