Various improvements to preferences code (#27710)

* Add isPostFlagged selector

* Remove unused code for actions_menu preferences

* Improve types of Preference and getPreference

* MM-58111 Change makeGetCategory to take category name once

* Use shouldShowJoinLeaveMessages everywhere

* Change makeGetCategory to better memoize its result

* Fix test that needs EnableJoinLeaveMessageByDefault to be set

* Remove more references to action_menu preferences
This commit is contained in:
Harrison Healey 2024-08-01 18:53:30 -04:00 committed by GitHub
parent 28939d84da
commit 6956923b6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 446 additions and 392 deletions

View File

@ -126,18 +126,6 @@ declare namespace Cypress {
*/
apiSaveCloudTrialBannerPreference(userId: string, name: string, value: string): Chainable<Response>;
/**
* Save actions menu preference.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put
* @param {string} userId - User ID
* @param {string} value - true (default) or false
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiSaveActionsMenuPreference('user-id', true);
*/
apiSaveActionsMenuPreference(userId: string, value: boolean): Chainable<Response>;
/**
* Save show trial modal.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put

View File

@ -256,17 +256,6 @@ Cypress.Commands.add('apiSaveCloudTrialBannerPreference', (userId, name, value)
return cy.apiSaveUserPreference([preference], userId);
});
Cypress.Commands.add('apiSaveActionsMenuPreference', (userId, value = true) => {
const preference = {
user_id: userId,
category: 'actions_menu',
name: 'actions_menu_tutorial_state',
value: JSON.stringify({actions_menu_modal_viewed: value}),
};
return cy.apiSaveUserPreference([preference], userId);
});
Cypress.Commands.add('apiSaveStartTrialModal', (userId, value = 'true') => {
const preference = {
user_id: userId,
@ -402,12 +391,6 @@ Cypress.Commands.add('apiDisableTutorials', (userId) => {
name: userId,
value: '999',
},
{
user_id: userId,
category: 'actions_menu',
name: 'actions_menu_tutorial_state',
value: '{"actions_menu_modal_viewed":true}',
},
{
user_id: userId,
category: 'drafts',

View File

@ -234,7 +234,6 @@ Cypress.Commands.add('apiCreateUser', ({
prefix = 'user',
createAt = 0,
bypassTutorial = true,
hideActionsMenu = true,
hideOnboarding = true,
bypassWhatsNewModal = true,
user = null,
@ -265,10 +264,6 @@ Cypress.Commands.add('apiCreateUser', ({
cy.apiDisableTutorials(createdUser.id);
}
if (hideActionsMenu) {
cy.apiSaveActionsMenuPreference(createdUser.id, true);
}
if (hideOnboarding) {
cy.apiSaveOnboardingPreference(createdUser.id, 'hide', 'true');
cy.apiSaveOnboardingPreference(createdUser.id, 'skip', 'true');

View File

@ -261,7 +261,6 @@ function resetUserPreference(userId) {
cy.apiSaveOnboardingTaskListPreference(userId, 'onboarding_task_list_open', 'false');
cy.apiSaveOnboardingTaskListPreference(userId, 'onboarding_task_list_show', 'false');
cy.apiSaveCloudTrialBannerPreference(userId, 'trial', 'max_days_banner');
cy.apiSaveActionsMenuPreference(userId);
cy.apiSaveSkipStepsPreference(userId, 'true');
cy.apiSaveStartTrialModal(userId, 'true');
cy.apiSaveUnreadScrollPositionPreference(userId, 'start_from_left_off');

View File

@ -81,7 +81,9 @@ describe('channel view actions', () => {
},
},
general: {
config: {},
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
serverVersion: '5.12.0',
},
roles: {
@ -589,7 +591,7 @@ describe('channel view actions', () => {
describe('markChannelAsReadOnFocus', () => {
test('should mark channel as read when channel is not manually unread', async () => {
test = mockStore(initialState);
store = mockStore(initialState);
await store.dispatch(Actions.markChannelAsReadOnFocus(channel1.id));

View File

@ -43,7 +43,7 @@ const CloudTrialBanner = ({trialEndDate}: Props): JSX.Element | null => {
// the banner when dismissed, will be dismissed for 10 days
const bannerIsStillDismissed = diffDays < 0;
shouldShowBanner = storedDismissedEndDate && bannerIsStillDismissed;
shouldShowBanner = Boolean(storedDismissedEndDate) && bannerIsStillDismissed;
}
const [showBanner, setShowBanner] = useState<boolean>(shouldShowBanner);

View File

@ -20,9 +20,9 @@ import type {GlobalState} from 'types/store';
import CloudTrialAnnouncementBar from './cloud_trial_announcement_bar';
function mapStateToProps(state: GlobalState) {
const getCategory = makeGetCategory();
const getCloudTrialBannerPreferences = makeGetCategory('getCloudTrialBannerPreferences', Preferences.CLOUD_TRIAL_BANNER);
function mapStateToProps(state: GlobalState) {
const subscription = state.entities.cloud.subscription;
const isCloud = getLicense(state).Cloud === 'true';
let isFreeTrial = false;
@ -43,7 +43,7 @@ function mapStateToProps(state: GlobalState) {
currentUser: getCurrentUser(state),
isCloud,
subscription,
preferences: getCategory(state, Preferences.CLOUD_TRIAL_BANNER),
preferences: getCloudTrialBannerPreferences(state),
};
}

View File

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import React from 'react';
import {FormattedMessage, defineMessages} from 'react-intl';
import {useSelector, useDispatch} from 'react-redux';
@ -30,14 +30,13 @@ import type {GlobalState} from 'types/store';
import AnnouncementBar from '../default_announcement_bar';
const getCloudTrialEndBannerPreferences = makeGetCategory('getCloudTrialEndBannerPreferences', Preferences.CLOUD_TRIAL_END_BANNER);
const CloudTrialEndAnnouncementBar: React.FC = () => {
const limits = useGetLimits();
const subscription = useGetSubscription();
const getCategory = useMemo(makeGetCategory, []);
const dispatch = useDispatch();
const preferences = useSelector((state: GlobalState) =>
getCategory(state, Preferences.CLOUD_TRIAL_END_BANNER),
);
const preferences = useSelector(getCloudTrialEndBannerPreferences);
const currentUser = useSelector((state: GlobalState) =>
getCurrentUser(state),
);

View File

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
@ -10,7 +10,7 @@ import type {PreferenceType} from '@mattermost/types/preferences';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {getOverageBannerPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
@ -46,9 +46,8 @@ const OverageUsersBanner = () => {
const license = useSelector(getLicense);
const seatsPurchased = parseInt(license.Users, 10);
const isCloud = useSelector(isCurrentLicenseCloud);
const getPreferencesCategory = useMemo(makeGetCategory, []);
const currentUser = useSelector((state: GlobalState) => getCurrentUser(state));
const overagePreferences = useSelector((state: GlobalState) => getPreferencesCategory(state, Preferences.OVERAGE_USERS_BANNER));
const overagePreferences = useSelector(getOverageBannerPreferences);
const activeUsers = ((stats || {})[StatTypes.TOTAL_USERS]) as number || 0;
const {
isBetween5PercerntAnd10PercentPurchasedSeats,

View File

@ -1,14 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useEffect, useMemo} from 'react';
import {useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import type {PreferenceType} from '@mattermost/types/preferences';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
import {trackEvent} from 'actions/telemetry_actions';
@ -29,7 +27,6 @@ import type {GlobalState} from 'types/store';
const ShowStartTrialModal = () => {
const isUserAdmin = useSelector((state: GlobalState) => isCurrentUserSystemAdmin(state));
const openStartTrialFormModal = useOpenStartTrialFormModal();
const getCategory = useMemo(makeGetCategory, []);
const dispatch = useDispatch();
@ -40,7 +37,7 @@ const ShowStartTrialModal = () => {
const installationDate = useSelector((state: GlobalState) => getConfig(state).InstallationDate);
const currentUser = useSelector((state: GlobalState) => getCurrentUser(state));
const preferences = useSelector<GlobalState, PreferenceType[]>((state) => getCategory(state, Preferences.START_TRIAL_MODAL));
const hadAdminDismissedModal = useSelector((state: GlobalState) => getBool(state, Preferences.START_TRIAL_MODAL, Constants.TRIAL_MODAL_AUTO_SHOWN));
const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense);
const currentLicense = useSelector(getLicense);
@ -77,7 +74,6 @@ const ShowStartTrialModal = () => {
const now = new Date().getTime();
const hasEnvMoreThan6Hours = now > installationDatePlus6Hours;
const hasEnvMoreThan10Users = Number(totalUsers) > userThreshold;
const hadAdminDismissedModal = preferences.some((pref: PreferenceType) => pref.name === Constants.TRIAL_MODAL_AUTO_SHOWN && pref.value === TRUE);
if (isUserAdmin && !isBenefitsModalOpened && hasEnvMoreThan10Users && hasEnvMoreThan6Hours && !hadAdminDismissedModal && !isLicensedOrPreviousLicensed) {
openStartTrialFormModal({trackingLocation: 'show_start_trial_modal'}, handleOnClose);
trackEvent(

View File

@ -8,18 +8,16 @@ import type {PreferenceType} from '@mattermost/types/preferences';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {get as getPreference} from 'mattermost-redux/selectors/entities/preferences';
import type {ActionResult} from 'mattermost-redux/types/actions';
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
import type {GlobalState} from 'types/store';
export default function usePreference(category: string, name: string): [string | undefined, (value: string) => Promise<ActionResult>] {
const dispatch = useDispatch();
const userId = useSelector(getCurrentUserId);
const preferences = useSelector(getMyPreferences);
const key = getPreferenceKey(category, name);
const preference = preferences[key];
const preferenceValue = useSelector((state: GlobalState) => getPreference(state, category, name));
const setPreference = useCallback((value: string) => {
const preference: PreferenceType = {
@ -31,5 +29,5 @@ export default function usePreference(category: string, name: string): [string |
return dispatch(savePreferences(userId, [preference]));
}, [category, name, userId]);
return useMemo(() => ([preference?.value, setPreference]), [preference?.value, setPreference]);
return useMemo(() => ([preferenceValue, setPreference]), [preferenceValue, setPreference]);
}

View File

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
@ -10,7 +10,7 @@ import type {PreferenceType} from '@mattermost/types/preferences';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {getOverageBannerPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
import AlertBanner from 'components/alert_banner';
@ -42,9 +42,8 @@ const OverageUsersBannerNotice = () => {
const isGovSku = getIsGovSku(license);
const seatsPurchased = parseInt(license.Users, 10);
const isCloud = useSelector(isCurrentLicenseCloud);
const getPreferencesCategory = useMemo(makeGetCategory, []);
const currentUser = useSelector((state: GlobalState) => getCurrentUser(state));
const overagePreferences = useSelector((state: GlobalState) => getPreferencesCategory(state, Preferences.OVERAGE_USERS_BANNER));
const overagePreferences = useSelector(getOverageBannerPreferences);
const activeUsers = ((stats || {})[StatTypes.TOTAL_USERS]) as number || 0;
const {

View File

@ -9,7 +9,6 @@ import {matchPath, useLocation} from 'react-router-dom';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {isCurrentUserGuestUser, isCurrentUserSystemAdmin, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
import {trackEvent as trackEventAction} from 'actions/telemetry_actions';
@ -22,6 +21,7 @@ import {
} from 'actions/views/onboarding_tasks';
import {setProductMenuSwitcherOpen} from 'actions/views/product_menu';
import {setStatusDropdown} from 'actions/views/status_dropdown';
import {getOnboardingTaskPreferences} from 'selectors/onboarding';
import Channels from 'components/common/svg_images_components/channels_svg';
import Gears from 'components/common/svg_images_components/gears_svg';
@ -46,8 +46,6 @@ import type {GlobalState} from 'types/store';
import {OnboardingTaskCategory, OnboardingTaskList, OnboardingTasksName, TaskNameMapToSteps} from './constants';
import {generateTelemetryTag} from './utils';
const getCategory = makeGetCategory();
const useGetTaskDetails = () => {
const {formatMessage} = useIntl();
return {
@ -145,7 +143,7 @@ export const useTasksList = () => {
};
export const useTasksListWithStatus = () => {
const dataInDb = useSelector((state: GlobalState) => getCategory(state, OnboardingTaskCategory));
const dataInDb = useSelector(getOnboardingTaskPreferences);
const tasksList = useTasksList();
const getTaskDetails = useGetTaskDetails();
return useMemo(() =>

View File

@ -4,16 +4,15 @@
import {connect} from 'react-redux';
import type {ConnectedProps} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {AnyAction, Dispatch} from 'redux';
import type {Dispatch} from 'redux';
import type {Emoji} from '@mattermost/types/emojis';
import type {Post} from '@mattermost/types/posts';
import {setActionsMenuInitialisationState} from 'mattermost-redux/actions/preferences';
import {General} from 'mattermost-redux/constants';
import {getDirectTeammate} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getPost, makeGetCommentCountForPost, makeIsPostCommentMention, isPostAcknowledgementsEnabled, isPostPriorityEnabled} from 'mattermost-redux/selectors/entities/posts';
import {getPost, makeGetCommentCountForPost, makeIsPostCommentMention, isPostAcknowledgementsEnabled, isPostPriorityEnabled, isPostFlagged} from 'mattermost-redux/selectors/entities/posts';
import type {UserActivityPost} from 'mattermost-redux/selectors/entities/posts';
import {
get,
@ -179,7 +178,7 @@ function makeMapStateToProps() {
channelIsArchived: isArchivedChannel(channel),
isConsecutivePost: isConsecutivePost(state, ownProps),
previousPostIsComment,
isFlagged: get(state, Preferences.CATEGORY_FLAGGED_POST, post.id, null) !== null,
isFlagged: isPostFlagged(state, post.id),
compactDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
colorizeUsernames: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLORIZE_USERNAMES, Preferences.COLORIZE_USERNAMES_DEFAULT) === 'true',
shouldShowActionsMenu: shouldShowActionsMenu(state, post),
@ -220,12 +219,11 @@ function makeMapStateToProps() {
};
}
function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators({
markPostAsUnread,
emitShortcutReactToLastPostFrom,
setActionsMenuInitialisationState,
selectPost,
selectPostFromRightHandSideSearch,
setRhsExpanded,

View File

@ -41,7 +41,6 @@ describe('PostComponent', () => {
actions: {
markPostAsUnread: jest.fn(),
emitShortcutReactToLastPostFrom: jest.fn(),
setActionsMenuInitialisationState: jest.fn(),
selectPost: jest.fn(),
selectPostFromRightHandSideSearch: jest.fn(),
removePost: jest.fn(),

View File

@ -91,7 +91,6 @@ export type Props = {
actions: {
markPostAsUnread: (post: Post, location: string) => void;
emitShortcutReactToLastPostFrom: (emittedFrom: 'CENTER' | 'RHS_ROOT' | 'NO_WHERE') => void;
setActionsMenuInitialisationState: (viewed: Record<string, boolean>) => void;
selectPost: (post: Post) => void;
selectPostFromRightHandSideSearch: (post: Post) => void;
removePost: (post: Post) => void;

View File

@ -8,8 +8,7 @@ import type {Dispatch} from 'redux';
import type {GlobalState} from '@mattermost/types/store';
import {getMissingProfilesByIds, getMissingProfilesByUsernames} from 'mattermost-redux/actions/users';
import {Preferences} from 'mattermost-redux/constants';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {shouldShowJoinLeaveMessages} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser, makeGetProfilesByIdsAndUsernames} from 'mattermost-redux/selectors/entities/users';
import CombinedSystemMessage from './combined_system_message';
@ -29,7 +28,7 @@ function makeMapStateToProps() {
return {
currentUserId: currentUser.id,
currentUsername: currentUser.username,
showJoinLeave: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE, true),
showJoinLeave: shouldShowJoinLeaveMessages(state),
userProfiles: getProfilesByIdsAndUsernames(state, {allUserIds, allUsernames}),
};
};

View File

@ -25,39 +25,36 @@ import {Preferences} from 'utils/constants';
import type {GlobalState} from 'types/store';
function makeMapStateToProps() {
const getCategory = makeGetCategory();
const getSystemNoticePreferences = makeGetCategory('getSystemNoticePreferences', Preferences.CATEGORY_SYSTEM_NOTICE);
const getPreferenceNameMap = createSelector(
'getPreferenceNameMap',
getSystemNoticePreferences,
(preferences) => {
const nameMap: {[key: string]: PreferenceType} = {};
preferences.forEach((p) => {
nameMap[p.name] = p;
});
return nameMap;
},
);
const getPreferenceNameMap = createSelector(
'getPreferenceNameMap',
getCategory,
(preferences) => {
const nameMap: {[key: string]: PreferenceType} = {};
preferences.forEach((p) => {
nameMap[p.name] = p;
});
return nameMap;
},
);
function mapStateToProps(state: GlobalState) {
const license = getLicense(state);
const config = getConfig(state);
const serverVersion = state.entities.general.serverVersion;
const analytics = state.entities.admin.analytics;
return function mapStateToProps(state: GlobalState) {
const license = getLicense(state);
const config = getConfig(state);
const serverVersion = state.entities.general.serverVersion;
const analytics = state.entities.admin.analytics;
return {
currentUserId: state.entities.users.currentUserId,
preferences: getPreferenceNameMap(state, Preferences.CATEGORY_SYSTEM_NOTICE),
dismissedNotices: state.views.notice.hasBeenDismissed,
isSystemAdmin: haveISystemPermission(state, {permission: Permissions.MANAGE_SYSTEM}),
notices: Notices,
config,
license,
serverVersion,
analytics,
currentChannel: getCurrentChannel(state),
};
return {
currentUserId: state.entities.users.currentUserId,
preferences: getPreferenceNameMap(state),
dismissedNotices: state.views.notice.hasBeenDismissed,
isSystemAdmin: haveISystemPermission(state, {permission: Permissions.MANAGE_SYSTEM}),
notices: Notices,
config,
license,
serverVersion,
analytics,
currentChannel: getCurrentChannel(state),
};
}
@ -71,4 +68,4 @@ function mapDispatchToProps(dispatch: Dispatch) {
};
}
export default connect(makeMapStateToProps, mapDispatchToProps)(SystemNotice);
export default connect(mapStateToProps, mapDispatchToProps)(SystemNotice);

View File

@ -4,13 +4,12 @@
import React, {memo, useCallback} from 'react';
import type {ReactNode} from 'react';
import {useIntl} from 'react-intl';
import {useDispatch, useSelector, shallowEqual} from 'react-redux';
import {useDispatch, useSelector} from 'react-redux';
import type {UserThread} from '@mattermost/types/threads';
import {setThreadFollow, updateThreadRead, markLastPostInThreadAsUnread} from 'mattermost-redux/actions/threads';
import {Preferences} from 'mattermost-redux/constants';
import {get} from 'mattermost-redux/selectors/entities/preferences';
import {isPostFlagged} from 'mattermost-redux/selectors/entities/posts';
import {
flagPost as savePost,
@ -56,7 +55,7 @@ function ThreadMenu({
goToInChannel,
} = useThreadRouting();
const isSaved = useSelector((state: GlobalState) => get(state, Preferences.CATEGORY_FLAGGED_POST, threadId, null) != null, shallowEqual);
const isSaved = useSelector((state: GlobalState) => isPostFlagged(state, threadId));
const handleReadUnread = useCallback(() => {
const lastViewedAt = hasUnreads ? Date.now() : unreadTimestamp;

View File

@ -24,8 +24,10 @@ import type {GlobalState} from 'types/store';
import AdvancedSettingsDisplay from './user_settings_advanced';
import type {OwnProps} from './user_settings_advanced';
function makeMapStateToProps(state: GlobalState, props: OwnProps) {
const getAdvancedSettingsCategory = props.adminMode ? makeGetUserCategory(props.user.id) : makeGetCategory();
const getAdvancedSettingsCategory = makeGetCategory('getAdvancedSettingsCategory', Preferences.CATEGORY_ADVANCED_SETTINGS);
function makeMapStateToProps() {
const getUserAdvancedSettingsCategory = makeGetUserCategory('getAdvancedSettingsCategory', Preferences.CATEGORY_ADVANCED_SETTINGS);
return (state: GlobalState, props: OwnProps) => {
const config = getConfig(state);
@ -34,9 +36,10 @@ function makeMapStateToProps(state: GlobalState, props: OwnProps) {
const enableJoinLeaveMessage = config.EnableJoinLeaveMessageByDefault === 'true';
const userPreferences = props.adminMode && props.userPreferences ? props.userPreferences : undefined;
const advancedSettingsCategory = userPreferences ? getUserAdvancedSettingsCategory(state, props.user.id) : getAdvancedSettingsCategory(state);
return {
advancedSettingsCategory: getAdvancedSettingsCategory(state, Preferences.CATEGORY_ADVANCED_SETTINGS),
advancedSettingsCategory,
sendOnCtrlEnter: get(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', 'false', userPreferences),
codeBlockOnCtrlEnter: get(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'code_block_ctrl_enter', 'true', userPreferences),
formatting: get(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', 'true', userPreferences),

View File

@ -25,7 +25,7 @@ export type OwnProps = {
type Props = OwnProps & {
active: boolean;
areAllSectionsInactive: boolean;
joinLeave?: string;
joinLeave: string;
onUpdateSection: (section?: string) => void;
renderOnOffLabel: (label: string) => ReactNode;
actions: {
@ -34,7 +34,7 @@ type Props = OwnProps & {
}
type State = {
joinLeaveState?: string;
joinLeaveState: string;
isSaving?: boolean;
serverError?: string;
}

View File

@ -108,7 +108,7 @@ export default class AdvancedSettingsDisplay extends React.PureComponent<Props,
user_id: userId,
category: Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
name: setting,
value: this.state.settings[setting],
value: this.state.settings[setting]!,
});
});

View File

@ -6,27 +6,21 @@ import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
import {saveTheme, deleteTeamSpecificThemes} from 'mattermost-redux/actions/preferences';
import {getTheme, makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme, getThemePreferences} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/entities/teams';
import {openModal} from 'actions/views/modals';
import {Preferences} from 'utils/constants';
import type {GlobalState} from 'types/store';
import UserSettingsTheme from './user_settings_theme';
function makeMapStateToProps() {
const getThemeCategory = makeGetCategory();
return (state: GlobalState) => {
return {
currentTeamId: getCurrentTeamId(state),
theme: getTheme(state),
applyToAllTeams: getThemeCategory(state, Preferences.CATEGORY_THEME).length <= 1,
showAllTeamsCheckbox: getMyTeamsCount(state) > 1,
};
function mapStateToProps(state: GlobalState) {
return {
currentTeamId: getCurrentTeamId(state),
theme: getTheme(state),
applyToAllTeams: getThemePreferences(state).length <= 1,
showAllTeamsCheckbox: getMyTeamsCount(state) > 1,
};
}
@ -40,4 +34,4 @@ function mapDispatchToProps(dispatch: Dispatch) {
};
}
export default connect(makeMapStateToProps, mapDispatchToProps)(UserSettingsTheme);
export default connect(mapStateToProps, mapDispatchToProps)(UserSettingsTheme);

View File

@ -14,8 +14,8 @@ import {createCustomEmoji} from 'mattermost-redux/actions/emojis';
import * as Actions from 'mattermost-redux/actions/posts';
import {loadMe} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import {isPostFlagged} from 'mattermost-redux/selectors/entities/posts';
import type {GetStateFunc} from 'mattermost-redux/types/actions';
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
import mockStore from 'tests/test_store';
@ -1038,10 +1038,8 @@ describe('Actions.Posts', () => {
reply(200, OK_RESPONSE);
dispatch(Actions.flagPost(post1.id));
const state = getState();
const prefKey = getPreferenceKey(Preferences.CATEGORY_FLAGGED_POST, post1.id);
const preference = state.entities.preferences.myPreferences[prefKey];
expect(preference).toBeTruthy();
expect(isPostFlagged(getState(), post1.id)).toBe(true);
});
it('unflagPost', async () => {
@ -1069,20 +1067,15 @@ describe('Actions.Posts', () => {
put(`/${TestHelper.basicUser!.id}/preferences`).
reply(200, OK_RESPONSE);
dispatch(Actions.flagPost(post1.id));
let state = getState();
const prefKey = getPreferenceKey(Preferences.CATEGORY_FLAGGED_POST, post1.id);
const preference = state.entities.preferences.myPreferences[prefKey];
expect(preference).toBeTruthy();
expect(isPostFlagged(getState(), post1.id)).toBe(true);
nock(Client4.getUsersRoute()).
delete(`/${TestHelper.basicUser!.id}/preferences`).
reply(200, OK_RESPONSE);
dispatch(Actions.unflagPost(post1.id));
state = getState();
const unflagged = state.entities.preferences.myPreferences[prefKey];
if (unflagged) {
throw new Error('unexpected unflagged');
}
expect(isPostFlagged(getState(), post1.id)).toBe(false);
});
it('setUnreadPost', async () => {

View File

@ -1234,6 +1234,7 @@ export function unflagPost(postId: string): ActionFuncAsync {
user_id: currentUserId,
category: Preferences.CATEGORY_FLAGGED_POST,
name: postId,
value: 'true',
};
Client4.trackEvent('action', 'action_posts_unflag');

View File

@ -5,7 +5,7 @@ import type {PreferenceType} from '@mattermost/types/preferences';
import {PreferenceTypes} from 'mattermost-redux/action_types';
import {Client4} from 'mattermost-redux/client';
import {getMyPreferences as getMyPreferencesSelector, makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {getMyPreferences as getMyPreferencesSelector, getThemePreferences} from 'mattermost-redux/selectors/entities/preferences';
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import type {ActionFuncAsync, ThunkActionFunc} from 'mattermost-redux/types/actions';
@ -56,20 +56,6 @@ export function getUserPreferences(userID: string) {
});
}
export function setActionsMenuInitialisationState(initializationState: Record<string, boolean>): ThunkActionFunc<void> {
return async (dispatch, getState) => {
const state = getState();
const currentUserId = getCurrentUserId(state);
const preference: PreferenceType = {
user_id: currentUserId,
category: Preferences.CATEGORY_ACTIONS_MENU,
name: Preferences.NAME_ACTIONS_MENU_TUTORIAL_STATE,
value: JSON.stringify(initializationState),
};
await dispatch(savePreferences(currentUserId, [preference]));
};
}
export function setCustomStatusInitialisationState(initializationState: Record<string, boolean>): ThunkActionFunc<void> {
return async (dispatch, getState) => {
const state = getState();
@ -130,7 +116,7 @@ export function deleteTeamSpecificThemes(): ActionFuncAsync {
return async (dispatch, getState) => {
const state = getState();
const themePreferences: PreferenceType[] = makeGetCategory()(state, Preferences.CATEGORY_THEME);
const themePreferences: PreferenceType[] = getThemePreferences(state);
const currentUserId = getCurrentUserId(state);
const toDelete = themePreferences.filter((pref) => pref.name !== '');

View File

@ -37,10 +37,6 @@ const Preferences = {
MENTION_KEYS: 'mention_keys',
USE_MILITARY_TIME: 'use_military_time',
CATEGORY_ACTIONS_MENU: 'actions_menu',
NAME_ACTIONS_MENU_TUTORIAL_STATE: 'actions_menu_tutorial_state',
ACTIONS_MENU_VIEWED: 'actions_menu_modal_viewed',
CATEGORY_CUSTOM_STATUS: 'custom_status',
NAME_CUSTOM_STATUS_TUTORIAL_STATE: 'custom_status_tutorial_state',
NAME_RECENT_CUSTOM_STATUSES: 'recent_custom_statuses',
@ -82,6 +78,8 @@ const Preferences = {
HIDE_BATCH_EXPORT_CONFIRM_MODAL: 'hide_batch_export_confirm_modal',
HIDE_MYSQL_STATS_NOTIFICATION: 'hide_mysql_stats_notifcation',
CATEGORY_OVERAGE_USERS_BANNER: 'overage_users_banner',
CATEGORY_THEME: 'theme',
THEMES: {
denim: {

View File

@ -253,6 +253,11 @@ describe('Selectors.Posts', () => {
const testStateAny = deepFreezeAndThrowOnMutation({
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
users: {
currentUserId: userAny.id,
profiles: profilesAny,
@ -372,6 +377,11 @@ describe('Selectors.Posts', () => {
const testStateRoot = deepFreezeAndThrowOnMutation({
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
users: {
currentUserId: userRoot.id,
profiles: profilesRoot,
@ -485,6 +495,11 @@ describe('Selectors.Posts', () => {
const testStateNever = deepFreezeAndThrowOnMutation({
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
users: {
currentUserId: userNever.id,
profiles: profilesNever,
@ -601,6 +616,11 @@ describe('Selectors.Posts', () => {
const testStateAny = deepFreezeAndThrowOnMutation({
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
users: {
currentUserId: userAny.id,
profiles: profilesAny,
@ -678,6 +698,11 @@ describe('Selectors.Posts', () => {
const testStateAny = deepFreezeAndThrowOnMutation({
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
users: {
currentUserId: userAny.id,
profiles: profilesAny,
@ -1823,6 +1848,11 @@ describe('Selectors.Posts', () => {
};
const state = {
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
posts: {
posts: testPosts,
postsInChannel: {
@ -1852,6 +1882,11 @@ describe('Selectors.Posts', () => {
};
const state = {
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
posts: {
posts: testPosts,
postsInChannel: {
@ -1880,6 +1915,11 @@ describe('Selectors.Posts', () => {
channels: {
currentChannelId: 'abcd',
},
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
posts: {
posts: {},
postsInChannel: [],
@ -1910,6 +1950,11 @@ describe('Selectors.Posts', () => {
channels: {
currentChannelId: 'abcd',
},
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
posts: {
posts: postsAny,
postsInChannel: {
@ -2147,6 +2192,11 @@ describe('getPostsInCurrentChannel', () => {
channels: {
currentChannelId: 'channel1',
},
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
posts: {
posts: {},
postsInChannel: {},
@ -2177,6 +2227,11 @@ describe('getPostsInCurrentChannel', () => {
channels: {
currentChannelId: 'channel1',
},
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
posts: {
posts: {
post1,
@ -2214,6 +2269,11 @@ describe('getPostsInCurrentChannel', () => {
channels: {
currentChannelId: 'channel1',
},
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
posts: {
posts: {
post1,

View File

@ -24,11 +24,10 @@ import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentChannelId, getCurrentUser} from 'mattermost-redux/selectors/entities/common';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {getBool, shouldShowJoinLeaveMessages} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getUsers, getCurrentUserId, getUserStatuses} from 'mattermost-redux/selectors/entities/users';
import {createIdsSelector} from 'mattermost-redux/utils/helpers';
import {shouldShowJoinLeaveMessages} from 'mattermost-redux/utils/post_list';
import {
isPostEphemeral,
isSystemMessage,
@ -37,7 +36,6 @@ import {
isPostPendingOrFailed,
isPostCommentMention,
} from 'mattermost-redux/utils/post_utils';
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
import {isGuest} from 'mattermost-redux/utils/user_utils';
export function getAllPosts(state: GlobalState) {
@ -53,6 +51,10 @@ export function getPost(state: GlobalState, postId: Post['id']): Post {
return getAllPosts(state)[postId];
}
export function isPostFlagged(state: GlobalState, postId: Post['id']): boolean {
return getBool(state, Preferences.CATEGORY_FLAGGED_POST, postId);
}
export function getPostRepliesCount(state: GlobalState, postId: Post['id']): number {
return state.entities.posts.postsReplies[postId] || 0;
}
@ -344,18 +346,15 @@ export function makeGetPostsInChannel(): (state: GlobalState, channelId: Channel
getPostsInThread,
(state: GlobalState, channelId: Channel['id']) => getPostIdsInChannel(state, channelId),
getCurrentUser,
getMyPreferences,
shouldShowJoinLeaveMessages,
(state: GlobalState, channelId: Channel['id'], numPosts: number) => numPosts || Posts.POST_CHUNK_SIZE,
(allPosts, postsInThread, allPostIds, currentUser, myPreferences, numPosts) => {
(allPosts, postsInThread, allPostIds, currentUser, showJoinLeave, numPosts) => {
if (!allPostIds) {
return null;
}
const posts: PostWithFormatData[] = [];
const joinLeavePref = myPreferences[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)];
const showJoinLeave = joinLeavePref ? joinLeavePref.value !== 'false' : true;
const postIds = numPosts === -1 ? allPostIds : allPostIds.slice(0, numPosts);
for (let i = 0; i < postIds.length; i++) {
@ -388,15 +387,13 @@ export function makeGetPostsAroundPost(): (state: GlobalState, postId: Post['id'
getPostsInThread,
(state: GlobalState, focusedPostId) => focusedPostId,
getCurrentUser,
getMyPreferences,
(postIds, allPosts, postsInThread, focusedPostId, currentUser, myPreferences) => {
shouldShowJoinLeaveMessages,
(postIds, allPosts, postsInThread, focusedPostId, currentUser, showJoinLeave) => {
if (!postIds || !currentUser) {
return null;
}
const posts: PostWithFormatData[] = [];
const joinLeavePref = myPreferences[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)];
const showJoinLeave = joinLeavePref ? joinLeavePref.value !== 'false' : true;
for (let i = 0; i < postIds.length; i++) {
const post = allPosts[postIds[i]];
@ -565,13 +562,11 @@ export const getMostRecentPostIdInChannel: (state: GlobalState, channelId: Chann
'getMostRecentPostIdInChannel',
getAllPosts,
(state: GlobalState, channelId: string) => getPostIdsInChannel(state, channelId),
getMyPreferences,
(posts, postIdsInChannel, preferences) => {
shouldShowJoinLeaveMessages,
(posts, postIdsInChannel, allowSystemMessages) => {
if (!postIdsInChannel) {
return '';
}
const key = getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE);
const allowSystemMessages = preferences[key] ? preferences[key].value === 'true' : true;
if (!allowSystemMessages) {
// return the most recent non-system message in the channel

View File

@ -135,11 +135,6 @@ describe('Selectors.Preferences', () => {
});
});
it('get preferences by category', () => {
const getCategory = Selectors.makeGetCategory();
expect(getCategory(testState, category1)).toEqual([pref1]);
});
it('get direct channel show preferences', () => {
expect(Selectors.getDirectShowPreferences(testState)).toEqual([dmPref1, dmPref2]);
});
@ -599,6 +594,233 @@ describe('Selectors.Preferences', () => {
});
});
describe('makeGetCategory', () => {
const category1 = 'category1';
const category2 = 'category2';
const name1 = 'name1';
const name2 = 'name2';
function getBaseState() {
return deepFreezeAndThrowOnMutation({
entities: {
preferences: {
myPreferences: {
[getPreferenceKey(category1, name1)]: {
category: category1,
name: name1,
value: 'value1',
},
[getPreferenceKey(category2, name1)]: {
category: category2,
name: name1,
value: 'value2',
},
[getPreferenceKey(category1, name2)]: {
category: category1,
name: name2,
value: 'value3',
},
},
},
},
}) as GlobalState;
}
it('should return preferences in a category', () => {
const state = getBaseState();
const getCategory1 = Selectors.makeGetCategory('getCategory1', category1);
const getCategory2 = Selectors.makeGetCategory('getCategory2', category2);
expect(getCategory1(state)).toEqual([
{
category: category1,
name: name1,
value: 'value1',
},
{
category: category1,
name: name2,
value: 'value3',
},
]);
expect(getCategory2(state)).toEqual([
{
category: category2,
name: name1,
value: 'value2',
},
]);
});
it('should return the same preference objects unless they change', () => {
let state = getBaseState();
const preference1 = state.entities.preferences.myPreferences[getPreferenceKey(category1, name1)];
const preference2 = state.entities.preferences.myPreferences[getPreferenceKey(category1, name2)];
const getCategory1 = Selectors.makeGetCategory('getCategory1', category1);
expect(getCategory1(state)[0]).toBe(preference1);
expect(getCategory1(state)[1]).toBe(preference2);
state = mergeObjects(state, {
entities: {
preferences: {
myPreferences: {
[getPreferenceKey(category1, name1)]: {
category: category1,
name: name1,
value: 'new value',
},
},
},
},
});
expect(getCategory1(state)[0]).not.toBe(preference1);
expect(getCategory1(state)[1]).toBe(preference2);
});
it('should only return a new array when one of the preferences in that category changes', () => {
let state = getBaseState();
const getCategory1 = Selectors.makeGetCategory('getCategory1', category1);
const getCategory2 = Selectors.makeGetCategory('getCategory2', category2);
const originalResult1 = getCategory1(state);
const originalResult2 = getCategory2(state);
expect(getCategory1(state)).toBe(originalResult1);
state = mergeObjects(state, {
entities: {
preferences: {
myPreferences: {
[getPreferenceKey(category2, name2)]: {
category: category2,
name: name2,
value: 'value4',
},
},
},
},
});
expect(getCategory1(state)).toBe(originalResult1);
expect(getCategory2(state)).not.toBe(originalResult2);
state = mergeObjects(state, {
entities: {
preferences: {
myPreferences: {
[getPreferenceKey(category1, name1)]: {
category: category1,
name: name1,
value: 'new value',
},
},
},
},
});
expect(getCategory1(state)).not.toBe(originalResult1);
expect(getCategory2(state)).not.toBe(originalResult2);
});
});
describe('shouldShowJoinLeaveMessages', () => {
it('should default to true', () => {
const state = {
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
preferences: {
myPreferences: {},
},
},
} as unknown as GlobalState;
// Defaults to show post
const show = Selectors.shouldShowJoinLeaveMessages(state);
expect(show).toEqual(true);
});
it('set config to false, return false', () => {
const state = {
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'false',
},
},
preferences: {
myPreferences: {},
},
},
} as unknown as GlobalState;
// Defaults to show post
const show = Selectors.shouldShowJoinLeaveMessages(state);
expect(show).toEqual(false);
});
it('if user preference, set default wont be used', () => {
const state = {
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'false',
},
},
preferences: {
myPreferences: {
[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: {
category: Preferences.CATEGORY_ADVANCED_SETTINGS,
name: Preferences.ADVANCED_FILTER_JOIN_LEAVE,
value: 'true',
},
},
},
},
} as unknown as GlobalState;
// Defaults to show post
const show = Selectors.shouldShowJoinLeaveMessages(state);
expect(show).toEqual(true);
});
it('if user preference, set default wont be used', () => {
const state = {
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
preferences: {
myPreferences: {
[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: {
category: Preferences.CATEGORY_ADVANCED_SETTINGS,
name: Preferences.ADVANCED_FILTER_JOIN_LEAVE,
value: 'false',
},
},
},
},
} as unknown as GlobalState;
// Defaults to show post
const show = Selectors.shouldShowJoinLeaveMessages(state);
expect(show).toEqual(false);
});
});
describe('shouldShowUnreadsCategory', () => {
test('should return value from the preference if set', () => {
const state = {

View File

@ -8,7 +8,7 @@ import type {GlobalState} from '@mattermost/types/store';
import {General, Preferences} from 'mattermost-redux/constants';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getConfig, getFeatureFlagValue, getLicense} from 'mattermost-redux/selectors/entities/general';
import {createShallowSelector} from 'mattermost-redux/utils/helpers';
import {createIdsSelector, createShallowSelector} from 'mattermost-redux/utils/helpers';
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
import {setThemeDefaults} from 'mattermost-redux/utils/theme_utils';
@ -20,19 +20,18 @@ export function getUserPreferences(state: GlobalState, userID: string): Preferen
return state.entities.preferences.userPreferences[userID];
}
export function get(state: GlobalState, category: string, name: string, defaultValue: any = '', preferences?: PreferencesType) {
function getPreferenceObject(state: GlobalState, category: string, name: string): PreferenceType | undefined {
return getMyPreferences(state)[getPreferenceKey(category, name)];
}
export function get(state: GlobalState, category: string, name: string, defaultValue = '', preferences?: PreferencesType): string {
if (preferences) {
return getFromPreferences(preferences, category, name, defaultValue);
}
const key = getPreferenceKey(category, name);
const prefs = getMyPreferences(state);
const pref = getPreferenceObject(state, category, name);
if (!(key in prefs)) {
return defaultValue;
}
return prefs[key].value;
return pref ? pref.value : defaultValue;
}
export function getFromPreferences(preferences: PreferencesType, category: string, name: string, defaultValue: any = '') {
@ -51,16 +50,15 @@ export function getBool(state: GlobalState, category: string, name: string, defa
}
export function getInt(state: GlobalState, category: string, name: string, defaultValue = 0, userPreferences?: PreferencesType): number {
const value = get(state, category, name, defaultValue, userPreferences);
const value = get(state, category, name, String(defaultValue), userPreferences);
return parseInt(value, 10);
}
export function makeGetCategory(): (state: GlobalState, category: string) => PreferenceType[] {
return createSelector(
'makeGetCategory',
export function makeGetCategory(selectorName: string, category: string): (state: GlobalState) => PreferenceType[] {
return createIdsSelector(
selectorName,
getMyPreferences,
(state: GlobalState, category: string) => category,
(preferences, category) => {
(preferences) => {
const prefix = category + '--';
const prefsInCategory: PreferenceType[] = [];
@ -75,12 +73,11 @@ export function makeGetCategory(): (state: GlobalState, category: string) => Pre
);
}
export function makeGetUserCategory(userID: string): (state: GlobalState, category: string) => PreferenceType[] {
return createSelector(
'makeGetCategory',
(state) => getUserPreferences(state, userID),
(state: GlobalState, category: string) => category,
(preferences, category) => {
export function makeGetUserCategory(selectorName: string, category: string): (state: GlobalState, userID: string) => PreferenceType[] {
return createIdsSelector(
selectorName,
(state: GlobalState, userID: string) => getUserPreferences(state, userID),
(preferences) => {
const prefix = category + '--';
const prefsInCategory: PreferenceType[] = [];
@ -95,28 +92,18 @@ export function makeGetUserCategory(userID: string): (state: GlobalState, catego
);
}
const getDirectShowCategory = makeGetCategory();
export function getDirectShowPreferences(state: GlobalState) {
return getDirectShowCategory(state, Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
}
const getGroupShowCategory = makeGetCategory();
export function getGroupShowPreferences(state: GlobalState) {
return getGroupShowCategory(state, Preferences.CATEGORY_GROUP_CHANNEL_SHOW);
}
export const getDirectShowPreferences = makeGetCategory('getDirectShowPreferences', Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
export const getGroupShowPreferences = makeGetCategory('getGroupShowPreferences', Preferences.CATEGORY_GROUP_CHANNEL_SHOW);
export const getTeammateNameDisplaySetting: (state: GlobalState) => string = createSelector(
'getTeammateNameDisplaySetting',
getConfig,
getMyPreferences,
(state) => getPreferenceObject(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT),
getLicense,
(config, preferences, license) => {
(config, teammateNameDisplayPreference, license) => {
const useAdminTeammateNameDisplaySetting = (license && license.LockTeammateNameDisplay === 'true') && config.LockTeammateNameDisplay === 'true';
const key = getPreferenceKey(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT);
if (preferences[key] && !useAdminTeammateNameDisplaySetting) {
return preferences[key].value || '';
if (teammateNameDisplayPreference && !useAdminTeammateNameDisplaySetting) {
return teammateNameDisplayPreference.value || '';
} else if (config.TeammateNameDisplay) {
return config.TeammateNameDisplay;
}
@ -124,6 +111,8 @@ export const getTeammateNameDisplaySetting: (state: GlobalState) => string = cre
},
);
export const getThemePreferences = makeGetCategory('getThemePreferences', Preferences.CATEGORY_THEME);
const getThemePreference = createSelector(
'getThemePreference',
getMyPreferences,
@ -218,6 +207,14 @@ export function makeGetStyleFromTheme<Style>(): (state: GlobalState, getStyleFro
);
}
export function shouldShowJoinLeaveMessages(state: GlobalState) {
const config = getConfig(state);
const enableJoinLeaveMessage = config.EnableJoinLeaveMessageByDefault === 'true';
// This setting is true or not set if join/leave messages are to be displayed
return getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE, enableJoinLeaveMessage);
}
// shouldShowUnreadsCategory returns true if the user has unereads grouped separately with the new sidebar enabled.
export const shouldShowUnreadsCategory: (state: GlobalState, userPreferences?: PreferencesType) => boolean = createSelector(
'shouldShowUnreadsCategory',
@ -336,3 +333,5 @@ export function moveThreadsEnabled(state: GlobalState): boolean {
export function streamlinedMarketplaceEnabled(state: GlobalState): boolean {
return getFeatureFlagValue(state, 'StreamlinedMarketplace') === 'true';
}
export const getOverageBannerPreferences = makeGetCategory('getOverageBannerPreferences', Preferences.CATEGORY_OVERAGE_USERS_BANNER);

View File

@ -23,7 +23,6 @@ import {
makeGenerateCombinedPost,
extractUserActivityData,
START_OF_NEW_MESSAGES,
shouldShowJoinLeaveMessages,
} from './post_list';
import TestHelper from '../../test/test_helper';
@ -1688,95 +1687,3 @@ describe('combineUserActivityData', () => {
expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput);
});
});
describe('shouldShowJoinLeaveMessages', () => {
it('should default to true', () => {
const state = {
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
preferences: {
myPreferences: {},
},
},
} as unknown as GlobalState;
// Defaults to show post
const show = shouldShowJoinLeaveMessages(state);
expect(show).toEqual(true);
});
it('set config to false, return false', () => {
const state = {
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'false',
},
},
preferences: {
myPreferences: {},
},
},
} as unknown as GlobalState;
// Defaults to show post
const show = shouldShowJoinLeaveMessages(state);
expect(show).toEqual(false);
});
it('if user preference, set default wont be used', () => {
const state = {
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'false',
},
},
preferences: {
myPreferences: {
[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: {
category: Preferences.CATEGORY_ADVANCED_SETTINGS,
name: Preferences.ADVANCED_FILTER_JOIN_LEAVE,
value: 'true',
},
},
},
},
} as unknown as GlobalState;
// Defaults to show post
const show = shouldShowJoinLeaveMessages(state);
expect(show).toEqual(true);
});
it('if user preference, set default wont be used', () => {
const state = {
entities: {
general: {
config: {
EnableJoinLeaveMessageByDefault: 'true',
},
},
preferences: {
myPreferences: {
[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: {
category: Preferences.CATEGORY_ADVANCED_SETTINGS,
name: Preferences.ADVANCED_FILTER_JOIN_LEAVE,
value: 'false',
},
},
},
},
} as unknown as GlobalState;
// Defaults to show post
const show = shouldShowJoinLeaveMessages(state);
expect(show).toEqual(false);
});
});

View File

@ -6,12 +6,11 @@ import moment from 'moment-timezone';
import type {ActivityEntry, Post} from '@mattermost/types/posts';
import type {GlobalState} from '@mattermost/types/store';
import {Posts, Preferences} from 'mattermost-redux/constants';
import {Posts} from 'mattermost-redux/constants';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {makeGetPostsForIds} from 'mattermost-redux/selectors/entities/posts';
import type {UserActivityPost} from 'mattermost-redux/selectors/entities/posts';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {shouldShowJoinLeaveMessages} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {createIdsSelector, memoizeResult} from 'mattermost-redux/utils/helpers';
import {isUserActivityPost, shouldFilterJoinLeavePost, isFromWebhook} from 'mattermost-redux/utils/post_utils';
@ -23,14 +22,6 @@ export const DATE_LINE = 'date-';
export const START_OF_NEW_MESSAGES = 'start-of-new-messages-';
export const MAX_COMBINED_SYSTEM_POSTS = 100;
export function shouldShowJoinLeaveMessages(state: GlobalState) {
const config = getConfig(state);
const enableJoinLeaveMessage = config.EnableJoinLeaveMessageByDefault === 'true';
// This setting is true or not set if join/leave messages are to be displayed
return getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE, enableJoinLeaveMessage);
}
interface PostFilterOptions {
postIds: string[];
lastViewedAt: number;

View File

@ -3,24 +3,14 @@
import type {Channel} from '@mattermost/types/channels';
import type {Post, PostType, PostMetadata, PostEmbed} from '@mattermost/types/posts';
import type {PreferenceType} from '@mattermost/types/preferences';
import type {GlobalState} from '@mattermost/types/store';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
import {getPreferenceKey} from './preference_utils';
import {Posts, Preferences, Permissions} from '../constants';
export function isPostFlagged(postId: Post['id'], myPreferences: {
[x: string]: PreferenceType;
}): boolean {
const key = getPreferenceKey(Preferences.CATEGORY_FLAGGED_POST, postId);
return myPreferences.hasOwnProperty(key);
}
export function isSystemMessage(post: Post): boolean {
return Boolean(post.type && post.type.startsWith(Posts.SYSTEM_MESSAGE_PREFIX));
}

View File

@ -1,18 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Preferences} from 'mattermost-redux/constants';
import {get} from 'mattermost-redux/selectors/entities/preferences';
import {getIsMobileView} from 'selectors/views/browser';
import type {GlobalState} from 'types/store';
export function showActionsDropdownPulsatingDot(state: GlobalState): boolean {
if (getIsMobileView(state)) {
return false;
}
const actionsMenuTutorialState = get(state, Preferences.CATEGORY_ACTIONS_MENU, Preferences.NAME_ACTIONS_MENU_TUTORIAL_STATE, false);
const modalAlreadyViewed = actionsMenuTutorialState && JSON.parse(actionsMenuTutorialState)[Preferences.ACTIONS_MENU_VIEWED];
return !modalAlreadyViewed;
}

View File

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {makeGetCategory, getBool} from 'mattermost-redux/selectors/entities/preferences';
import {get as getString, getBool, makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
import {getIsMobileView} from 'selectors/views/browser';
@ -13,21 +13,8 @@ import {RecommendedNextStepsLegacy, Preferences} from 'utils/constants';
import type {GlobalState} from 'types/store';
const getABTestCategory = makeGetCategory();
export const getABTestPreferences = (() => {
return (state: GlobalState) => getABTestCategory(state, Preferences.AB_TEST_PREFERENCE_VALUE);
})();
const getFirstChannelNamePref = createSelector(
'getFirstChannelNamePref',
getABTestPreferences,
(preferences) => {
return preferences.find((pref) => pref.name === RecommendedNextStepsLegacy.CREATE_FIRST_CHANNEL);
},
);
export function getFirstChannelName(state: GlobalState) {
return getFirstChannelNamePref(state)?.value || '';
return getString(state, Preferences.AB_TEST_PREFERENCE_VALUE, RecommendedNextStepsLegacy.CREATE_FIRST_CHANNEL, '');
}
export function getShowLaunchingWorkspace(state: GlobalState) {
@ -91,13 +78,13 @@ const getSteps = createSelector(
},
);
const getNextStepsCategory = makeGetCategory();
const getOnboardingTaskCategory = makeGetCategory();
const getNextStepsPreferences = makeGetCategory('getNextStepsPreferences', Preferences.RECOMMENDED_NEXT_STEPS);
export const getOnboardingTaskPreferences = makeGetCategory('getOnboardingTaskPreferences', OnboardingTaskCategory);
// Loop through all Steps. For each step, check that
export const legacyNextStepsNotFinished = createSelector(
'legacyNextStepsNotFinished',
(state: GlobalState) => getNextStepsCategory(state, Preferences.RECOMMENDED_NEXT_STEPS),
getNextStepsPreferences,
(state: GlobalState) => getCurrentUser(state),
(state: GlobalState) => isFirstAdmin(state),
(state: GlobalState) => getSteps(state),
@ -111,7 +98,7 @@ export const legacyNextStepsNotFinished = createSelector(
// Loop through all Steps. For each step, check that
export const hasLegacyNextStepsPreferences = createSelector(
'hasLegacyNextStepsPreferences',
(state: GlobalState) => getNextStepsCategory(state, Preferences.RECOMMENDED_NEXT_STEPS),
getNextStepsPreferences,
(state: GlobalState) => getSteps(state),
(stepPreferences, mySteps) => {
const checkPref = (step: StepType) => stepPreferences.some((pref) => (pref.name === step.id));
@ -121,8 +108,8 @@ export const hasLegacyNextStepsPreferences = createSelector(
export const getShowTaskListBool = createSelector(
'getShowTaskListBool',
(state: GlobalState) => getOnboardingTaskCategory(state, OnboardingTaskCategory),
(state: GlobalState) => getNextStepsCategory(state, Preferences.RECOMMENDED_NEXT_STEPS),
getOnboardingTaskPreferences,
getNextStepsPreferences,
getIsMobileView,
(state: GlobalState) => getBool(state, OnboardingTaskCategory, OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW),
(state: GlobalState) => hasLegacyNextStepsPreferences(state),

View File

@ -103,7 +103,6 @@ export const Preferences = {
UNREAD_SCROLL_POSITION_START_FROM_LEFT: 'start_from_left_off',
UNREAD_SCROLL_POSITION_START_FROM_NEWEST: 'start_from_newest',
CATEGORY_THEME: 'theme',
CATEGORY_FLAGGED_POST: 'flagged_post',
CATEGORY_NOTIFICATIONS: 'notifications',
EMAIL_INTERVAL: 'email_interval',
INTERVAL_IMMEDIATE: 30, // "immediate" is a 30 second interval
@ -147,7 +146,7 @@ export const Preferences = {
DELINQUENCY_MODAL_CONFIRMED: 'delinquency_modal_confirmed',
CONFIGURATION_BANNERS: 'configuration_banners',
NOTIFY_ADMIN_REVOKE_DOWNGRADED_WORKSPACE: 'admin_revoke_downgraded_instance',
OVERAGE_USERS_BANNER: 'overage_users_banner',
OVERAGE_USERS_BANNER: ReduxPreferences.CATEGORY_OVERAGE_USERS_BANNER,
TO_CLOUD_YEARLY_PLAN_NUDGE: 'to_cloud_yearly_plan_nudge',
TO_PAID_PLAN_NUDGE: 'to_paid_plan_nudge',
CLOUD_ANNUAL_RENEWAL_BANNER: 'cloud_annual_renewal_banner',

View File

@ -21,8 +21,8 @@ import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getAllGroupsForReferenceByName} from 'mattermost-redux/selectors/entities/groups';
import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts';
import {get, getTeammateNameDisplaySetting, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {isPostFlagged, makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts';
import {getTeammateNameDisplaySetting, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
import {getCurrentTeamId, getTeam} from 'mattermost-redux/selectors/entities/teams';
import {makeGetDisplayName, getCurrentUserId, getUser, getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
@ -36,7 +36,7 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {getEmojiMap} from 'selectors/emojis';
import {getIsMobileView} from 'selectors/views/browser';
import Constants, {PostListRowListIds, Preferences} from 'utils/constants';
import Constants, {PostListRowListIds} from 'utils/constants';
import * as Keyboard from 'utils/keyboard';
import {formatWithRenderer} from 'utils/markdown';
import MentionableRenderer from 'utils/markdown/mentionable_renderer';
@ -471,8 +471,8 @@ export function usePostAriaLabel(post: Post | undefined) {
}
const authorDisplayName = getDisplayName(state, post.user_id);
const reactions = getReactionsForPost(state, post?.id);
const isFlagged = get(state, Preferences.CATEGORY_FLAGGED_POST, post.id, null) != null;
const reactions = getReactionsForPost(state, post.id);
const isFlagged = isPostFlagged(state, post.id);
const emojiMap = getEmojiMap(state);
const mentions = getMentionsFromMessage(state, post);
const teammateNameDisplaySetting = getTeammateNameDisplaySetting(state);

View File

@ -5,7 +5,7 @@ export type PreferenceType = {
category: string;
name: string;
user_id: string;
value?: string;
value: string;
};
export type PreferencesType = {