diff --git a/server/channels/api4/report.go b/server/channels/api4/report.go
index 7a06de1f62..9185b1284a 100644
--- a/server/channels/api4/report.go
+++ b/server/channels/api4/report.go
@@ -21,7 +21,7 @@ func (api *API) InitReports() {
}
func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
- if !(c.IsSystemAdmin()) {
+ if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return
}
@@ -52,7 +52,7 @@ func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getUserCountForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
- if !(c.IsSystemAdmin()) {
+ if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return
}
diff --git a/server/channels/app/user.go b/server/channels/app/user.go
index 121ad8cf4c..fb84335464 100644
--- a/server/channels/app/user.go
+++ b/server/channels/app/user.go
@@ -864,7 +864,7 @@ func (a *App) SetDefaultProfileImage(c request.CTX, user *model.User) *model.App
}
options := a.Config().GetSanitizeOptions()
- updatedUser.SanitizeProfile(options)
+ updatedUser.SanitizeProfile(options, false)
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", nil, "")
message.Add("user", updatedUser)
@@ -1117,7 +1117,7 @@ func (a *App) GetSanitizeOptions(asAdmin bool) map[string]bool {
func (a *App) SanitizeProfile(user *model.User, asAdmin bool) {
options := a.ch.srv.userService.GetSanitizeOptions(asAdmin)
- user.SanitizeProfile(options)
+ user.SanitizeProfile(options, asAdmin)
}
func (a *App) UpdateUserAsUser(c request.CTX, user *model.User, asAdmin bool) (*model.User, *model.AppError) {
@@ -2558,7 +2558,7 @@ func (a *App) invalidateUserCacheAndPublish(rctx request.CTX, userID string) {
}
options := a.Config().GetSanitizeOptions()
- user.SanitizeProfile(options)
+ user.SanitizeProfile(options, false)
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", nil, "")
message.Add("user", user)
diff --git a/server/channels/app/users/utils.go b/server/channels/app/users/utils.go
index e708a606d7..0c8368974c 100644
--- a/server/channels/app/users/utils.go
+++ b/server/channels/app/users/utils.go
@@ -42,7 +42,7 @@ func (us *UserService) sanitizeProfiles(users []*model.User, asAdmin bool) []*mo
func (us *UserService) SanitizeProfile(user *model.User, asAdmin bool) {
options := us.GetSanitizeOptions(asAdmin)
- user.SanitizeProfile(options)
+ user.SanitizeProfile(options, asAdmin)
}
func (us *UserService) GetSanitizeOptions(asAdmin bool) map[string]bool {
diff --git a/server/channels/store/sqlstore/post_store.go b/server/channels/store/sqlstore/post_store.go
index d4cc9bb6a0..519c09d527 100644
--- a/server/channels/store/sqlstore/post_store.go
+++ b/server/channels/store/sqlstore/post_store.go
@@ -1144,7 +1144,7 @@ func (s *SqlPostStore) prepareThreadedResponse(posts []*postWithExtra, extended,
return nil, err
}
for _, user := range users {
- user.SanitizeProfile(sanitizeOptions)
+ user.SanitizeProfile(sanitizeOptions, false)
usersMap[user.Id] = user
}
} else {
diff --git a/server/public/model/report.go b/server/public/model/report.go
index e8587e448f..022f58207f 100644
--- a/server/public/model/report.go
+++ b/server/public/model/report.go
@@ -144,7 +144,7 @@ func (u *UserReportOptions) IsValid() *AppError {
}
func (u *UserReportQuery) ToReport() *UserReport {
- u.ClearNonProfileFields()
+ u.ClearNonProfileFields(false)
return &UserReport{
User: u.User,
UserPostStats: u.UserPostStats,
diff --git a/server/public/model/user.go b/server/public/model/user.go
index 5c3ad3d40b..b1882e2bd5 100644
--- a/server/public/model/user.go
+++ b/server/public/model/user.go
@@ -695,19 +695,22 @@ func (u *User) SanitizeInput(isAdmin bool) {
u.Email = strings.TrimSpace(u.Email)
}
-func (u *User) ClearNonProfileFields() {
+func (u *User) ClearNonProfileFields(asAdmin bool) {
u.Password = ""
u.AuthData = NewString("")
u.MfaSecret = ""
u.EmailVerified = false
u.AllowMarketing = false
- u.NotifyProps = StringMap{}
u.LastPasswordUpdate = 0
u.FailedAttempts = 0
+
+ if !asAdmin {
+ u.NotifyProps = StringMap{}
+ }
}
-func (u *User) SanitizeProfile(options map[string]bool) {
- u.ClearNonProfileFields()
+func (u *User) SanitizeProfile(options map[string]bool, asAdmin bool) {
+ u.ClearNonProfileFields(asAdmin)
u.Sanitize(options)
}
diff --git a/server/public/model/user_test.go b/server/public/model/user_test.go
index df17fc3157..0a8ad7faaa 100644
--- a/server/public/model/user_test.go
+++ b/server/public/model/user_test.go
@@ -551,12 +551,12 @@ func TestSanitizeProfile(t *testing.T) {
Props: StringMap{UserPropsKeyRemoteEmail: "remote@doe.com"},
}
- user.SanitizeProfile(nil)
+ user.SanitizeProfile(nil, false)
require.Equal(t, "john@doe.com", user.Email)
require.Equal(t, "remote@doe.com", user.Props[UserPropsKeyRemoteEmail])
- user.SanitizeProfile(map[string]bool{"email": false})
+ user.SanitizeProfile(map[string]bool{"email": false}, false)
require.Empty(t, user.Email)
require.Empty(t, user.Props[UserPropsKeyRemoteEmail])
diff --git a/webapp/channels/src/components/admin_console/admin_user_card/admin_user_card.scss b/webapp/channels/src/components/admin_console/admin_user_card/admin_user_card.scss
index f3134e0315..ec8e62ca49 100644
--- a/webapp/channels/src/components/admin_console/admin_user_card/admin_user_card.scss
+++ b/webapp/channels/src/components/admin_console/admin_user_card/admin_user_card.scss
@@ -17,7 +17,7 @@
height: 92px;
flex-direction: row;
align-items: flex-start;
- padding: 30px 20px 12px 30px;
+ padding: 0 20px 0 30px;
background-color: #295eb9;
}
@@ -31,18 +31,23 @@
}
.AdminUserCard__footer {
+ display: flex;
+ flex-direction: row;
padding: 20px;
border-top: solid 1px rgba(0, 0, 0, 0.2);
background-color: #fff;
}
.AdminUserCard__user-info {
+ overflow: hidden;
+ min-width: 0;
align-self: flex-end;
padding: 0;
margin-left: 20px;
color: #fff;
font-size: 20px;
font-weight: normal;
+ text-overflow: ellipsis;
}
.AdminUserCard__user-nickname {
diff --git a/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap b/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap
index bc27af1ea3..91432c72cc 100644
--- a/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap
@@ -411,3 +411,630 @@ exports[`SystemUserDetail should match snapshot if MFA is enabled 1`] = `
/>
`;
+
+exports[`SystemUserDetail should not show manage user settings button when user doesnt have permission 1`] = `
+
+
+
+
+
+
+
+
+
+ }
+ subtitle={
+ Object {
+ "defaultMessage": "Teams to which this user belongs",
+ "id": "admin.userManagement.userDetail.teamsSubtitle",
+ }
+ }
+ title={
+ Object {
+ "defaultMessage": "Team Membership",
+ "id": "admin.userManagement.userDetail.teamsTitle",
+ }
+ }
+ >
+
+
+
+
+
+
+
+
+ }
+ disabled={true}
+ extraClasses=""
+ onClick={[Function]}
+ saving={false}
+ savingMessage={
+
+ }
+ />
+
+
+
+
+
+ }
+ message={
+
+
+
+
+
+
+
+
+ }
+ modalClass=""
+ onCancel={[Function]}
+ onConfirm={[Function]}
+ show={false}
+ title={
+
+ }
+ />
+
+`;
+
+exports[`SystemUserDetail should show manage user settings button as activated 1`] = `
+
+
+
+
+
+
+
+
+
+ }
+ subtitle={
+ Object {
+ "defaultMessage": "Teams to which this user belongs",
+ "id": "admin.userManagement.userDetail.teamsSubtitle",
+ }
+ }
+ title={
+ Object {
+ "defaultMessage": "Team Membership",
+ "id": "admin.userManagement.userDetail.teamsTitle",
+ }
+ }
+ >
+
+
+
+
+
+
+
+
+ }
+ disabled={true}
+ extraClasses=""
+ onClick={[Function]}
+ saving={false}
+ savingMessage={
+
+ }
+ />
+
+
+
+
+
+ }
+ message={
+
+
+
+
+
+
+
+
+ }
+ modalClass=""
+ onCancel={[Function]}
+ onConfirm={[Function]}
+ show={false}
+ title={
+
+ }
+ />
+
+`;
+
+exports[`SystemUserDetail should show manage user settings button as disabled when no license 1`] = `
+
+
+
+
+
+
+
+
+
+ }
+ subtitle={
+ Object {
+ "defaultMessage": "Teams to which this user belongs",
+ "id": "admin.userManagement.userDetail.teamsSubtitle",
+ }
+ }
+ title={
+ Object {
+ "defaultMessage": "Team Membership",
+ "id": "admin.userManagement.userDetail.teamsTitle",
+ }
+ }
+ >
+
+
+
+
+
+
+
+
+ }
+ disabled={true}
+ extraClasses=""
+ onClick={[Function]}
+ saving={false}
+ savingMessage={
+
+ }
+ />
+
+
+
+
+
+ }
+ message={
+
+
+
+
+
+
+
+
+ }
+ modalClass=""
+ onCancel={[Function]}
+ onConfirm={[Function]}
+ show={false}
+ title={
+
+ }
+ />
+
+`;
diff --git a/webapp/channels/src/components/admin_console/system_user_detail/index.ts b/webapp/channels/src/components/admin_console/system_user_detail/index.ts
index 3fed0ac32b..6efbb009b1 100644
--- a/webapp/channels/src/components/admin_console/system_user_detail/index.ts
+++ b/webapp/channels/src/components/admin_console/system_user_detail/index.ts
@@ -6,20 +6,27 @@ import {connect} from 'react-redux';
import type {GlobalState} from '@mattermost/types/store';
+import {getUserPreferences} from 'mattermost-redux/actions/preferences';
import {addUserToTeam} from 'mattermost-redux/actions/teams';
import {updateUserActive, getUser, patchUser, updateUserMfa} from 'mattermost-redux/actions/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {setNavigationBlocked} from 'actions/admin_actions.jsx';
import {openModal} from 'actions/views/modals';
+import {getShowLockedManageUserSettings, getShowManageUserSettings} from 'selectors/admin_console';
import SystemUserDetail from './system_user_detail';
function mapStateToProps(state: GlobalState) {
const config = getConfig(state);
+ const showManageUserSettings = getShowManageUserSettings(state);
+ const showLockedManageUserSettings = getShowLockedManageUserSettings(state);
+
return {
mfaEnabled: config?.EnableMultifactorAuthentication === 'true' || false,
+ showManageUserSettings,
+ showLockedManageUserSettings,
};
}
@@ -31,6 +38,7 @@ const mapDispatchToProps = {
addUserToTeam,
setNavigationBlocked,
openModal,
+ getUserPreferences,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
diff --git a/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.scss b/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.scss
index 58925effbf..bdef0d7144 100644
--- a/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.scss
+++ b/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.scss
@@ -37,4 +37,11 @@
align-items: center;
justify-content: center;
}
+
+ .AdminUserCard__footer {
+ .manageUserSettingsBtn {
+ margin-left: auto;
+ cursor: pointer;
+ }
+ }
}
diff --git a/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.test.tsx b/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.test.tsx
index c87ea1f29e..9d9b0096c5 100644
--- a/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.test.tsx
+++ b/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.test.tsx
@@ -16,6 +16,8 @@ import {shallowWithIntl, type MockIntl} from 'tests/helpers/intl-test-helper';
describe('SystemUserDetail', () => {
const defaultProps: Props = {
+ showManageUserSettings: false,
+ showLockedManageUserSettings: false,
mfaEnabled: false,
patchUser: jest.fn(),
updateUserMfa: jest.fn(),
@@ -24,6 +26,7 @@ describe('SystemUserDetail', () => {
setNavigationBlocked: jest.fn(),
addUserToTeam: jest.fn(),
openModal: jest.fn(),
+ getUserPreferences: jest.fn(),
intl: {
formatMessage: jest.fn(),
} as MockIntl,
@@ -50,6 +53,33 @@ describe('SystemUserDetail', () => {
const wrapper = shallowWithIntl( );
expect(wrapper).toMatchSnapshot();
});
+
+ test('should show manage user settings button as activated', () => {
+ const props = {
+ ...defaultProps,
+ showManageUserSettings: true,
+ };
+ const wrapper = shallowWithIntl( );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should show manage user settings button as disabled when no license', () => {
+ const props = {
+ ...defaultProps,
+ showLockedManageUserSettings: false,
+ };
+ const wrapper = shallowWithIntl( );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should not show manage user settings button when user doesnt have permission', () => {
+ const props = {
+ ...defaultProps,
+ showManageUserSettings: false,
+ };
+ const wrapper = shallowWithIntl( );
+ expect(wrapper).toMatchSnapshot();
+ });
});
describe('getUserAuthenticationTextField', () => {
diff --git a/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.tsx b/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.tsx
index 0e47c8ec4c..a11bbf26b5 100644
--- a/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.tsx
+++ b/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.tsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import classNames from 'classnames';
import React, {PureComponent} from 'react';
import type {ChangeEvent, MouseEvent} from 'react';
import type {IntlShape, WrappedComponentProps} from 'react-intl';
@@ -18,16 +19,19 @@ import AdminUserCard from 'components/admin_console/admin_user_card/admin_user_c
import BlockableLink from 'components/admin_console/blockable_link';
import ResetPasswordModal from 'components/admin_console/reset_password_modal';
import TeamList from 'components/admin_console/system_user_detail/team_list';
+import {ConfirmManageUserSettingsModal} from 'components/admin_console/system_users/system_users_list_actions/confirmManageUserSettingsModal';
import ConfirmModal from 'components/confirm_modal';
import FormError from 'components/form_error';
import SaveButton from 'components/save_button';
import TeamSelectorModal from 'components/team_selector_modal';
+import UserSettingsModal from 'components/user_settings/modal';
import AdminHeader from 'components/widgets/admin_console/admin_header';
import AdminPanel from 'components/widgets/admin_console/admin_panel';
import AtIcon from 'components/widgets/icons/at_icon';
import EmailIcon from 'components/widgets/icons/email_icon';
import SheidOutlineIcon from 'components/widgets/icons/shield_outline_icon';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
+import WithTooltip from 'components/with_tooltip';
import {Constants, ModalIdentifiers} from 'utils/constants';
import {toTitleCase} from 'utils/utils';
@@ -284,6 +288,37 @@ export class SystemUserDetail extends PureComponent {
this.setState({showTeamSelectorModal: false});
};
+ openConfirmEditUserSettingsModal = () => {
+ if (!this.state.user) {
+ return;
+ }
+
+ this.props.openModal({
+ modalId: ModalIdentifiers.CONFIRM_MANAGE_USER_SETTINGS_MODAL,
+ dialogType: ConfirmManageUserSettingsModal,
+ dialogProps: {
+ user: this.state.user,
+ onConfirm: this.openUserSettingsModal,
+ },
+ });
+ };
+
+ openUserSettingsModal = async () => {
+ if (!this.state.user) {
+ return;
+ }
+
+ this.props.openModal({
+ modalId: ModalIdentifiers.USER_SETTINGS,
+ dialogType: UserSettingsModal,
+ dialogProps: {
+ adminMode: true,
+ isContentProductSettings: true,
+ userID: this.state.user.id,
+ },
+ });
+ };
+
render() {
return (
@@ -385,14 +420,61 @@ export class SystemUserDetail extends PureComponent
{
/>
)}
+
+ {
+ this.props.showManageUserSettings &&
+
+
+
+ }
+
+ {
+ this.props.showLockedManageUserSettings &&
+
+
+
+
+
+
+
+
+ }
>
}
/>
{/* User's team details */}
{
onConfirm={this.handleDeactivateMember}
onCancel={this.toggleCloseModalDeactivateMember}
/>
+
{this.state.showTeamSelectorModal && (
void;
+ onExited: () => void;
+ onHide: () => void;
+}
+
+export function ConfirmManageUserSettingsModal(props: Props) {
+ const title = (
+
+ );
+
+ const message = (
+ (<> {x}>),
+ }}
+ />
+ );
+
+ const confirmButtonText = (
+
+ );
+
+ return (
+
+ );
+}
diff --git a/webapp/channels/src/components/admin_console/system_users/system_users_list_actions/index.tsx b/webapp/channels/src/components/admin_console/system_users/system_users_list_actions/index.tsx
index f9b6bbe682..3cb7e71139 100644
--- a/webapp/channels/src/components/admin_console/system_users/system_users_list_actions/index.tsx
+++ b/webapp/channels/src/components/admin_console/system_users/system_users_list_actions/index.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import classNames from 'classnames';
-import React from 'react';
+import React, {useCallback} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
@@ -18,14 +18,19 @@ import {isSystemAdmin, isGuest} from 'mattermost-redux/utils/user_utils';
import {adminResetMfa} from 'actions/admin_actions';
import {openModal} from 'actions/views/modals';
+import {getShowManageUserSettings} from 'selectors/admin_console';
import ManageRolesModal from 'components/admin_console/manage_roles_modal';
import ManageTeamsModal from 'components/admin_console/manage_teams_modal';
import ManageTokensModal from 'components/admin_console/manage_tokens_modal';
import ResetEmailModal from 'components/admin_console/reset_email_modal';
import ResetPasswordModal from 'components/admin_console/reset_password_modal';
+import {
+ ConfirmManageUserSettingsModal,
+} from 'components/admin_console/system_users/system_users_list_actions/confirmManageUserSettingsModal';
import * as Menu from 'components/menu';
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
+import UserSettingsModal from 'components/user_settings/modal';
import Constants, {ModalIdentifiers} from 'utils/constants';
@@ -49,6 +54,7 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
const dispatch = useDispatch();
const config = useSelector(getConfig);
const isLicensed = useSelector(getLicense)?.IsLicensed === 'true';
+ const showManageUserSettings = useSelector(getShowManageUserSettings);
function getTranslatedUserRole(userRoles: UserProfile['roles']) {
if (user.delete_at > 0) {
@@ -96,6 +102,18 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
const onPromoteToMember = () => updateUser({roles: user.roles.replace(General.SYSTEM_GUEST_ROLE, '')});
const onDemoteToGuest = () => updateUser({roles: `${user.roles} ${General.SYSTEM_GUEST_ROLE}`});
+ const openUserSettingsModal = useCallback(() => {
+ dispatch(openModal({
+ modalId: ModalIdentifiers.USER_SETTINGS,
+ dialogType: UserSettingsModal,
+ dialogProps: {
+ adminMode: true,
+ isContentProductSettings: true,
+ userID: user.id,
+ },
+ }));
+ }, [dispatch, user.id]);
+
return (
+ {
+ showManageUserSettings &&
+
+ }
+ onClick={() => {
+ dispatch(openModal({
+ modalId: ModalIdentifiers.CONFIRM_MANAGE_USER_SETTINGS_MODAL,
+ dialogType: ConfirmManageUserSettingsModal,
+ dialogProps: {
+ user,
+ onConfirm: openUserSettingsModal,
+ },
+ }));
+ }}
+ />
+ }
+
{config.ServiceSettings?.EnableUserAccessTokens &&
);
@@ -78,6 +83,8 @@ export default function UserSettings(props: Props) {
collapseModal={props.collapseModal}
setEnforceFocus={props.setEnforceFocus}
setRequireConfirm={props.setRequireConfirm}
+ adminMode={props.adminMode}
+ userPreferences={props.userPreferences}
/>
);
@@ -89,6 +96,9 @@ export default function UserSettings(props: Props) {
updateSection={props.updateSection}
closeModal={props.closeModal}
collapseModal={props.collapseModal}
+ adminMode={props.adminMode}
+ currentUserId={props.user.id}
+ userPreferences={props.userPreferences}
/>
);
@@ -100,6 +110,9 @@ export default function UserSettings(props: Props) {
updateSection={props.updateSection}
closeModal={props.closeModal}
collapseModal={props.collapseModal}
+ adminMode={props.adminMode}
+ currentUser={props.user}
+ userPreferences={props.userPreferences}
/>
);
diff --git a/webapp/channels/src/components/user_settings/modal/index.ts b/webapp/channels/src/components/user_settings/modal/index.ts
index 46799ce0d0..774aa871e8 100644
--- a/webapp/channels/src/components/user_settings/modal/index.ts
+++ b/webapp/channels/src/components/user_settings/modal/index.ts
@@ -6,9 +6,11 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
-import {sendVerificationEmail} from 'mattermost-redux/actions/users';
+import {getUserPreferences} from 'mattermost-redux/actions/preferences';
+import {getUser, sendVerificationEmail} from 'mattermost-redux/actions/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
-import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
+import {getUserPreferences as getUserPreferencesSelector} from 'mattermost-redux/selectors/entities/preferences';
+import {getCurrentUser, getUser as getUserSelector} from 'mattermost-redux/selectors/entities/users';
import {getPluginUserSettings} from 'selectors/plugins';
@@ -18,14 +20,19 @@ import type {GlobalState} from 'types/store';
const UserSettingsModalAsync = makeAsyncComponent('UserSettingsModal', lazy(() => import('./user_settings_modal')));
-function mapStateToProps(state: GlobalState) {
+import type {OwnProps} from './user_settings_modal';
+
+function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
const config = getConfig(state);
const sendEmailNotifications = config.SendEmailNotifications === 'true';
const requireEmailVerification = config.RequireEmailVerification === 'true';
+ const currentUser = ownProps.adminMode && ownProps.userID ? getUserSelector(state, ownProps.userID) : getCurrentUser(state);
+
return {
- currentUser: getCurrentUser(state),
+ currentUser,
+ userPreferences: ownProps.adminMode && ownProps.userID ? getUserPreferencesSelector(state, ownProps.userID) : undefined,
sendEmailNotifications,
requireEmailVerification,
pluginSettings: getPluginUserSettings(state),
@@ -36,6 +43,8 @@ function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators({
sendVerificationEmail,
+ getUserPreferences,
+ getUser,
}, dispatch),
};
}
diff --git a/webapp/channels/src/components/user_settings/modal/user_settings_modal.tsx b/webapp/channels/src/components/user_settings/modal/user_settings_modal.tsx
index 472035fff8..278961fcc3 100644
--- a/webapp/channels/src/components/user_settings/modal/user_settings_modal.tsx
+++ b/webapp/channels/src/components/user_settings/modal/user_settings_modal.tsx
@@ -4,9 +4,10 @@
import React from 'react';
import {Modal} from 'react-bootstrap';
import ReactDOM from 'react-dom';
-import {injectIntl} from 'react-intl';
+import {FormattedMessage, injectIntl} from 'react-intl';
import type {IntlShape} from 'react-intl';
+import type {PreferencesType} from '@mattermost/types/preferences';
import type {UserProfile} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions';
@@ -14,20 +15,31 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
import ConfirmModal from 'components/confirm_modal';
import SettingsSidebar from 'components/settings_sidebar';
import UserSettings from 'components/user_settings';
+import LoadingSpinner from 'components/widgets/loading/loading_spinner';
+import SmartLoader from 'components/widgets/smartLoader';
import Constants from 'utils/constants';
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
import {stopTryNotificationRing} from 'utils/notification_sounds';
+import {getDisplayName} from 'utils/utils';
import type {PluginConfiguration} from 'types/plugins/user_settings';
-export type Props = {
- currentUser: UserProfile;
+export type OwnProps = {
+ userID?: string;
+ adminMode?: boolean;
+ currentUser?: UserProfile;
+ isContentProductSettings: boolean;
+ userPreferences?: PreferencesType;
+}
+
+export type Props = OwnProps & {
onExited: () => void;
intl: IntlShape;
- isContentProductSettings: boolean;
actions: {
sendVerificationEmail: (email: string) => Promise;
+ getUserPreferences: (userID: string) => Promise;
+ getUser: (userID: string) => Promise;
};
pluginSettings: {[pluginId: string]: PluginConfiguration};
}
@@ -39,6 +51,7 @@ type State = {
enforceFocus?: boolean;
show: boolean;
resendStatus: string;
+ loading: boolean;
}
class UserSettingsModal extends React.PureComponent {
@@ -57,6 +70,7 @@ class UserSettingsModal extends React.PureComponent {
enforceFocus: true,
show: true,
resendStatus: '',
+ loading: false,
};
this.requireConfirm = false;
@@ -84,6 +98,22 @@ class UserSettingsModal extends React.PureComponent {
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown);
+
+ if (this.props.adminMode && this.props.userID) {
+ this.setState({loading: true});
+
+ if (!this.props.userPreferences) {
+ this.props.actions.getUserPreferences(this.props.userID);
+ }
+
+ if (!this.props.currentUser) {
+ this.props.actions.getUser(this.props.userID);
+ }
+ }
+
+ if (!this.props.adminMode) {
+ this.setState({loading: false});
+ }
}
componentWillUnmount() {
@@ -97,6 +127,10 @@ class UserSettingsModal extends React.PureComponent {
}
}
+ setLoadingFinished = () => {
+ this.setState({loading: false});
+ };
+
handleKeyDown = (e: KeyboardEvent) => {
if (cmdOrCtrlPressed(e) && e.shiftKey && isKeyPressed(e, Constants.KeyCodes.A)) {
e.preventDefault();
@@ -275,17 +309,25 @@ class UserSettingsModal extends React.PureComponent {
render() {
const {formatMessage} = this.props.intl;
- if (this.props.currentUser == null) {
- return (
);
- }
- const modalTitle = this.props.isContentProductSettings ? formatMessage({
- id: 'global_header.productSettings',
- defaultMessage: 'Settings',
- }) : formatMessage({
- id: 'user.settings.modal.title',
- defaultMessage: 'Profile',
- });
+ let modalTitle: string;
+
+ if (this.props.adminMode && this.props.currentUser) {
+ modalTitle = formatMessage({
+ id: 'userSettings.adminMode.modal_header',
+ defaultMessage: "{userDisplayName}'s Settings",
+ }, {
+ userDisplayName: getDisplayName(this.props.currentUser),
+ });
+ } else {
+ modalTitle = this.props.isContentProductSettings ? formatMessage({
+ id: 'global_header.productSettings',
+ defaultMessage: 'Settings',
+ }) : formatMessage({
+ id: 'user.settings.modal.title',
+ defaultMessage: 'Profile',
+ });
+ }
return (
{
>
{modalTitle}
+
+ {
+ this.props.adminMode &&
+
+
+
+ }
-
-
-
-
-
-
this.setState({enforceFocus})}
- setRequireConfirm={
- (requireConfirm?: boolean, customConfirmAction?: () => () => void) => {
- this.requireConfirm = requireConfirm!;
- this.customConfirmAction = customConfirmAction!;
+ {
+ this.props.adminMode &&
+
+
+
+ }
+
+ {
+ !this.state.loading && this.props.currentUser &&
+
+
+
+
+
+ this.setState({enforceFocus})}
+ setRequireConfirm={
+ (requireConfirm?: boolean, customConfirmAction?: () => () => void) => {
+ this.requireConfirm = requireConfirm!;
+ this.customConfirmAction = customConfirmAction!;
+ }
}
- }
- pluginSettings={this.props.pluginSettings}
- user={this.props.currentUser}
- />
+ pluginSettings={this.props.pluginSettings}
+ user={this.props.currentUser}
+ adminMode={this.props.adminMode}
+ userPreferences={this.props.userPreferences}
+ />
+
-
+ }
{
id: 'user.settings.modal.confirmMsg',
defaultMessage: 'You have unsaved changes, are you sure you want to discard them?',
})}
- confirmButtonText={formatMessage({id: 'user.settings.modal.confirmBtns', defaultMessage: 'Yes, Discard'})}
+ confirmButtonText={formatMessage({
+ id: 'user.settings.modal.confirmBtns',
+ defaultMessage: 'Yes, Discard',
+ })}
show={this.state.showConfirmModal}
onConfirm={this.handleConfirm}
onCancel={this.handleCancelConfirmation}
diff --git a/webapp/channels/src/components/user_settings/notifications/index.ts b/webapp/channels/src/components/user_settings/notifications/index.ts
index b60330a92d..480f77673e 100644
--- a/webapp/channels/src/components/user_settings/notifications/index.ts
+++ b/webapp/channels/src/components/user_settings/notifications/index.ts
@@ -3,10 +3,13 @@
import {connect, type ConnectedProps} from 'react-redux';
-import {updateMe} from 'mattermost-redux/actions/users';
+import {patchUser, updateMe} from 'mattermost-redux/actions/users';
import {getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
-import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
+import {
+ isCollapsedThreadsEnabled,
+ isCollapsedThreadsEnabledForUser,
+} from 'mattermost-redux/selectors/entities/preferences';
import {isCallsEnabled, isCallsRingingEnabledOnServer} from 'selectors/calls';
@@ -14,9 +17,11 @@ import {isEnterpriseOrCloudOrSKUStarterFree} from 'utils/license_utils';
import type {GlobalState} from 'types/store';
+import type {OwnProps} from './user_settings_notifications';
import UserSettingsNotifications from './user_settings_notifications';
-const mapStateToProps = (state: GlobalState) => {
+const mapStateToProps = (state: GlobalState, props: OwnProps) => {
+ // server config, related to server configuration, not the user
const config = getConfig(state);
const sendPushNotifications = config.SendPushNotifications === 'true';
@@ -30,16 +35,16 @@ const mapStateToProps = (state: GlobalState) => {
return {
sendPushNotifications,
enableAutoResponder,
- isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state),
+ isCollapsedThreadsEnabled: props.adminMode && props.userPreferences ? isCollapsedThreadsEnabledForUser(state, props.userPreferences) : isCollapsedThreadsEnabled(state),
isCallsRingingEnabled: isCallsEnabled(state, '0.17.0') && isCallsRingingEnabledOnServer(state),
isEnterpriseOrCloudOrSKUStarterFree: isEnterpriseOrCloudOrSKUStarterFree(license, subscriptionProduct, isEnterpriseReady),
isEnterpriseReady,
-
};
};
const mapDispatchToProps = {
updateMe,
+ patchUser,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
diff --git a/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.test.tsx b/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.test.tsx
index 6c8d74ef67..d779f530b5 100644
--- a/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.test.tsx
+++ b/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.test.tsx
@@ -18,6 +18,7 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
closeModal: jest.fn(),
collapseModal: jest.fn(),
updateMe: jest.fn(() => Promise.resolve({})),
+ patchUser: jest.fn(() => Promise.resolve({})),
isCollapsedThreadsEnabled: true,
sendPushNotifications: false,
enableAutoResponder: false,
diff --git a/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx b/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx
index d6305c1bb7..3488639681 100644
--- a/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx
+++ b/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx
@@ -11,6 +11,7 @@ import type {Styles as ReactSelectStyles, ValueType} from 'react-select';
import CreatableReactSelect from 'react-select/creatable';
import {LightbulbOutlineIcon} from '@mattermost/compass-icons/components';
+import type {PreferencesType} from '@mattermost/types/preferences';
import type {UserNotifyProps, UserProfile} from '@mattermost/types/users';
import ExternalLink from 'components/external_link';
@@ -40,12 +41,14 @@ type MultiInputValue = {
value: string;
}
-type OwnProps = {
+export type OwnProps = {
user: UserProfile;
updateSection: (section: string) => void;
activeSection: string;
closeModal: () => void;
collapseModal: () => void;
+ adminMode?: boolean;
+ userPreferences?: PreferencesType;
}
export type Props = PropsFromRedux & OwnProps & WrappedComponentProps;
@@ -284,7 +287,20 @@ class NotificationsTab extends React.PureComponent {
this.setState({isSaving: true});
stopTryNotificationRing();
- const {data: updatedUser, error} = await this.props.updateMe({notify_props: data});
+ let updatedUser: UserProfile | undefined;
+ let error;
+
+ if (this.props.adminMode) {
+ const payloadUser = {...this.props.user, notify_props: data};
+ const response = await this.props.patchUser(payloadUser);
+ updatedUser = response.data;
+ error = response.error;
+ } else {
+ const response = await this.props.updateMe({notify_props: data});
+ updatedUser = response.data;
+ error = response.error;
+ }
+
if (updatedUser) {
this.handleUpdateSection('');
this.setState(getDefaultStateFromProps(this.props));
diff --git a/webapp/channels/src/components/user_settings/sidebar/limit_visible_gms_dms/index.ts b/webapp/channels/src/components/user_settings/sidebar/limit_visible_gms_dms/index.ts
index d6c4114ef0..7f914f3e66 100644
--- a/webapp/channels/src/components/user_settings/sidebar/limit_visible_gms_dms/index.ts
+++ b/webapp/channels/src/components/user_settings/sidebar/limit_visible_gms_dms/index.ts
@@ -4,17 +4,18 @@
import {connect} from 'react-redux';
import {savePreferences} from 'mattermost-redux/actions/preferences';
-import {getVisibleDmGmLimit} from 'mattermost-redux/selectors/entities/preferences';
+import {getUserVisibleDmGmLimit, getVisibleDmGmLimit} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import type {GlobalState} from 'types/store';
+import type {OwnProps} from './limit_visible_gms_dms';
import LimitVisibleGMsDMs from './limit_visible_gms_dms';
-function mapStateToProps(state: GlobalState) {
+function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
return {
- currentUserId: getCurrentUserId(state),
- dmGmLimit: getVisibleDmGmLimit(state),
+ currentUserId: ownProps.adminMode ? ownProps.currentUserId : getCurrentUserId(state),
+ dmGmLimit: ownProps.adminMode && ownProps.userPreferences ? getUserVisibleDmGmLimit(ownProps.userPreferences) : getVisibleDmGmLimit(state),
};
}
diff --git a/webapp/channels/src/components/user_settings/sidebar/limit_visible_gms_dms/limit_visible_gms_dms.tsx b/webapp/channels/src/components/user_settings/sidebar/limit_visible_gms_dms/limit_visible_gms_dms.tsx
index d520aa454a..331b386a53 100644
--- a/webapp/channels/src/components/user_settings/sidebar/limit_visible_gms_dms/limit_visible_gms_dms.tsx
+++ b/webapp/channels/src/components/user_settings/sidebar/limit_visible_gms_dms/limit_visible_gms_dms.tsx
@@ -7,7 +7,7 @@ import {FormattedMessage} from 'react-intl';
import ReactSelect from 'react-select';
import type {ValueType} from 'react-select';
-import type {PreferenceType} from '@mattermost/types/preferences';
+import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
import {Preferences} from 'mattermost-redux/constants';
import type {ActionResult} from 'mattermost-redux/types/actions';
@@ -23,10 +23,15 @@ type Limit = {
label: string;
};
-type Props = {
+export type OwnProps = {
+ adminMode?: boolean;
+ currentUserId?: string;
+ userPreferences?: PreferencesType;
+}
+
+type Props = OwnProps & {
active: boolean;
areAllSectionsInactive: boolean;
- currentUserId: string;
savePreferences: (userId: string, preferences: PreferenceType[]) => Promise;
dmGmLimit: number;
updateSection: (section: string) => void;
@@ -99,6 +104,10 @@ export default class LimitVisibleGMsDMs extends React.PureComponent {
+ if (!this.props.currentUserId) {
+ return;
+ }
+
this.setState({isSaving: true});
await this.props.savePreferences(this.props.currentUserId, [{
diff --git a/webapp/channels/src/components/user_settings/sidebar/show_unreads_category/index.ts b/webapp/channels/src/components/user_settings/sidebar/show_unreads_category/index.ts
index 07deabfe94..843496de79 100644
--- a/webapp/channels/src/components/user_settings/sidebar/show_unreads_category/index.ts
+++ b/webapp/channels/src/components/user_settings/sidebar/show_unreads_category/index.ts
@@ -4,17 +4,23 @@
import {connect} from 'react-redux';
import {savePreferences} from 'mattermost-redux/actions/preferences';
-import {shouldShowUnreadsCategory} from 'mattermost-redux/selectors/entities/preferences';
+import {getConfig} from 'mattermost-redux/selectors/entities/general';
+import {
+ calculateUserShouldShowUnreadsCategory,
+ shouldShowUnreadsCategory,
+} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import type {GlobalState} from 'types/store';
+import type {OwnProps} from './show_unreads_category';
import ShowUnreadsCategory from './show_unreads_category';
-function mapStateToProps(state: GlobalState) {
+function mapStateToProps(state: GlobalState, props: OwnProps) {
+ const serverDefault = getConfig(state).ExperimentalGroupUnreadChannels;
return {
- currentUserId: getCurrentUserId(state),
- showUnreadsCategory: shouldShowUnreadsCategory(state),
+ currentUserId: props.adminMode ? props.currentUserId : getCurrentUserId(state),
+ showUnreadsCategory: props.adminMode && props.userPreferences ? calculateUserShouldShowUnreadsCategory(props.userPreferences, serverDefault) : shouldShowUnreadsCategory(state),
};
}
diff --git a/webapp/channels/src/components/user_settings/sidebar/show_unreads_category/show_unreads_category.tsx b/webapp/channels/src/components/user_settings/sidebar/show_unreads_category/show_unreads_category.tsx
index 2c1c401eb3..0f63bf0a4b 100644
--- a/webapp/channels/src/components/user_settings/sidebar/show_unreads_category/show_unreads_category.tsx
+++ b/webapp/channels/src/components/user_settings/sidebar/show_unreads_category/show_unreads_category.tsx
@@ -5,7 +5,7 @@ import React from 'react';
import type {RefObject} from 'react';
import {FormattedMessage} from 'react-intl';
-import type {PreferenceType} from '@mattermost/types/preferences';
+import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
import {Preferences} from 'mattermost-redux/constants';
import type {ActionResult} from 'mattermost-redux/types/actions';
@@ -16,10 +16,15 @@ import type SettingItemMinComponent from 'components/setting_item_min';
import {a11yFocus} from 'utils/utils';
-type Props = {
+export type OwnProps = {
+ adminMode?: boolean;
+ currentUserId?: string;
+ userPreferences?: PreferencesType;
+}
+
+type Props = OwnProps & {
active: boolean;
areAllSectionsInactive: boolean;
- currentUserId: string;
savePreferences: (userId: string, preferences: PreferenceType[]) => Promise;
showUnreadsCategory: boolean;
updateSection: (section: string) => void;
@@ -75,6 +80,11 @@ export default class ShowUnreadsCategory extends React.PureComponent {
+ if (!this.props.currentUserId) {
+ // Only for type safety, won't actually happen
+ return;
+ }
+
this.setState({isSaving: true});
await this.props.savePreferences(this.props.currentUserId, [{
diff --git a/webapp/channels/src/components/user_settings/sidebar/user_settings_sidebar.tsx b/webapp/channels/src/components/user_settings/sidebar/user_settings_sidebar.tsx
index 2dbfc4bd55..bbb5c1f223 100644
--- a/webapp/channels/src/components/user_settings/sidebar/user_settings_sidebar.tsx
+++ b/webapp/channels/src/components/user_settings/sidebar/user_settings_sidebar.tsx
@@ -4,6 +4,8 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
+import type {PreferencesType} from '@mattermost/types/preferences';
+
import LimitVisibleGMsDMs from './limit_visible_gms_dms';
import ShowUnreadsCategory from './show_unreads_category';
@@ -15,6 +17,9 @@ export interface Props {
activeSection: string;
closeModal: () => void;
collapseModal: () => void;
+ adminMode?: boolean;
+ currentUserId?: string;
+ userPreferences?: PreferencesType;
}
export default function UserSettingsSidebar(props: Props): JSX.Element {
@@ -48,12 +53,18 @@ export default function UserSettingsSidebar(props: Props): JSX.Element {
active={props.activeSection === 'showUnreadsCategory'}
updateSection={props.updateSection}
areAllSectionsInactive={props.activeSection === ''}
+ adminMode={props.adminMode}
+ currentUserId={props.currentUserId}
+ userPreferences={props.userPreferences}
/>
diff --git a/webapp/channels/src/components/widgets/smartLoader/index.tsx b/webapp/channels/src/components/widgets/smartLoader/index.tsx
new file mode 100644
index 0000000000..8d042facda
--- /dev/null
+++ b/webapp/channels/src/components/widgets/smartLoader/index.tsx
@@ -0,0 +1,37 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {type ReactNode, useEffect, useState} from 'react';
+
+const DEFAULT_MIN_LOADER_DURATION = 1500;
+
+type Props = {
+ loading: boolean;
+ children: ReactNode;
+ className?: string;
+ onLoaded: () => void;
+}
+
+const SmartLoader = ({loading, children, className, onLoaded}: Props) => {
+ const [timeoutFinished, setTimeoutFinished] = useState(false);
+
+ useEffect(() => {
+ setTimeout(() => {
+ setTimeoutFinished(true);
+ }, DEFAULT_MIN_LOADER_DURATION);
+ }, []);
+
+ useEffect(() => {
+ if (!loading && timeoutFinished) {
+ onLoaded();
+ }
+ }, [loading, timeoutFinished, onLoaded]);
+
+ return loading || !timeoutFinished ? (
+
+ {children}
+
+ ) : null;
+};
+
+export default SmartLoader;
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index d0c58952be..5574ac462b 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -2702,6 +2702,9 @@
"admin.user_item.makeActive": "Activate",
"admin.user_item.makeMember": "Make Team Member",
"admin.user_item.makeTeamAdmin": "Make Team Admin",
+ "admin.user_item.manageSettings": "Manage User Settings",
+ "admin.user_item.manageSettings.confirm_dialog.body": "You are about to access {userDisplayName}'s account settings. Any modifications you make will take effect immediately in their account. {userDisplayName} retains the ability to view and modify these settings at any time. Are you sure you want to proceed with managing {userDisplayName}'s settings?",
+ "admin.user_item.manageSettings.disabled_tooltip": "Please upgrade to Enterprise to manage user settings",
"admin.user_item.manageTeams": "Manage Teams",
"admin.user_item.member": "Member",
"admin.user_item.menuAriaLabel": "User Actions Menu",
@@ -3768,6 +3771,7 @@
"generic_modal.confirm": "Confirm",
"generic.close": "Close",
"generic.done": "Done",
+ "generic.enterprise_feature": "Enterprise Feature",
"generic.next": "Next",
"generic.okay": "Okay",
"generic.previous": "Previous",
@@ -5713,6 +5717,8 @@
"userGuideHelp.trainingResources": "Training resources",
"users_limits_announcement_bar.copyText": "User limits exceeded. Contact administrator with: {ErrorCode}",
"users_limits_announcement_bar.ctaText": "Learn More",
+ "userSettings.adminMode.admin_mode_badge": "Admin Mode",
+ "userSettings.adminMode.modal_header": "Manage {userDisplayName}'s Settings",
"userSettingsModal.pluginPreferences.header": "PLUGIN PREFERENCES",
"version_bar.new": "A new version of Mattermost is available.",
"version_bar.refresh": "Refresh the app now",
diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/preferences.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/preferences.ts
index a3ad214ab0..846e4dbf27 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/action_types/preferences.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/preferences.ts
@@ -7,4 +7,6 @@ export default keyMirror({
RECEIVED_PREFERENCES: null,
RECEIVED_ALL_PREFERENCES: null,
DELETED_PREFERENCES: null,
+ RECEIVED_USER_PREFERENCES: null,
+ RECEIVED_USER_ALL_PREFERENCES: null,
});
diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/preferences.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/preferences.ts
index da03304219..7c3207f968 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/actions/preferences.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/actions/preferences.ts
@@ -48,6 +48,14 @@ export function getMyPreferences() {
});
}
+// used for fetching some other user's preferences other than current user
+export function getUserPreferences(userID: string) {
+ return bindClientFunc({
+ clientFunc: () => Client4.getUserPreferences(userID),
+ onSuccess: PreferenceTypes.RECEIVED_USER_ALL_PREFERENCES,
+ });
+}
+
export function setActionsMenuInitialisationState(initializationState: Record): ThunkActionFunc {
return async (dispatch, getState) => {
const state = getState();
@@ -77,11 +85,15 @@ export function setCustomStatusInitialisationState(initializationState: Record {
+ return async (dispatch, getState) => {
(async function savePreferencesWrapper() {
+ const state = getState();
+ const currentUserId = getCurrentUserId(state);
+ const actionType = userId === currentUserId ? PreferenceTypes.RECEIVED_PREFERENCES : PreferenceTypes.RECEIVED_USER_PREFERENCES;
+
try {
dispatch({
- type: PreferenceTypes.RECEIVED_PREFERENCES,
+ type: actionType,
data: preferences,
});
diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/preferences.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/preferences.ts
index a2cafb2350..a3ce67cf59 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/preferences.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/preferences.ts
@@ -4,7 +4,7 @@
import type {AnyAction} from 'redux';
import {combineReducers} from 'redux';
-import type {PreferenceType} from '@mattermost/types/preferences';
+import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
import {PreferenceTypes, UserTypes} from 'mattermost-redux/action_types';
@@ -24,6 +24,24 @@ function setAllPreferences(preferences: PreferenceType[]): any {
return nextState;
}
+function setAllUserPreferences(preferences: PreferenceType[]): {[key: string]: PreferencesType} {
+ const nextState: {[key: string]: PreferencesType} = {};
+ if (preferences.length === 0) {
+ return nextState;
+ }
+
+ const userID = preferences[0].user_id;
+ nextState[userID] = {};
+
+ if (preferences) {
+ for (const preference of preferences) {
+ nextState[userID][getKey(preference)] = preference;
+ }
+ }
+
+ return nextState;
+}
+
function myPreferences(state: Record = {}, action: AnyAction) {
switch (action.type) {
case PreferenceTypes.RECEIVED_ALL_PREFERENCES:
@@ -62,8 +80,37 @@ function myPreferences(state: Record = {}, action: AnyAc
}
}
+function userPreferences(state: Record = {}, action: AnyAction) {
+ switch (action.type) {
+ case PreferenceTypes.RECEIVED_USER_ALL_PREFERENCES:
+ return setAllUserPreferences(action.data);
+
+ case PreferenceTypes.RECEIVED_USER_PREFERENCES: {
+ const nextState = {...state};
+
+ const data = action.data as PreferenceType[];
+ if (action.data && data.length > 0) {
+ const userID = data[0].user_id;
+ nextState[userID] = nextState[userID] ? {...nextState[userID]} : {};
+
+ for (const preference of action.data) {
+ nextState[preference.user_id][getKey(preference)] = preference;
+ }
+ }
+
+ return nextState;
+ }
+
+ case UserTypes.LOGOUT_SUCCESS:
+ return {};
+ default:
+ return state;
+ }
+}
+
export default combineReducers({
// object where the key is the category-name and has the corresponding value
myPreferences,
+ userPreferences,
});
diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts
index b4212e71f9..6067467a39 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {CollapsedThreads} from '@mattermost/types/config';
-import type {PreferenceType} from '@mattermost/types/preferences';
+import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
import type {GlobalState} from '@mattermost/types/store';
import {General, Preferences} from 'mattermost-redux/constants';
@@ -16,6 +16,10 @@ export function getMyPreferences(state: GlobalState): { [x: string]: PreferenceT
return state.entities.preferences.myPreferences;
}
+export function getUserPreferences(state: GlobalState, userID: string): { [x: string]: PreferenceType } {
+ return state.entities.preferences.userPreferences[userID];
+}
+
export function get(state: GlobalState, category: string, name: string, defaultValue: any = '') {
const key = getPreferenceKey(category, name);
const prefs = getMyPreferences(state);
@@ -27,11 +31,26 @@ export function get(state: GlobalState, category: string, name: string, defaultV
return prefs[key].value;
}
+export function getFromPreferences(preferences: PreferencesType, category: string, name: string, defaultValue: any = '') {
+ const key = getPreferenceKey(category, name);
+
+ if (!(key in preferences)) {
+ return defaultValue;
+ }
+
+ return preferences[key].value;
+}
+
export function getBool(state: GlobalState, category: string, name: string, defaultValue = false): boolean {
const value = get(state, category, name, String(defaultValue));
return value !== 'false';
}
+export function getBoolFromPreferences(userPreferences: PreferencesType, category: string, name: string, defaultValue = false): boolean {
+ const value = getFromPreferences(userPreferences, category, name, String(defaultValue));
+ return value !== 'false';
+}
+
export function getInt(state: GlobalState, category: string, name: string, defaultValue = 0): number {
const value = get(state, category, name, defaultValue);
return parseInt(value, 10);
@@ -57,6 +76,26 @@ 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) => {
+ const prefix = category + '--';
+ const prefsInCategory: PreferenceType[] = [];
+
+ for (const key in preferences) {
+ if (key.startsWith(prefix)) {
+ prefsInCategory.push(preferences[key]);
+ }
+ }
+
+ return prefsInCategory;
+ },
+ );
+}
+
const getDirectShowCategory = makeGetCategory();
export function getDirectShowPreferences(state: GlobalState) {
@@ -180,31 +219,44 @@ export function makeGetStyleFromTheme