Migrate profile popover from class to functional component (#25608)
* Migrate profile popover from class to functional component * Fix lint * Fix tests and minor fixes * Address feedback --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
23fcb7728f
Коммит
77bdeb1c2b
@@ -149,7 +149,7 @@ exports[`components/AtMention should match snapshot when mentioning current user
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Connect(injectIntl(ProfilePopover))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc1/image"
|
||||
@@ -189,7 +189,7 @@ exports[`components/AtMention should match snapshot when mentioning user 1`] = `
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Connect(injectIntl(ProfilePopover))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
@@ -229,7 +229,7 @@ exports[`components/AtMention should match snapshot when mentioning user contain
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Connect(injectIntl(ProfilePopover))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc3/image"
|
||||
@@ -270,7 +270,7 @@ exports[`components/AtMention should match snapshot when mentioning user contain
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Connect(injectIntl(ProfilePopover))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc3/image"
|
||||
@@ -310,7 +310,7 @@ exports[`components/AtMention should match snapshot when mentioning user followe
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Connect(injectIntl(ProfilePopover))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
@@ -351,7 +351,7 @@ exports[`components/AtMention should match snapshot when mentioning user with di
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Connect(injectIntl(ProfilePopover))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
@@ -391,7 +391,7 @@ exports[`components/AtMention should match snapshot when mentioning user with mi
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Connect(injectIntl(ProfilePopover))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
|
||||
@@ -31,7 +31,6 @@ type Props = {
|
||||
groupsByName: Record<string, Group>;
|
||||
children?: React.ReactNode;
|
||||
channelId?: string;
|
||||
hasMention?: boolean;
|
||||
disableHighlight?: boolean;
|
||||
disableGroupHighlight?: boolean;
|
||||
}
|
||||
@@ -122,7 +121,6 @@ export const AtMention = (props: Props) => {
|
||||
className='user-profile-popover'
|
||||
userId={user.id}
|
||||
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
|
||||
hasMention={props.hasMention}
|
||||
hide={hideOverlay}
|
||||
channelId={props.channelId}
|
||||
/>
|
||||
@@ -187,7 +185,6 @@ export const AtMention = (props: Props) => {
|
||||
userId={groupUser.id}
|
||||
src={Client4.getProfilePictureUrl(groupUser.id, groupUser.last_picture_update)}
|
||||
channelId={props.channelId}
|
||||
hasMention={props.hasMention}
|
||||
hide={hideGroupUserOverlay}
|
||||
returnFocus={returnFocus}
|
||||
/>
|
||||
|
||||
@@ -48,8 +48,7 @@ function CustomStatusEmoji({
|
||||
|
||||
const customStatusExpired = useSelector((state: GlobalState) => isCustomStatusExpired(state, customStatus));
|
||||
const customStatusEnabled = useSelector(isCustomStatusEnabled);
|
||||
const isCustomStatusSet = Boolean(customStatusEnabled && customStatus?.emoji && !customStatusExpired);
|
||||
if (!isCustomStatusSet) {
|
||||
if (!customStatusEnabled || !customStatus?.emoji || customStatusExpired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,10 +44,9 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
@@ -67,7 +66,6 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
|
||||
<OverlayWrapper
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
@@ -317,10 +315,9 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
@@ -340,7 +337,6 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
|
||||
<OverlayWrapper
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
@@ -596,10 +592,9 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
@@ -619,7 +614,6 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
|
||||
<OverlayWrapper
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
@@ -937,10 +931,9 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
@@ -960,7 +953,6 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
|
||||
<OverlayWrapper
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
|
||||
@@ -28,6 +28,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
const intl = useIntl();
|
||||
const {post, compactDisplay, isMobileView, isConsecutivePost, enablePostUsernameOverride, isBot, isSystemMessage, colorizeUsernames} = props;
|
||||
const isFromAutoResponder = fromAutoResponder(post);
|
||||
const colorize = compactDisplay && colorizeUsernames;
|
||||
@@ -55,7 +56,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
userId={post.user_id}
|
||||
channelId={post.channel_id}
|
||||
isRHS={isRHS}
|
||||
hasMention={true}
|
||||
colorize={colorize}
|
||||
/>
|
||||
);
|
||||
@@ -67,7 +67,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
userId={post.user_id}
|
||||
channelId={post.channel_id}
|
||||
isRHS={isRHS}
|
||||
hasMention={true}
|
||||
colorize={colorize}
|
||||
/>
|
||||
);
|
||||
@@ -77,7 +76,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
userId={post.user_id}
|
||||
channelId={post.channel_id}
|
||||
isRHS={isRHS}
|
||||
hasMention={true}
|
||||
colorize={colorize}
|
||||
/>
|
||||
);
|
||||
@@ -108,7 +106,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
channelId={post.channel_id}
|
||||
hideStatus={true}
|
||||
isRHS={isRHS}
|
||||
hasMention={true}
|
||||
colorize={colorize}
|
||||
/>
|
||||
</span>
|
||||
@@ -135,12 +132,10 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
} else if (isSystemMessage) {
|
||||
userProfile = (
|
||||
<UserProfile
|
||||
overwriteName={
|
||||
<FormattedMessage
|
||||
id='post_info.system'
|
||||
defaultMessage='System'
|
||||
/>
|
||||
}
|
||||
overwriteName={intl.formatMessage({
|
||||
id: 'post_info.system',
|
||||
defaultMessage: 'System',
|
||||
})}
|
||||
userId={post.user_id}
|
||||
disablePopover={true}
|
||||
channelId={post.channel_id}
|
||||
|
||||
@@ -230,7 +230,6 @@ exports[`components/post_edit_history/edited_post_item should match snapshot whe
|
||||
>
|
||||
<Connect(UserProfile)
|
||||
disablePopover={true}
|
||||
hasMention={true}
|
||||
userId="user_id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -145,7 +145,6 @@ const EditedPostItem = ({post, isCurrent = false, postCurrentVersion, theme, act
|
||||
<div className={'edit-post-history__header__username'}>
|
||||
<UserProfileComponent
|
||||
userId={post.user_id}
|
||||
hasMention={true}
|
||||
disablePopover={true}
|
||||
overwriteName={overwriteName}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,6 @@ describe('components/PostProfilePicture', () => {
|
||||
enablePostIconOverride: true,
|
||||
compactDisplay: true,
|
||||
hasImageProxy: true,
|
||||
isBusy: true,
|
||||
post,
|
||||
user,
|
||||
isBot: Boolean(user.is_bot),
|
||||
|
||||
@@ -18,7 +18,6 @@ type Props = {
|
||||
compactDisplay?: boolean;
|
||||
enablePostIconOverride: boolean;
|
||||
hasImageProxy: boolean;
|
||||
isBusy?: boolean;
|
||||
post: Post;
|
||||
status?: string;
|
||||
user: UserProfile;
|
||||
@@ -81,7 +80,6 @@ export default class PostProfilePicture extends React.PureComponent<Props> {
|
||||
const {
|
||||
availabilityStatusOnPosts,
|
||||
compactDisplay,
|
||||
isBusy,
|
||||
post,
|
||||
user,
|
||||
isBot,
|
||||
@@ -107,7 +105,6 @@ export default class PostProfilePicture extends React.PureComponent<Props> {
|
||||
return (
|
||||
<ProfilePicture
|
||||
hasMention={hasMention}
|
||||
isBusy={isBusy}
|
||||
size='md'
|
||||
src={src}
|
||||
profileSrc={profileSrc}
|
||||
|
||||
@@ -225,7 +225,6 @@ function createDMIntroMessage(channel: Channel, centeredIntro: string, teammate?
|
||||
<UserProfile
|
||||
userId={teammate?.id}
|
||||
disablePopover={false}
|
||||
hasMention={true}
|
||||
/>
|
||||
</div>
|
||||
<p className='channel-intro-text'>
|
||||
|
||||
@@ -16,7 +16,6 @@ exports[`components/post_view/CommentedOn should match snapshot 1`] = `
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
hasMention={true}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -49,7 +48,6 @@ exports[`components/post_view/CommentedOn should match snapshot 2`] = `
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
hasMention={true}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -82,7 +80,6 @@ exports[`components/post_view/CommentedOn should match snapshot 3`] = `
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
hasMention={true}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -117,7 +114,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
hasMention={true}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -150,7 +146,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
hasMention={true}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -183,7 +178,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
hasMention={true}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -216,7 +210,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
hasMention={true}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
|
||||
@@ -44,7 +44,6 @@ function CommentedOn({post, parentPostUser, onCommentClick}: Props) {
|
||||
const parentUserProfile = (
|
||||
<UserProfile
|
||||
userId={parentPostUserId}
|
||||
hasMention={true}
|
||||
disablePopover={false}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,6 @@ exports[`PostMessagePreview direct and group messages should render preview for
|
||||
>
|
||||
<Connect(UserProfile)
|
||||
disablePopover={true}
|
||||
hasMention={true}
|
||||
overwriteName=""
|
||||
userId="user_1"
|
||||
/>
|
||||
@@ -121,7 +120,6 @@ exports[`PostMessagePreview direct and group messages should render preview for
|
||||
>
|
||||
<Connect(UserProfile)
|
||||
disablePopover={true}
|
||||
hasMention={true}
|
||||
overwriteName=""
|
||||
userId="user_1"
|
||||
/>
|
||||
@@ -208,7 +206,6 @@ exports[`PostMessagePreview nested previews should render file preview 1`] = `
|
||||
>
|
||||
<Connect(UserProfile)
|
||||
disablePopover={true}
|
||||
hasMention={true}
|
||||
overwriteName=""
|
||||
userId="user_1"
|
||||
/>
|
||||
@@ -315,7 +312,6 @@ exports[`PostMessagePreview nested previews should render opengraph preview 1`]
|
||||
>
|
||||
<Connect(UserProfile)
|
||||
disablePopover={true}
|
||||
hasMention={true}
|
||||
overwriteName=""
|
||||
userId="user_1"
|
||||
/>
|
||||
@@ -430,7 +426,6 @@ exports[`PostMessagePreview should not render bot icon 1`] = `
|
||||
>
|
||||
<Connect(UserProfile)
|
||||
disablePopover={true}
|
||||
hasMention={true}
|
||||
overwriteName=""
|
||||
userId="user_1"
|
||||
/>
|
||||
@@ -522,7 +517,6 @@ exports[`PostMessagePreview should render bot icon 1`] = `
|
||||
>
|
||||
<Connect(UserProfile)
|
||||
disablePopover={true}
|
||||
hasMention={true}
|
||||
overwriteName=""
|
||||
userId="user_1"
|
||||
/>
|
||||
@@ -614,7 +608,6 @@ exports[`PostMessagePreview should render correctly 1`] = `
|
||||
>
|
||||
<Connect(UserProfile)
|
||||
disablePopover={true}
|
||||
hasMention={true}
|
||||
overwriteName=""
|
||||
userId="user_1"
|
||||
/>
|
||||
@@ -703,7 +696,6 @@ exports[`PostMessagePreview show render without preview when preview posts becom
|
||||
>
|
||||
<Connect(UserProfile)
|
||||
disablePopover={true}
|
||||
hasMention={true}
|
||||
overwriteName=""
|
||||
userId="user_1"
|
||||
/>
|
||||
|
||||
@@ -174,7 +174,6 @@ const PostMessagePreview = (props: Props) => {
|
||||
<div className={classNames('col col__name', 'permalink--username')}>
|
||||
<UserProfileComponent
|
||||
userId={user?.id ?? ''}
|
||||
hasMention={true}
|
||||
disablePopover={true}
|
||||
overwriteName={previewPost.props?.override_username || ''}
|
||||
/>
|
||||
|
||||
@@ -44,11 +44,9 @@ exports[`components/ProfilePicture should match snapshot, profile and src, defau
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
isBusy={true}
|
||||
src="http://example.com/image.png"
|
||||
userId="uid"
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,6 @@ describe('components/ProfilePicture', () => {
|
||||
const baseProps: Props = {
|
||||
src: 'http://example.com/image.png',
|
||||
status: 'away',
|
||||
isBusy: true,
|
||||
};
|
||||
|
||||
test('should match snapshot, no user specified, default props', () => {
|
||||
@@ -43,7 +42,6 @@ describe('components/ProfilePicture', () => {
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
size: 'xl',
|
||||
hasMention: true,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ProfilePicture {...props}/>,
|
||||
@@ -69,7 +67,6 @@ describe('components/ProfilePicture', () => {
|
||||
...baseProps,
|
||||
username: 'username',
|
||||
size: 'xs',
|
||||
hasMention: true,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ProfilePicture {...props}/>,
|
||||
|
||||
@@ -20,8 +20,6 @@ interface MMOverlayTrigger extends BaseOverlayTrigger {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
hasMention?: boolean;
|
||||
isBusy?: boolean;
|
||||
isEmoji?: boolean;
|
||||
profileSrc?: string;
|
||||
size?: ComponentProps<typeof Avatar>['size'];
|
||||
@@ -80,10 +78,8 @@ export default class ProfilePicture extends React.PureComponent<Props> {
|
||||
className='user-profile-popover'
|
||||
userId={this.props.userId}
|
||||
src={profileSrc}
|
||||
isBusy={this.props.isBusy}
|
||||
hide={this.hideProfilePopover}
|
||||
channelId={this.props.channelId}
|
||||
hasMention={this.props.hasMention}
|
||||
overwriteIcon={this.props.overwriteIcon}
|
||||
overwriteName={this.props.overwriteName}
|
||||
fromWebhook={this.props.fromWebhook}
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
321
webapp/channels/src/components/profile_popover/index.test.tsx
Обычный файл
321
webapp/channels/src/components/profile_popover/index.test.tsx
Обычный файл
@@ -0,0 +1,321 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen} from '@testing-library/react';
|
||||
import type {ComponentProps} from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
import {CustomStatusDuration} from '@mattermost/types/users';
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General, Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ProfilePopover from '.';
|
||||
|
||||
jest.mock('@mattermost/client', () => ({
|
||||
...jest.requireActual('@mattermost/client'),
|
||||
Client4: class MockClient4 extends jest.requireActual('@mattermost/client').Client4 {
|
||||
getCallsChannelState = jest.fn();
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = ComponentProps<typeof ProfilePopover>;
|
||||
|
||||
function renderWithPluginReducers(
|
||||
c: Parameters<typeof renderWithContext>[0],
|
||||
s: Parameters<typeof renderWithContext>[1],
|
||||
o?: Parameters<typeof renderWithContext>[2],
|
||||
): ReturnType<typeof renderWithContext> {
|
||||
const options = o || {};
|
||||
options.pluginReducers = ['plugins-com.mattermost.calls'];
|
||||
return renderWithContext(c, s, options);
|
||||
}
|
||||
function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
|
||||
const user = TestHelper.getUserMock({
|
||||
id: 'user1',
|
||||
first_name: 'user',
|
||||
props: {
|
||||
customStatus: JSON.stringify({
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.DONT_CLEAR,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const currentUser = TestHelper.getUserMock({id: 'currentUser', roles: 'role'});
|
||||
const currentTeam = TestHelper.getTeamMock({id: 'currentTeam'});
|
||||
const channel = TestHelper.getChannelMock({id: 'channelId', team_id: currentTeam.id, type: General.OPEN_CHANNEL});
|
||||
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
users: {
|
||||
profiles: {
|
||||
[user.id]: user,
|
||||
[currentUser.id]: currentUser,
|
||||
},
|
||||
statuses: {
|
||||
user1: 'offline',
|
||||
},
|
||||
currentUserId: currentUser.id,
|
||||
lastActivity: {
|
||||
user1: 1,
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
teams: {
|
||||
[currentTeam.id]: currentTeam,
|
||||
},
|
||||
currentTeamId: currentTeam.id,
|
||||
membersInTeam: {
|
||||
[currentTeam.id]: {
|
||||
[user.id]: {
|
||||
delete_at: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
channels: {
|
||||
[channel.id]: channel,
|
||||
},
|
||||
myMembers: {
|
||||
[channel.id]: {},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
EnableCustomUserStatuses: 'true',
|
||||
EnableLastActiveTime: 'true',
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
groups: {
|
||||
groups: {},
|
||||
myGroups: [],
|
||||
},
|
||||
emojis: {
|
||||
customEmoji: {},
|
||||
},
|
||||
posts: {
|
||||
posts: {},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
role: {
|
||||
permissions: [Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
rhs: {
|
||||
isSidebarOpen: false,
|
||||
},
|
||||
modals: {
|
||||
modalState: {},
|
||||
},
|
||||
browser: {
|
||||
windowSize: '',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
components: {
|
||||
CallButton: [{}],
|
||||
},
|
||||
plugins: {
|
||||
'com.mattermost.calls': {
|
||||
version: '0.4.2',
|
||||
},
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
'plugins-com.mattermost.calls': {
|
||||
profiles: {},
|
||||
},
|
||||
};
|
||||
const props: Props = {
|
||||
src: 'src',
|
||||
userId: user.id,
|
||||
hide: jest.fn(),
|
||||
channelId: 'channelId',
|
||||
};
|
||||
|
||||
return [props, state];
|
||||
}
|
||||
|
||||
describe('components/ProfilePopover', () => {
|
||||
(Client4.getCallsChannelState as jest.Mock).mockImplementation(async () => ({enabled: true}));
|
||||
|
||||
test('should mark shared user as shared', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
initialState.entities!.users!.profiles!.user1!.remote_id = 'fakeuser';
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(await screen.findByLabelText('shared user indicator')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should have bot description', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
initialState.entities!.users!.profiles!.user1!.is_bot = true;
|
||||
initialState.entities!.users!.profiles!.user1!.bot_description = 'bot description';
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(await screen.findByText('bot description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show add-to-channel option if in a team', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(await screen.findByLabelText('Add to a Channel dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should hide add-to-channel option if not on team', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
initialState.entities!.teams!.membersInTeam!.currentTeam = {};
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
|
||||
// Use find to wait for the first re-render because of the calls fetch
|
||||
await screen.findByText('user');
|
||||
|
||||
expect(await screen.queryByLabelText('Add to a Channel dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should match props passed into PopoverUserAttributes Pluggable component', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
const mockPluginComponent = ({
|
||||
hide,
|
||||
status,
|
||||
user,
|
||||
}: {
|
||||
hide: Props['hide'];
|
||||
status?: string;
|
||||
user: UserProfile;
|
||||
}) => {
|
||||
hide?.();
|
||||
return (<span>{`${status} ${user.id}`}</span>);
|
||||
};
|
||||
|
||||
initialState.plugins!.components!.PopoverUserAttributes = [{component: mockPluginComponent as any}];
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(props.hide).toHaveBeenCalled();
|
||||
expect(await screen.findByText('offline user1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should match props passed into PopoverUserActions Pluggable component', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
const mockPluginComponent = ({
|
||||
hide,
|
||||
status,
|
||||
user,
|
||||
}: {
|
||||
hide: Props['hide'];
|
||||
status?: string;
|
||||
user: UserProfile;
|
||||
}) => {
|
||||
hide?.();
|
||||
return (<span>{`${status} ${user.id}`}</span>);
|
||||
};
|
||||
|
||||
initialState.plugins!.components!.PopoverUserActions = [{component: mockPluginComponent as any}];
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(props.hide).toHaveBeenCalled();
|
||||
expect(await screen.findByText('offline user1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show custom status', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(await screen.findByText('In a meeting')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show to set a status for the current user', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
props.userId = initialState.entities!.users!.currentUserId!;
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(await screen.findByText('Set a status')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not show custom status expired', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
const customStatus = JSON.stringify({
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.TODAY,
|
||||
expires_at: '2021-05-03T23:59:59.000Z',
|
||||
});
|
||||
|
||||
initialState.entities!.users!.profiles!.user1!.props!.customStatus = customStatus;
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
|
||||
// Use find to wait for the first re-render because of the calls fetch
|
||||
await screen.findByText('user');
|
||||
|
||||
expect(await screen.queryByText('In a meeting')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show last active display', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(await screen.findByText('January 01, 1970')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not show last active display if disabled', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
initialState.entities!.general!.config!.EnableLastActiveTime = 'false';
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
|
||||
// Use find to wait for the first re-render because of the calls fetch
|
||||
await screen.findByText('user');
|
||||
expect(screen.queryByText('January 01, 1970')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show start a call button', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(await screen.findByLabelText('Start Call')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not show start a call button when calls are disabled', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
initialState.plugins!.plugins = {};
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(screen.queryByLabelText('Start Call')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should disable start call button when user is in another call', async () => {
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
(initialState as any)['plugins-com.mattermost.calls'].profiles = {fakeChannel: {currentUser: {id: 'currentUser'}}};
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
const button = (await screen.findByLabelText('Start Call')).closest('button');
|
||||
expect(button?.getAttribute('aria-disabled')).toBe('true');
|
||||
});
|
||||
|
||||
test('should not show the start call button when isCallsDefaultEnabledOnAllChannels, isCallsCanBeDisabledOnSpecificChannels is false and callsChannelState.enabled is false', async () => {
|
||||
(Client4.getCallsChannelState as jest.Mock).mockImplementationOnce(async () => ({enabled: false}));
|
||||
const [props, initialState] = getBasePropsAndState();
|
||||
|
||||
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
|
||||
expect(await screen.findByLabelText('Start Call')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {
|
||||
canManageAnyChannelMembersInCurrentTeam,
|
||||
getCurrentChannelId,
|
||||
getChannelByName,
|
||||
getChannelMember,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCallsConfig, getProfilesInCalls} from 'mattermost-redux/selectors/entities/common';
|
||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {
|
||||
getCurrentTeam,
|
||||
getCurrentRelativeTeamUrl,
|
||||
getTeamMember,
|
||||
} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {displayLastActiveLabel, getCurrentUserId, getLastActiveTimestampUnits, getLastActivityForUserId, getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {openDirectChannelToUserId} from 'actions/channel_actions';
|
||||
import {closeModal, openModal} from 'actions/views/modals';
|
||||
import {getMembershipForEntities} from 'actions/views/profile_popover';
|
||||
import {isCallsEnabled} from 'selectors/calls';
|
||||
import {getRhsState, getSelectedPost} from 'selectors/rhs';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled, isCustomStatusExpired} from 'selectors/views/custom_status';
|
||||
import {isAnyModalOpen} from 'selectors/views/modals';
|
||||
|
||||
import {getDirectChannelName} from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ProfilePopover from './profile_popover';
|
||||
|
||||
type OwnProps = {
|
||||
userId: string;
|
||||
channelId?: string;
|
||||
}
|
||||
|
||||
function getDefaultChannelId(state: GlobalState) {
|
||||
const selectedPost = getSelectedPost(state);
|
||||
return selectedPost.exists ? selectedPost.channel_id : getCurrentChannelId(state);
|
||||
}
|
||||
|
||||
export function checkUserInCall(state: GlobalState, userId: string) {
|
||||
for (const profilesMap of Object.values(getProfilesInCalls(state))) {
|
||||
for (const profile of Object.values(profilesMap || {})) {
|
||||
if (profile?.id === userId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getCustomStatus = makeGetCustomStatus();
|
||||
|
||||
return (state: GlobalState, {userId, channelId = getDefaultChannelId(state)}: OwnProps) => {
|
||||
const team = getCurrentTeam(state);
|
||||
const teamMember = getTeamMember(state, team.id, userId);
|
||||
|
||||
const isTeamAdmin = Boolean(teamMember && teamMember.scheme_admin);
|
||||
const channelMember = getChannelMember(state, channelId, userId);
|
||||
|
||||
let isChannelAdmin = false;
|
||||
if (getRhsState(state) !== 'search' && channelMember != null && channelMember.scheme_admin) {
|
||||
isChannelAdmin = true;
|
||||
}
|
||||
|
||||
const customStatus = getCustomStatus(state, userId);
|
||||
const status = getStatusForUserId(state, userId);
|
||||
const user = getUser(state, userId);
|
||||
|
||||
const lastActivityTimestamp = getLastActivityForUserId(state, userId);
|
||||
const timestampUnits = getLastActiveTimestampUnits(state, userId);
|
||||
const enableLastActiveTime = displayLastActiveLabel(state, userId);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const callsEnabled = isCallsEnabled(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const callsConfig = callsEnabled ? getCallsConfig(state) : undefined;
|
||||
|
||||
return {
|
||||
currentTeamId: team.id,
|
||||
currentUserId,
|
||||
isTeamAdmin,
|
||||
isChannelAdmin,
|
||||
isInCurrentTeam: Boolean(teamMember) && teamMember?.delete_at === 0,
|
||||
canManageAnyChannelMembersInCurrentTeam: canManageAnyChannelMembersInCurrentTeam(state),
|
||||
status,
|
||||
teamUrl: getCurrentRelativeTeamUrl(state),
|
||||
user,
|
||||
modals: state.views.modals,
|
||||
customStatus,
|
||||
isCustomStatusEnabled: isCustomStatusEnabled(state),
|
||||
isCustomStatusExpired: isCustomStatusExpired(state, customStatus),
|
||||
channelId,
|
||||
currentUserTimezone: getCurrentTimezone(state),
|
||||
lastActivityTimestamp,
|
||||
enableLastActiveTime,
|
||||
timestampUnits,
|
||||
isMobileView: getIsMobileView(state),
|
||||
isCallsEnabled: callsEnabled,
|
||||
isUserInCall: callsEnabled ? checkUserInCall(state, userId) : undefined,
|
||||
isCurrentUserInCall: callsEnabled ? checkUserInCall(state, currentUserId) : undefined,
|
||||
isCallsDefaultEnabledOnAllChannels: callsConfig?.DefaultEnabled,
|
||||
isCallsCanBeDisabledOnSpecificChannels: callsConfig?.AllowEnableCalls,
|
||||
dMChannel: getChannelByName(state, getDirectChannelName(currentUserId, userId)),
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
isAnyModalOpen: isAnyModalOpen(state),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
closeModal,
|
||||
openDirectChannelToUserId,
|
||||
openModal,
|
||||
getMembershipForEntities,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ProfilePopover);
|
||||
337
webapp/channels/src/components/profile_popover/index.tsx
Обычный файл
337
webapp/channels/src/components/profile_popover/index.tsx
Обычный файл
@@ -0,0 +1,337 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {getCurrentChannelId, getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
|
||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import type {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {openDirectChannelToUserId} from 'actions/channel_actions';
|
||||
import * as GlobalActions from 'actions/global_actions';
|
||||
import {closeModal} from 'actions/views/modals';
|
||||
import {getMembershipForEntities} from 'actions/views/profile_popover';
|
||||
import {getSelectedPost} from 'selectors/rhs';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
import {isAnyModalOpen as getIsAnyModalOpen} from 'selectors/views/modals';
|
||||
|
||||
import useDidUpdate from 'components/common/hooks/useDidUpdate';
|
||||
import Popover from 'components/widgets/popover';
|
||||
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import Constants, {A11yClassNames, A11yCustomEventTypes, UserStatuses} from 'utils/constants';
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {shouldFocusMainTextbox} from 'utils/post_utils';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ProfilePopoverActions from './profile_popover_actions';
|
||||
import ProfilePopoverAvatar from './profile_popover_avatar';
|
||||
import ProfilePopoverCustomStatus from './profile_popover_custom_status';
|
||||
import ProfilePopoverEdit from './profile_popover_edit';
|
||||
import ProfilePopoverEmail from './profile_popover_email';
|
||||
import ProfilePopoverLastActive from './profile_popover_last_active';
|
||||
import ProfilePopoverName from './profile_popover_name';
|
||||
import ProfilePopoverOverrideDisclaimer from './profile_popover_override_disclaimer';
|
||||
import ProfilePopoverTitle from './profile_popover_title';
|
||||
import ProfileTimezone from './profile_timezone';
|
||||
|
||||
import './profile_popover.scss';
|
||||
|
||||
export interface ProfilePopoverProps extends Omit<React.ComponentProps<typeof Popover>, 'id'> {
|
||||
|
||||
/**
|
||||
* Source URL from the image to display in the popover
|
||||
*/
|
||||
src: string;
|
||||
|
||||
/**
|
||||
* Source URL from the image that should override default image
|
||||
*/
|
||||
overwriteIcon?: string;
|
||||
|
||||
/**
|
||||
* Set to true of the popover was opened from a webhook post
|
||||
*/
|
||||
fromWebhook?: boolean;
|
||||
|
||||
userId: string;
|
||||
channelId?: string;
|
||||
|
||||
hideStatus?: boolean;
|
||||
|
||||
/**
|
||||
* Function to call to hide the popover
|
||||
*/
|
||||
hide?: () => void;
|
||||
|
||||
/**
|
||||
* Function to call to return focus to the previously focused element when the popover closes.
|
||||
* If not provided, the popover will automatically determine the previously focused element
|
||||
* and focus that on close. However, if the previously focused element is not correctly detected
|
||||
* by the popover, or the previously focused element will disappear after the popover opens,
|
||||
* it is necessary to provide this function to focus the correct element.
|
||||
*/
|
||||
returnFocus?: () => void;
|
||||
|
||||
/**
|
||||
* The overwritten username that should be shown at the top of the popover
|
||||
*/
|
||||
overwriteName?: string;
|
||||
}
|
||||
|
||||
function getDefaultChannelId(state: GlobalState) {
|
||||
const selectedPost = getSelectedPost(state);
|
||||
return selectedPost.exists ? selectedPost.channel_id : getCurrentChannelId(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* The profile popover, or hovercard, that appears with user information when clicking
|
||||
* on the username or profile picture of a user.
|
||||
*/
|
||||
const ProfilePopover = ({
|
||||
returnFocus: returnFocusProp,
|
||||
userId,
|
||||
channelId: channelIdProp,
|
||||
hide,
|
||||
overwriteIcon,
|
||||
overwriteName,
|
||||
src,
|
||||
hideStatus,
|
||||
fromWebhook,
|
||||
...restProps
|
||||
}: ProfilePopoverProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const dispatch = useDispatch<DispatchFunc>();
|
||||
const user = useSelector((state: GlobalState) => getUser(state, userId));
|
||||
const currentTeamId = useSelector((state: GlobalState) => getCurrentTeamId(state));
|
||||
const channelId = useSelector((state: GlobalState) => (channelIdProp || getDefaultChannelId(state)));
|
||||
const isAnyModalOpen = useSelector(getIsAnyModalOpen);
|
||||
const isMobileView = useSelector(getIsMobileView);
|
||||
const teamUrl = useSelector(getCurrentRelativeTeamUrl);
|
||||
const modals = useSelector((state: GlobalState) => state.views.modals);
|
||||
const teammateNameDisplay = useSelector(getTeammateNameDisplaySetting);
|
||||
const status = useSelector((state: GlobalState) => getStatusForUserId(state, userId) || UserStatuses.OFFLINE);
|
||||
const currentUserTimezone = useSelector(getCurrentTimezone);
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [loadingDMChannel, setLoadingDMChannel] = useState<string>();
|
||||
const returnFocus = useMemo(() => {
|
||||
if (returnFocusProp) {
|
||||
return returnFocusProp;
|
||||
}
|
||||
|
||||
const previouslyFocused = document.activeElement;
|
||||
return () => {
|
||||
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
||||
A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: previouslyFocused as HTMLElement,
|
||||
keyboardOnly: true,
|
||||
},
|
||||
},
|
||||
));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCloseModals = useCallback(() => {
|
||||
for (const modal in modals?.modalState) {
|
||||
if (!Object.prototype.hasOwnProperty.call(modals, modal)) {
|
||||
continue;
|
||||
}
|
||||
if (modals?.modalState[modal].open) {
|
||||
dispatch(closeModal(modal));
|
||||
}
|
||||
}
|
||||
}, [modals]);
|
||||
|
||||
const handleShowDirectChannel = useCallback(async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadingDMChannel !== undefined) {
|
||||
return;
|
||||
}
|
||||
setLoadingDMChannel(user.id);
|
||||
|
||||
handleCloseModals();
|
||||
const result = await dispatch(openDirectChannelToUserId(user.id));
|
||||
if (!result.error) {
|
||||
if (isMobileView) {
|
||||
GlobalActions.emitCloseRightHandSide();
|
||||
}
|
||||
setLoadingDMChannel(undefined);
|
||||
hide?.();
|
||||
getHistory().push(`${teamUrl}/messages/@${user.username}`);
|
||||
}
|
||||
}, [user, loadingDMChannel, handleCloseModals, isMobileView, hide, teamUrl]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (shouldFocusMainTextbox(e, document.activeElement)) {
|
||||
hide?.();
|
||||
} else if (Keyboard.isKeyPressed(e, Constants.KeyCodes.ESCAPE)) {
|
||||
returnFocus();
|
||||
}
|
||||
}, [hide, returnFocus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTeamId && userId) {
|
||||
dispatch(getMembershipForEntities(
|
||||
currentTeamId,
|
||||
userId,
|
||||
channelId,
|
||||
));
|
||||
}
|
||||
|
||||
// Focus the close button when the popover first opens, to bring the focus into the popover.
|
||||
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
||||
A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: closeButtonRef.current,
|
||||
keyboardOnly: true,
|
||||
},
|
||||
},
|
||||
));
|
||||
}, []);
|
||||
|
||||
useDidUpdate(() => {
|
||||
hide?.();
|
||||
}, [isAnyModalOpen]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlSrc = overwriteIcon || src;
|
||||
const haveOverrideProp = Boolean(overwriteIcon || overwriteName);
|
||||
const fullname = overwriteName || Utils.getFullName(user);
|
||||
|
||||
const displayName = displayUsername(user, teammateNameDisplay);
|
||||
|
||||
const tabCatcher = (
|
||||
<span
|
||||
tabIndex={0}
|
||||
onFocus={(e) => (e.relatedTarget as HTMLElement).focus()}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
{...restProps}
|
||||
id='user-profile-popover'
|
||||
>
|
||||
{tabCatcher}
|
||||
<div
|
||||
role='dialog'
|
||||
aria-label={formatMessage(
|
||||
{
|
||||
id: 'profile_popover.profileLabel',
|
||||
defaultMessage: 'Profile for {name}',
|
||||
},
|
||||
{name: displayName},
|
||||
)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={A11yClassNames.POPUP}
|
||||
aria-modal={true}
|
||||
>
|
||||
<ProfilePopoverTitle
|
||||
channelId={channelId}
|
||||
closeButtonRef={closeButtonRef}
|
||||
isBot={user.is_bot}
|
||||
returnFocus={returnFocus}
|
||||
roles={user.roles}
|
||||
userId={user.id}
|
||||
username={user.username}
|
||||
hide={hide}
|
||||
/>
|
||||
<div className='user-profile-popover__content'>
|
||||
<ProfilePopoverAvatar
|
||||
hideStatus={hideStatus}
|
||||
urlSrc={urlSrc}
|
||||
username={user.username}
|
||||
status={status}
|
||||
/>
|
||||
<ProfilePopoverLastActive userId={user.id}/>
|
||||
<ProfilePopoverName
|
||||
user={user}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
fullname={fullname}
|
||||
/>
|
||||
<hr className='divider divider--expanded'/>
|
||||
<ProfilePopoverEmail
|
||||
email={user.email}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
isBot={user.is_bot}
|
||||
/>
|
||||
<Pluggable
|
||||
pluggableName='PopoverUserAttributes'
|
||||
user={user}
|
||||
hide={hide}
|
||||
status={hideStatus ? null : status}
|
||||
fromWebhook={fromWebhook}
|
||||
/>
|
||||
<ProfileTimezone
|
||||
currentUserTimezone={currentUserTimezone}
|
||||
profileUserTimezone={user.timezone}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
/>
|
||||
<ProfilePopoverCustomStatus
|
||||
currentUserId={currentUserId}
|
||||
currentUserTimezone={currentUserTimezone}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
hideStatus={hideStatus}
|
||||
user={user}
|
||||
returnFocus={returnFocus}
|
||||
hide={hide}
|
||||
/>
|
||||
<ProfilePopoverEdit
|
||||
currentUserId={currentUserId}
|
||||
handleCloseModals={handleCloseModals}
|
||||
handleShowDirectChannel={handleShowDirectChannel}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
returnFocus={returnFocus}
|
||||
userId={user.id}
|
||||
hide={hide}
|
||||
/>
|
||||
<ProfilePopoverOverrideDisclaimer
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
username={user.username}
|
||||
/>
|
||||
<ProfilePopoverActions
|
||||
channelId={channelId}
|
||||
currentUserId={currentUserId}
|
||||
fullname={fullname}
|
||||
handleCloseModals={handleCloseModals}
|
||||
handleShowDirectChannel={handleShowDirectChannel}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
returnFocus={returnFocus}
|
||||
user={user}
|
||||
hide={hide}
|
||||
/>
|
||||
<Pluggable
|
||||
pluggableName='PopoverUserActions'
|
||||
user={user}
|
||||
hide={hide}
|
||||
status={hideStatus ? null : status}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{tabCatcher}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ProfilePopover);
|
||||
@@ -1,339 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {CustomStatusDuration} from '@mattermost/types/users';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import {checkUserInCall} from 'components/profile_popover';
|
||||
import ProfilePopover from 'components/profile_popover/profile_popover';
|
||||
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
import {mountWithIntl, shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {mockStore} from 'tests/test_store';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
describe('components/ProfilePopover', () => {
|
||||
const baseProps = {
|
||||
userId: '0',
|
||||
user: TestHelper.getUserMock({
|
||||
username: 'some_username',
|
||||
}),
|
||||
hide: jest.fn(),
|
||||
src: 'src',
|
||||
currentUserId: '',
|
||||
currentTeamId: 'team_id',
|
||||
isChannelAdmin: false,
|
||||
isTeamAdmin: false,
|
||||
isInCurrentTeam: true,
|
||||
teamUrl: '',
|
||||
canManageAnyChannelMembersInCurrentTeam: true,
|
||||
isCustomStatusEnabled: true,
|
||||
isCustomStatusExpired: false,
|
||||
isMobileView: false,
|
||||
actions: {
|
||||
getMembershipForEntities: jest.fn(),
|
||||
openDirectChannelToUserId: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
closeModal: jest.fn(),
|
||||
loadBot: jest.fn(),
|
||||
},
|
||||
lastActivityTimestamp: 1632146562846,
|
||||
enableLastActiveTime: true,
|
||||
timestampUnits: [
|
||||
'now',
|
||||
'minute',
|
||||
'hour',
|
||||
],
|
||||
isCallsEnabled: true,
|
||||
isCallsDefaultEnabledOnAllChannels: true,
|
||||
teammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME,
|
||||
isAnyModalOpen: false,
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
entities: {
|
||||
teams: {},
|
||||
channels: {
|
||||
channels: {},
|
||||
myMembers: {},
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
users: {
|
||||
currentUserId: '',
|
||||
profiles: {
|
||||
user1: {
|
||||
id: 'user1',
|
||||
roles: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
groups: {
|
||||
groups: {},
|
||||
myGroups: [],
|
||||
},
|
||||
emojis: {
|
||||
customEmoji: {},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
components: {
|
||||
CallButton: [],
|
||||
},
|
||||
},
|
||||
views: {
|
||||
rhs: {
|
||||
isSidebarOpen: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const props = {...baseProps};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for shared user', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
user: TestHelper.getUserMock({
|
||||
username: 'shared_user',
|
||||
first_name: 'shared',
|
||||
remote_id: 'fakeuser',
|
||||
}),
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should have bot description', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
user: TestHelper.getUserMock({
|
||||
is_bot: true,
|
||||
bot_description: 'bot description',
|
||||
}),
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper.containsMatchingElement(
|
||||
<div
|
||||
key='bot-description'
|
||||
>
|
||||
{'bot description'}
|
||||
</div>,
|
||||
)).toEqual(true);
|
||||
});
|
||||
|
||||
test('should hide add-to-channel option if not on team', () => {
|
||||
const props = {...baseProps};
|
||||
props.isInCurrentTeam = false;
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match props passed into Pluggable component', () => {
|
||||
const hide = jest.fn();
|
||||
const status = 'online';
|
||||
const props = {...baseProps, hide, status};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
|
||||
const pluggableProps = {
|
||||
hide,
|
||||
status,
|
||||
user: props.user,
|
||||
};
|
||||
expect(wrapper.find(Pluggable).first().props()).toEqual({...pluggableProps, pluggableName: 'PopoverUserAttributes'});
|
||||
expect(wrapper.find(Pluggable).last().props()).toEqual({...pluggableProps, pluggableName: 'PopoverUserActions'});
|
||||
});
|
||||
|
||||
test('should match snapshot with custom status', () => {
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.TODAY,
|
||||
expires_at: '2021-05-03T23:59:59.000Z',
|
||||
};
|
||||
const props = {
|
||||
...baseProps,
|
||||
customStatus,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with custom status not set but can set', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
user: {
|
||||
...baseProps.user,
|
||||
id: '',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with custom status expired', () => {
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.TODAY,
|
||||
expires_at: '2021-05-03T23:59:59.000Z',
|
||||
};
|
||||
const props = {
|
||||
...baseProps,
|
||||
isCustomStatusExpired: true,
|
||||
customStatus,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with last active display', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
status: 'offline',
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with no last active display because it is disabled', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
enableLastActiveTime: false,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when calls are disabled', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isCallsEnabled: false,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should disable start call button when user is in another call', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isUserInCall: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ProfilePopover {...props}/>,
|
||||
);
|
||||
expect(wrapper.find('#startCallButton').hasClass('icon-btn-disabled')).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show the start call button when isCallsDefaultEnabledOnAllChannels, isCallsCanBeDisabledOnSpecificChannels is false and callsChannelState.enabled is true', () => {
|
||||
const mock = mockStore(initialState);
|
||||
const props = {
|
||||
...baseProps,
|
||||
isCallsDefaultEnabledOnAllChannels: false,
|
||||
isCallsCanBeDisabledOnSpecificChannels: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={mock.store}>
|
||||
<ProfilePopover {...props}/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper.find('ProfilePopoverCallButton').exists()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkUserInCall', () => {
|
||||
test('missing state', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {},
|
||||
} as any, 'userA')).toBe(false);
|
||||
});
|
||||
|
||||
test('call state missing', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {
|
||||
profiles: {
|
||||
channelID: null,
|
||||
},
|
||||
},
|
||||
} as any, 'userA')).toBe(false);
|
||||
});
|
||||
|
||||
test('user not in call', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {
|
||||
profiles: {
|
||||
channelID: {
|
||||
sessionB: {
|
||||
id: 'userB',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any, 'userA')).toBe(false);
|
||||
});
|
||||
|
||||
test('user in call', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {
|
||||
profiles: {
|
||||
channelID: {
|
||||
sessionB: {
|
||||
id: 'userB',
|
||||
},
|
||||
sessionA: {
|
||||
id: 'userA',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any, 'userA')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,913 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
import {AccountOutlineIcon, AccountPlusOutlineIcon, CloseIcon, EmoticonHappyOutlineIcon, PhoneInTalkIcon, SendIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserCustomStatus, UserProfile} from '@mattermost/types/users';
|
||||
import {CustomStatusDuration} from '@mattermost/types/users';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {displayUsername, isGuest, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions';
|
||||
|
||||
import AddUserToChannelModal from 'components/add_user_to_channel_modal';
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import CustomStatusModal from 'components/custom_status/custom_status_modal';
|
||||
import CustomStatusText from 'components/custom_status/custom_status_text';
|
||||
import ExpiryTime from 'components/custom_status/expiry_time';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import ProfilePopoverCallButton from 'components/profile_popover_call_button';
|
||||
import SharedUserIndicator from 'components/shared_user_indicator';
|
||||
import StatusIcon from 'components/status_icon';
|
||||
import Timestamp from 'components/timestamp';
|
||||
import ToggleModalButton from 'components/toggle_modal_button';
|
||||
import Tooltip from 'components/tooltip';
|
||||
import UserSettingsModal from 'components/user_settings/modal';
|
||||
import Popover from 'components/widgets/popover';
|
||||
import BotTag from 'components/widgets/tag/bot_tag';
|
||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||
import Tag from 'components/widgets/tag/tag';
|
||||
import Avatar from 'components/widgets/users/avatar';
|
||||
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import Constants, {A11yClassNames, A11yCustomEventTypes, ModalIdentifiers, UserStatuses} from 'utils/constants';
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {shouldFocusMainTextbox} from 'utils/post_utils';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
import type {ModalData} from 'types/actions';
|
||||
|
||||
import {ProfileTimezone} from './profile_localtime';
|
||||
|
||||
import './profile_popover.scss';
|
||||
|
||||
export interface ProfilePopoverProps extends Omit<React.ComponentProps<typeof Popover>, 'id'> {
|
||||
|
||||
/**
|
||||
* Source URL from the image to display in the popover
|
||||
*/
|
||||
src: string;
|
||||
|
||||
/**
|
||||
* Source URL from the image that should override default image
|
||||
*/
|
||||
overwriteIcon?: string;
|
||||
|
||||
/**
|
||||
* Set to true of the popover was opened from a webhook post
|
||||
*/
|
||||
fromWebhook?: boolean;
|
||||
|
||||
/**
|
||||
* User the popover is being opened for
|
||||
*/
|
||||
user?: UserProfile;
|
||||
userId: string;
|
||||
channelId?: string;
|
||||
|
||||
/**
|
||||
* Status for the user, either 'offline', 'away', 'dnd' or 'online'
|
||||
*/
|
||||
status?: string;
|
||||
hideStatus?: boolean;
|
||||
|
||||
/**
|
||||
* Function to call to hide the popover
|
||||
*/
|
||||
hide?: () => void;
|
||||
|
||||
/**
|
||||
* Function to call to return focus to the previously focused element when the popover closes.
|
||||
* If not provided, the popover will automatically determine the previously focused element
|
||||
* and focus that on close. However, if the previously focused element is not correctly detected
|
||||
* by the popover, or the previously focused element will disappear after the popover opens,
|
||||
* it is necessary to provide this function to focus the correct element.
|
||||
*/
|
||||
returnFocus?: () => void;
|
||||
|
||||
isBusy?: boolean;
|
||||
isMobileView: boolean;
|
||||
|
||||
/**
|
||||
* Returns state of modals in redux for determing which need to be closed
|
||||
*/
|
||||
modals?: {
|
||||
modalState: {
|
||||
[modalId: string]: {
|
||||
open: boolean;
|
||||
dialogProps: Record<string, any>;
|
||||
dialogType: React.ComponentType;
|
||||
};
|
||||
};
|
||||
};
|
||||
currentTeamId: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
currentUserId: string;
|
||||
customStatus?: UserCustomStatus | null;
|
||||
isCustomStatusEnabled: boolean;
|
||||
isCustomStatusExpired: boolean;
|
||||
currentUserTimezone?: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
hasMention?: boolean;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
isInCurrentTeam: boolean;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
teamUrl: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
isTeamAdmin: boolean;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
isChannelAdmin: boolean;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
canManageAnyChannelMembersInCurrentTeam: boolean;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
teammateNameDisplay: string;
|
||||
|
||||
/**
|
||||
* The overwritten username that should be shown at the top of the popover
|
||||
*/
|
||||
overwriteName?: React.ReactNode;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
actions: {
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
closeModal: (modalId: string) => void;
|
||||
openDirectChannelToUserId: (userId: string) => Promise<ActionResult>;
|
||||
getMembershipForEntities: (teamId: string, userId: string, channelId?: string) => void;
|
||||
};
|
||||
intl: IntlShape;
|
||||
lastActivityTimestamp: number;
|
||||
enableLastActiveTime: boolean;
|
||||
timestampUnits: string[];
|
||||
isCallsEnabled: boolean;
|
||||
isUserInCall?: boolean;
|
||||
isCurrentUserInCall?: boolean;
|
||||
isCallsDefaultEnabledOnAllChannels?: boolean;
|
||||
isCallsCanBeDisabledOnSpecificChannels?: boolean;
|
||||
dMChannel?: Channel | null;
|
||||
isAnyModalOpen: boolean;
|
||||
}
|
||||
type ProfilePopoverState = {
|
||||
loadingDMChannel?: string;
|
||||
callsChannelState?: ChannelCallsState;
|
||||
callsDMChannelState?: ChannelCallsState;
|
||||
};
|
||||
|
||||
type ChannelCallsState = {
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The profile popover, or hovercard, that appears with user information when clicking
|
||||
* on the username or profile picture of a user.
|
||||
*/
|
||||
|
||||
class ProfilePopover extends React.PureComponent<ProfilePopoverProps, ProfilePopoverState> {
|
||||
closeButtonRef: React.RefObject<HTMLButtonElement>;
|
||||
returnFocus: () => void;
|
||||
|
||||
static getComponentName() {
|
||||
return 'ProfilePopover';
|
||||
}
|
||||
static defaultProps = {
|
||||
hasMention: false,
|
||||
status: UserStatuses.OFFLINE,
|
||||
customStatus: null,
|
||||
};
|
||||
constructor(props: ProfilePopoverProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loadingDMChannel: undefined,
|
||||
};
|
||||
this.closeButtonRef = React.createRef();
|
||||
|
||||
if (this.props.returnFocus) {
|
||||
this.returnFocus = this.props.returnFocus;
|
||||
} else {
|
||||
const previouslyFocused = document.activeElement;
|
||||
this.returnFocus = () => {
|
||||
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
||||
A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: previouslyFocused as HTMLElement,
|
||||
keyboardOnly: true,
|
||||
},
|
||||
},
|
||||
));
|
||||
};
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
const {currentTeamId, userId, channelId} = this.props;
|
||||
if (currentTeamId && userId) {
|
||||
this.props.actions.getMembershipForEntities(
|
||||
currentTeamId,
|
||||
userId,
|
||||
channelId,
|
||||
);
|
||||
}
|
||||
if (this.props.isCallsEnabled && this.props.dMChannel) {
|
||||
this.getCallsChannelState(this.props.dMChannel.id).then((data) => {
|
||||
this.setState({callsDMChannelState: data});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.isCallsEnabled && this.props.channelId) {
|
||||
this.getCallsChannelState(this.props.channelId).then((data) => {
|
||||
this.setState({callsChannelState: data});
|
||||
});
|
||||
}
|
||||
|
||||
// Focus the close button when the popover first opens, to bring the focus into the popover.
|
||||
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
||||
A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: this.closeButtonRef.current,
|
||||
keyboardOnly: true,
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ProfilePopoverProps) {
|
||||
if (this.props.isAnyModalOpen !== prevProps.isAnyModalOpen) {
|
||||
this.props.hide?.();
|
||||
}
|
||||
}
|
||||
|
||||
handleShowDirectChannel = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const {actions} = this.props;
|
||||
e.preventDefault();
|
||||
if (!this.props.user) {
|
||||
return;
|
||||
}
|
||||
const user = this.props.user;
|
||||
if (this.state.loadingDMChannel !== undefined) {
|
||||
return;
|
||||
}
|
||||
this.setState({loadingDMChannel: user.id});
|
||||
actions.openDirectChannelToUserId(user.id).then((result: ActionResult) => {
|
||||
if (!result.error) {
|
||||
if (this.props.isMobileView) {
|
||||
GlobalActions.emitCloseRightHandSide();
|
||||
}
|
||||
this.setState({loadingDMChannel: undefined});
|
||||
this.props.hide?.();
|
||||
getHistory().push(`${this.props.teamUrl}/messages/@${user.username}`);
|
||||
}
|
||||
});
|
||||
this.handleCloseModals();
|
||||
};
|
||||
handleEditAccountSettings = () => {
|
||||
if (!this.props.user) {
|
||||
return;
|
||||
}
|
||||
this.props.hide?.();
|
||||
this.props.actions.openModal({
|
||||
modalId: ModalIdentifiers.USER_SETTINGS,
|
||||
dialogType: UserSettingsModal,
|
||||
dialogProps: {isContentProductSettings: false, onExited: this.returnFocus},
|
||||
});
|
||||
this.handleCloseModals();
|
||||
};
|
||||
showCustomStatusModal = () => {
|
||||
this.props.hide?.();
|
||||
const customStatusInputModalData = {
|
||||
modalId: ModalIdentifiers.CUSTOM_STATUS,
|
||||
dialogType: CustomStatusModal,
|
||||
dialogProps: {onExited: this.returnFocus},
|
||||
};
|
||||
this.props.actions.openModal(customStatusInputModalData);
|
||||
};
|
||||
handleAddToChannel = () => {
|
||||
this.props.hide?.();
|
||||
this.handleCloseModals();
|
||||
};
|
||||
handleCloseModals = () => {
|
||||
const {modals} = this.props;
|
||||
for (const modal in modals?.modalState) {
|
||||
if (!Object.prototype.hasOwnProperty.call(modals, modal)) {
|
||||
continue;
|
||||
}
|
||||
if (modals?.modalState[modal].open) {
|
||||
this.props.actions.closeModal(modal);
|
||||
}
|
||||
}
|
||||
};
|
||||
handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (shouldFocusMainTextbox(e, document.activeElement)) {
|
||||
this.props.hide?.();
|
||||
} else if (Keyboard.isKeyPressed(e, Constants.KeyCodes.ESCAPE)) {
|
||||
this.returnFocus();
|
||||
}
|
||||
};
|
||||
renderCustomStatus() {
|
||||
const {
|
||||
customStatus,
|
||||
isCustomStatusEnabled,
|
||||
user,
|
||||
currentUserId,
|
||||
hideStatus,
|
||||
isCustomStatusExpired,
|
||||
} = this.props;
|
||||
const customStatusSet = (customStatus?.text || customStatus?.emoji) && !isCustomStatusExpired;
|
||||
const canSetCustomStatus = user?.id === currentUserId;
|
||||
const shouldShowCustomStatus =
|
||||
isCustomStatusEnabled &&
|
||||
!hideStatus &&
|
||||
(customStatusSet || canSetCustomStatus);
|
||||
if (!shouldShowCustomStatus) {
|
||||
return null;
|
||||
}
|
||||
let customStatusContent;
|
||||
let expiryContent;
|
||||
if (customStatusSet) {
|
||||
const customStatusEmoji = (
|
||||
<CustomStatusEmoji
|
||||
userID={this.props.user?.id}
|
||||
showTooltip={false}
|
||||
emojiStyle={{
|
||||
marginRight: 4,
|
||||
marginTop: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
customStatusContent = (
|
||||
<div className='d-flex align-items-center'>
|
||||
{customStatusEmoji}
|
||||
<CustomStatusText
|
||||
tooltipDirection='top'
|
||||
text={customStatus?.text || ''}
|
||||
className='user-popover__email'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
expiryContent = customStatusSet && customStatus?.expires_at && customStatus.duration !== CustomStatusDuration.DONT_CLEAR && (
|
||||
<ExpiryTime
|
||||
time={customStatus.expires_at}
|
||||
timezone={this.props.currentUserTimezone}
|
||||
className='ml-1'
|
||||
withinBrackets={true}
|
||||
/>
|
||||
);
|
||||
} else if (canSetCustomStatus) {
|
||||
customStatusContent = (
|
||||
<button
|
||||
className='user-popover__set-custom-status-btn'
|
||||
onClick={this.showCustomStatusModal}
|
||||
>
|
||||
<EmoticonHappyOutlineIcon size={14}/>
|
||||
<FormattedMessage
|
||||
id='user_profile.custom_status.set_status'
|
||||
defaultMessage='Set a status'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return {customStatusContent, expiryContent};
|
||||
}
|
||||
handleClose = () => {
|
||||
this.props.hide?.();
|
||||
this.returnFocus();
|
||||
};
|
||||
getCallsChannelState(channelId: string): Promise<ChannelCallsState> {
|
||||
let data: Promise<ChannelCallsState>;
|
||||
try {
|
||||
data = Client4.getCallsChannelState(channelId);
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
render() {
|
||||
if (!this.props.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keysToBeRemoved: Array<keyof ProfilePopoverProps> = ['user', 'userId', 'channelId', 'src', 'status', 'hideStatus', 'isBusy',
|
||||
'hide', 'hasMention', 'currentUserId', 'currentTeamId', 'teamUrl', 'actions', 'isTeamAdmin',
|
||||
'isChannelAdmin', 'canManageAnyChannelMembersInCurrentTeam', 'intl'];
|
||||
const popoverProps: React.ComponentProps<typeof Popover> = Utils.deleteKeysFromObject({...this.props},
|
||||
keysToBeRemoved);
|
||||
const {formatMessage} = this.props.intl;
|
||||
const dataContent = [];
|
||||
const urlSrc = this.props.overwriteIcon ? this.props.overwriteIcon : this.props.src;
|
||||
dataContent.push(
|
||||
<div
|
||||
className='user-popover-image'
|
||||
key='user-popover-image'
|
||||
>
|
||||
<Avatar
|
||||
id='userAvatar'
|
||||
size='xxl'
|
||||
username={this.props.user?.username || ''}
|
||||
url={urlSrc}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<StatusIcon
|
||||
className='status user-popover-status'
|
||||
status={this.props.hideStatus ? undefined : this.props.status}
|
||||
button={true}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
if (this.props.enableLastActiveTime && this.props.lastActivityTimestamp && this.props.timestampUnits) {
|
||||
dataContent.push(
|
||||
<div
|
||||
className='user-popover-last-active'
|
||||
key='user-popover-last-active'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.lastOnline'
|
||||
defaultMessage='Last online {timestamp}'
|
||||
values={{
|
||||
timestamp: (
|
||||
<Timestamp
|
||||
value={this.props.lastActivityTimestamp}
|
||||
units={this.props.timestampUnits}
|
||||
useTime={false}
|
||||
style={'short'}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
const fullname = this.props.overwriteName ? this.props.overwriteName : Utils.getFullName(this.props.user);
|
||||
const haveOverrideProp = this.props.overwriteIcon || this.props.overwriteName;
|
||||
if (fullname) {
|
||||
let sharedIcon;
|
||||
if (this.props.user.remote_id) {
|
||||
sharedIcon = (
|
||||
<SharedUserIndicator
|
||||
className='shared-user-icon'
|
||||
withTooltip={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
dataContent.push(
|
||||
<div
|
||||
data-testid={`popover-fullname-${this.props.user.username}`}
|
||||
className='overflow--ellipsis pb-1'
|
||||
key='user-popover-fullname'
|
||||
>
|
||||
<span className='user-profile-popover__heading'>{fullname}</span>
|
||||
{sharedIcon}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (this.props.user.is_bot && !haveOverrideProp) {
|
||||
dataContent.push(
|
||||
<div
|
||||
key='bot-description'
|
||||
className='overflow--ellipsis text-nowrap pb-1'
|
||||
>
|
||||
{this.props.user.bot_description}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
const userNameClass = classNames('overflow--ellipsis pb-1', {'user-profile-popover__heading': fullname === ''});
|
||||
const userName: React.ReactNode = `@${this.props.user.username}`;
|
||||
dataContent.push(
|
||||
<div
|
||||
id='userPopoverUsername'
|
||||
className={userNameClass}
|
||||
key='user-popover-username'
|
||||
>
|
||||
{userName}
|
||||
</div>,
|
||||
);
|
||||
if (this.props.user.position && !haveOverrideProp) {
|
||||
const position = (this.props.user?.position || '').substring(
|
||||
0,
|
||||
Constants.MAX_POSITION_LENGTH,
|
||||
);
|
||||
dataContent.push(
|
||||
<div
|
||||
className='text-center'
|
||||
key='user-popover-position'
|
||||
>
|
||||
{position}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
dataContent.push(
|
||||
<hr
|
||||
key='user-popover-hr'
|
||||
className='divider divider--expanded'
|
||||
/>,
|
||||
);
|
||||
const email = this.props.user.email || '';
|
||||
if (email && !this.props.user.is_bot && !haveOverrideProp) {
|
||||
dataContent.push(
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
title={email}
|
||||
key='user-popover-email'
|
||||
>
|
||||
<a
|
||||
href={'mailto:' + email}
|
||||
className='text-nowrap text-lowercase user-popover__email pb-1'
|
||||
>
|
||||
{email}
|
||||
</a>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
dataContent.push(
|
||||
<Pluggable
|
||||
key='profilePopoverPluggable2'
|
||||
pluggableName='PopoverUserAttributes'
|
||||
user={this.props.user}
|
||||
hide={this.props.hide}
|
||||
status={this.props.hideStatus ? null : this.props.status}
|
||||
fromWebhook={this.props.fromWebhook}
|
||||
/>,
|
||||
);
|
||||
if (this.props.user.timezone && !haveOverrideProp) {
|
||||
dataContent.push(
|
||||
<ProfileTimezone
|
||||
currentUserTimezone={this.props.currentUserTimezone}
|
||||
profileUserTimezone={this.props.user.timezone}
|
||||
key='user-popover-local-time'
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
const customStatusAndExpiryContent = !haveOverrideProp && this.renderCustomStatus();
|
||||
if (customStatusAndExpiryContent) {
|
||||
const {customStatusContent, expiryContent} = customStatusAndExpiryContent;
|
||||
dataContent.push(
|
||||
<div
|
||||
key='user-popover-status'
|
||||
id='user-popover-status'
|
||||
className='user-popover__time-status-container'
|
||||
>
|
||||
<span className='user-popover__subtitle'>
|
||||
<FormattedMessage
|
||||
id='user_profile.custom_status'
|
||||
defaultMessage='Status'
|
||||
/>
|
||||
{expiryContent}
|
||||
</span>
|
||||
{customStatusContent}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
const sendMessageTooltip = (
|
||||
<Tooltip id='sendMessageTooltip'>
|
||||
<FormattedMessage
|
||||
id='user_profile.send.dm.yourself'
|
||||
defaultMessage='Send yourself a message'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
if (this.props.user.id === this.props.currentUserId && !haveOverrideProp) {
|
||||
dataContent.push(
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
key='user-popover-settings'
|
||||
className='popover__row first'
|
||||
>
|
||||
<button
|
||||
id='editProfileButton'
|
||||
type='button'
|
||||
className='btn'
|
||||
onClick={this.handleEditAccountSettings}
|
||||
>
|
||||
<AccountOutlineIcon
|
||||
size={16}
|
||||
aria-label={formatMessage({
|
||||
id: t('generic_icons.edit'),
|
||||
defaultMessage: 'Edit Icon',
|
||||
})}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='user_profile.account.editProfile'
|
||||
defaultMessage='Edit Profile'
|
||||
/>
|
||||
</button>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={sendMessageTooltip}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='btn icon-btn'
|
||||
onClick={this.handleShowDirectChannel}
|
||||
>
|
||||
<SendIcon
|
||||
size={18}
|
||||
aria-label={formatMessage({
|
||||
id: t('user_profile.send.dm.icon'),
|
||||
defaultMessage: 'Send Message Icon',
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (haveOverrideProp) {
|
||||
dataContent.push(
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
key='user-popover-settings'
|
||||
className='popover__row first'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='user_profile.account.post_was_created'
|
||||
defaultMessage='This post was created by an integration from @{username}'
|
||||
values={{
|
||||
username: this.props.user.username,
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
const addToChannelMessage = formatMessage({
|
||||
id: 'user_profile.add_user_to_channel',
|
||||
defaultMessage: 'Add to a Channel',
|
||||
});
|
||||
|
||||
const addToChannelButton = (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={
|
||||
<Tooltip id='addToChannelTooltip'>
|
||||
{addToChannelMessage}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<ToggleModalButton
|
||||
id='addToChannelButton'
|
||||
className='btn icon-btn'
|
||||
ariaLabel={addToChannelMessage}
|
||||
modalId={ModalIdentifiers.ADD_USER_TO_CHANNEL}
|
||||
dialogType={AddUserToChannelModal}
|
||||
dialogProps={{user: this.props.user, onExited: this.returnFocus}}
|
||||
onClick={this.handleAddToChannel}
|
||||
>
|
||||
<AccountPlusOutlineIcon
|
||||
size={18}
|
||||
aria-label={formatMessage({
|
||||
id: t('user_profile.add_user_to_channel.icon'),
|
||||
defaultMessage: 'Add User to Channel Icon',
|
||||
})}
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
const renderCallButton = () => {
|
||||
const {isCallsEnabled, isCallsDefaultEnabledOnAllChannels, isCallsCanBeDisabledOnSpecificChannels, dMChannel} = this.props;
|
||||
if (
|
||||
!isCallsEnabled ||
|
||||
this.state.callsDMChannelState?.enabled === false ||
|
||||
(!isCallsDefaultEnabledOnAllChannels && !isCallsCanBeDisabledOnSpecificChannels && this.state.callsChannelState?.enabled === false)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const disabled = this.props.isUserInCall || this.props.isCurrentUserInCall;
|
||||
const startCallMessage = this.props.isUserInCall ? formatMessage({
|
||||
id: t('user_profile.call.userBusy'),
|
||||
defaultMessage: '{user} is in another call',
|
||||
}, {user: fullname === '' ? this.props.user?.username : fullname},
|
||||
) : formatMessage({
|
||||
id: t('webapp.mattermost.feature.start_call'),
|
||||
defaultMessage: 'Start Call',
|
||||
});
|
||||
const iconButtonClassname = classNames('btn icon-btn', {'icon-btn-disabled': disabled});
|
||||
const callButton = (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={
|
||||
<Tooltip id='startCallTooltip'>
|
||||
{startCallMessage}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<button
|
||||
id='startCallButton'
|
||||
type='button'
|
||||
aria-disabled={disabled}
|
||||
className={iconButtonClassname}
|
||||
>
|
||||
<PhoneInTalkIcon
|
||||
size={18}
|
||||
aria-label={startCallMessage}
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
if (disabled) {
|
||||
return callButton;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProfilePopoverCallButton
|
||||
dmChannel={dMChannel}
|
||||
userId={this.props.userId}
|
||||
customButton={callButton}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (this.props.user.id !== this.props.currentUserId && !haveOverrideProp) {
|
||||
dataContent.push(
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
key='user-popover-dm'
|
||||
className='popover__row first'
|
||||
>
|
||||
<button
|
||||
id='messageButton'
|
||||
type='button'
|
||||
className='btn'
|
||||
onClick={this.handleShowDirectChannel}
|
||||
>
|
||||
<SendIcon
|
||||
size={16}
|
||||
aria-label={formatMessage({
|
||||
id: t('user_profile.send.dm.icon'),
|
||||
defaultMessage: 'Send Message Icon',
|
||||
})}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='user_profile.send.dm'
|
||||
defaultMessage='Message'
|
||||
/>
|
||||
</button>
|
||||
<div className='popover_row-controlContainer'>
|
||||
{(this.props.canManageAnyChannelMembersInCurrentTeam && this.props.isInCurrentTeam) ? addToChannelButton : null}
|
||||
{renderCallButton()}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
dataContent.push(
|
||||
<Pluggable
|
||||
key='profilePopoverPluggable3'
|
||||
pluggableName='PopoverUserActions'
|
||||
user={this.props.user}
|
||||
hide={this.props.hide}
|
||||
status={this.props.hideStatus ? null : this.props.status}
|
||||
/>,
|
||||
);
|
||||
let roleTitle;
|
||||
if (this.props.user.is_bot) {
|
||||
roleTitle = (
|
||||
<BotTag
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
/>
|
||||
);
|
||||
} else if (isGuest(this.props.user.roles)) {
|
||||
roleTitle = (
|
||||
<GuestTag
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
/>
|
||||
);
|
||||
} else if (isSystemAdmin(this.props.user.roles)) {
|
||||
roleTitle = (
|
||||
<Tag
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
text={Utils.localizeMessage(
|
||||
'admin.permissions.roles.system_admin.name',
|
||||
'System Admin',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
} else if (this.props.isTeamAdmin) {
|
||||
roleTitle = (
|
||||
<Tag
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
text={Utils.localizeMessage(
|
||||
'admin.permissions.roles.team_admin.name',
|
||||
'Team Admin',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
} else if (this.props.isChannelAdmin) {
|
||||
roleTitle = (
|
||||
<Tag
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
text={Utils.localizeMessage(
|
||||
'admin.permissions.roles.channel_admin.name',
|
||||
'Channel Admin',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const title = (
|
||||
<span data-testid={`profilePopoverTitle_${this.props.user.username}`}>
|
||||
{roleTitle}
|
||||
<button
|
||||
ref={this.closeButtonRef}
|
||||
className='user-popover__close'
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
<CloseIcon
|
||||
size={18}
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
|
||||
const displayName = displayUsername(this.props.user, this.props.teammateNameDisplay);
|
||||
const titleClassName = classNames('popover-title', {'popover-title_height': !roleTitle});
|
||||
|
||||
const tabCatcher = (
|
||||
<span
|
||||
tabIndex={0}
|
||||
onFocus={(e) => (e.relatedTarget as HTMLElement).focus()}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
{...popoverProps}
|
||||
id='user-profile-popover'
|
||||
>
|
||||
{tabCatcher}
|
||||
<div
|
||||
role='dialog'
|
||||
aria-label={formatMessage(
|
||||
{
|
||||
id: 'profile_popover.profileLabel',
|
||||
defaultMessage: 'Profile for {name}',
|
||||
},
|
||||
{
|
||||
name: displayName,
|
||||
},
|
||||
)}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
className={A11yClassNames.POPUP}
|
||||
aria-modal={true}
|
||||
>
|
||||
<div className={titleClassName}>
|
||||
{title}
|
||||
</div>
|
||||
<div className='user-profile-popover__content'>
|
||||
{dataContent}
|
||||
</div>
|
||||
</div>
|
||||
{tabCatcher}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(ProfilePopover);
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {AccountPlusOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {canManageAnyChannelMembersInCurrentTeam as getCanManageAnyChannelMembersInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeam, getTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import AddUserToChannelModal from 'components/add_user_to_channel_modal';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import ToggleModalButton from 'components/toggle_modal_button';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
returnFocus: () => void;
|
||||
handleCloseModals: () => void;
|
||||
hide?: () => void;
|
||||
};
|
||||
|
||||
function getIsInCurrentTeam(state: GlobalState, userId: string) {
|
||||
const team = getCurrentTeam(state);
|
||||
const teamMember = getTeamMember(state, team.id, userId);
|
||||
return Boolean(teamMember) && teamMember?.delete_at === 0;
|
||||
}
|
||||
|
||||
const AddToChannel = ({
|
||||
handleCloseModals,
|
||||
returnFocus,
|
||||
user,
|
||||
hide,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const canManageAnyChannelMembersInCurrentTeam = useSelector((state: GlobalState) => getCanManageAnyChannelMembersInCurrentTeam(state));
|
||||
const isInCurrentTeam = useSelector((state: GlobalState) => getIsInCurrentTeam(state, user.id));
|
||||
|
||||
const handleAddToChannel = useCallback(() => {
|
||||
hide?.();
|
||||
handleCloseModals();
|
||||
}, [hide, handleCloseModals]);
|
||||
|
||||
if (!canManageAnyChannelMembersInCurrentTeam || !isInCurrentTeam) {
|
||||
return null;
|
||||
}
|
||||
const addToChannelMessage = formatMessage({
|
||||
id: 'user_profile.add_user_to_channel',
|
||||
defaultMessage: 'Add to a Channel',
|
||||
});
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={
|
||||
<Tooltip id='addToChannelTooltip'>
|
||||
{addToChannelMessage}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<ToggleModalButton
|
||||
id='addToChannelButton'
|
||||
className='btn icon-btn'
|
||||
ariaLabel={addToChannelMessage}
|
||||
modalId={ModalIdentifiers.ADD_USER_TO_CHANNEL}
|
||||
dialogType={AddUserToChannelModal}
|
||||
dialogProps={{user, onExited: returnFocus}}
|
||||
onClick={handleAddToChannel}
|
||||
>
|
||||
<AccountPlusOutlineIcon
|
||||
size={18}
|
||||
aria-label={formatMessage({
|
||||
id: 'user_profile.add_user_to_channel.icon',
|
||||
defaultMessage: 'Add User to Channel Icon',
|
||||
})}
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddToChannel;
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {checkUserInCall} from './call_button';
|
||||
|
||||
describe('checkUserInCall', () => {
|
||||
test('missing state', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {},
|
||||
} as any, 'userA')).toBe(false);
|
||||
});
|
||||
|
||||
test('call state missing', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {
|
||||
profiles: {
|
||||
channelID: null,
|
||||
},
|
||||
},
|
||||
} as any, 'userA')).toBe(false);
|
||||
});
|
||||
|
||||
test('user not in call', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {
|
||||
profiles: {
|
||||
channelID: {
|
||||
sessionB: {
|
||||
id: 'userB',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any, 'userA')).toBe(false);
|
||||
});
|
||||
|
||||
test('user in call', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {
|
||||
profiles: {
|
||||
channelID: {
|
||||
sessionB: {
|
||||
id: 'userB',
|
||||
},
|
||||
sessionA: {
|
||||
id: 'userA',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any, 'userA')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {PhoneInTalkIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getChannelByName} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCallsConfig, getProfilesInCalls} from 'mattermost-redux/selectors/entities/common';
|
||||
|
||||
import {isCallsEnabled as getIsCallsEnabled} from 'selectors/calls';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import ProfilePopoverCallButton from 'components/profile_popover_call_button';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {getDirectChannelName} from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
type Props = {
|
||||
userId: string;
|
||||
currentUserId: string;
|
||||
channelId?: string;
|
||||
fullname: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
type ChannelCallsState = {
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export function checkUserInCall(state: GlobalState, userId: string) {
|
||||
for (const profilesMap of Object.values(getProfilesInCalls(state))) {
|
||||
for (const profile of Object.values(profilesMap || {})) {
|
||||
if (profile?.id === userId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const CallButton = ({
|
||||
userId,
|
||||
currentUserId,
|
||||
channelId,
|
||||
fullname,
|
||||
username,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const isCallsEnabled = useSelector((state: GlobalState) => getIsCallsEnabled(state));
|
||||
const isUserInCall = useSelector((state: GlobalState) => (isCallsEnabled ? checkUserInCall(state, userId) : undefined));
|
||||
const isCurrentUserInCall = useSelector((state: GlobalState) => (isCallsEnabled ? checkUserInCall(state, currentUserId) : undefined));
|
||||
const callsConfig = useSelector((state: GlobalState) => (isCallsEnabled ? getCallsConfig(state) : undefined));
|
||||
const isCallsDefaultEnabledOnAllChannels = callsConfig?.DefaultEnabled;
|
||||
const isCallsCanBeDisabledOnSpecificChannels = callsConfig?.AllowEnableCalls;
|
||||
const dmChannel = useSelector((state: GlobalState) => getChannelByName(state, getDirectChannelName(currentUserId, userId)));
|
||||
|
||||
const [callsDMChannelState, setCallsDMChannelState] = useState<ChannelCallsState>();
|
||||
const [callsChannelState, setCallsChannelState] = useState<ChannelCallsState>();
|
||||
|
||||
const getCallsChannelState = useCallback((channelId: string): Promise<ChannelCallsState> => {
|
||||
let data: Promise<ChannelCallsState>;
|
||||
try {
|
||||
data = Client4.getCallsChannelState(channelId);
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCallsEnabled && dmChannel) {
|
||||
getCallsChannelState(dmChannel.id).then((data) => {
|
||||
setCallsDMChannelState(data);
|
||||
});
|
||||
}
|
||||
|
||||
if (isCallsEnabled && channelId) {
|
||||
getCallsChannelState(channelId).then((data) => {
|
||||
setCallsChannelState(data);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (
|
||||
!isCallsEnabled ||
|
||||
callsDMChannelState?.enabled === false ||
|
||||
(!isCallsDefaultEnabledOnAllChannels && !isCallsCanBeDisabledOnSpecificChannels && callsChannelState?.enabled === false)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const disabled = isUserInCall || isCurrentUserInCall;
|
||||
const startCallMessage = isUserInCall ? formatMessage({
|
||||
id: 'user_profile.call.userBusy',
|
||||
defaultMessage: '{user} is in another call',
|
||||
}, {user: fullname || username},
|
||||
) : formatMessage({
|
||||
id: 'webapp.mattermost.feature.start_call',
|
||||
defaultMessage: 'Start Call',
|
||||
});
|
||||
const iconButtonClassname = classNames('btn icon-btn', {'icon-btn-disabled': disabled});
|
||||
const callButton = (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={
|
||||
<Tooltip id='startCallTooltip'>
|
||||
{startCallMessage}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<button
|
||||
id='startCallButton'
|
||||
type='button'
|
||||
aria-disabled={disabled}
|
||||
className={iconButtonClassname}
|
||||
>
|
||||
<PhoneInTalkIcon
|
||||
size={18}
|
||||
aria-label={startCallMessage}
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
if (disabled) {
|
||||
return callButton;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProfilePopoverCallButton
|
||||
dmChannel={dmChannel}
|
||||
userId={userId}
|
||||
customButton={callButton}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallButton;
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {SendIcon} from '@mattermost/compass-icons/components';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import AddToChannel from './add_to_channel';
|
||||
import CallButton from './call_button';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
fullname: string;
|
||||
channelId?: string;
|
||||
currentUserId: string;
|
||||
haveOverrideProp: boolean;
|
||||
handleShowDirectChannel: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
returnFocus: () => void;
|
||||
handleCloseModals: () => void;
|
||||
hide?: () => void;
|
||||
};
|
||||
|
||||
const ProfilePopoverActions = ({
|
||||
currentUserId,
|
||||
haveOverrideProp,
|
||||
user,
|
||||
handleShowDirectChannel,
|
||||
handleCloseModals,
|
||||
returnFocus,
|
||||
hide,
|
||||
fullname,
|
||||
channelId,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
if (user.id === currentUserId || haveOverrideProp) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
className='popover__row first'
|
||||
>
|
||||
<button
|
||||
id='messageButton'
|
||||
type='button'
|
||||
className='btn'
|
||||
onClick={handleShowDirectChannel}
|
||||
>
|
||||
<SendIcon
|
||||
size={16}
|
||||
aria-label={formatMessage({
|
||||
id: 'user_profile.send.dm.icon',
|
||||
defaultMessage: 'Send Message Icon',
|
||||
})}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='user_profile.send.dm'
|
||||
defaultMessage='Message'
|
||||
/>
|
||||
</button>
|
||||
<div className='popover_row-controlContainer'>
|
||||
<AddToChannel
|
||||
handleCloseModals={handleCloseModals}
|
||||
returnFocus={returnFocus}
|
||||
user={user}
|
||||
hide={hide}
|
||||
/>
|
||||
<CallButton
|
||||
channelId={channelId}
|
||||
currentUserId={currentUserId}
|
||||
fullname={fullname}
|
||||
userId={user.id}
|
||||
username={user.username}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverActions;
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import StatusIcon from 'components/status_icon';
|
||||
import Avatar from 'components/widgets/users/avatar';
|
||||
|
||||
type Props = {
|
||||
username?: string;
|
||||
hideStatus?: boolean;
|
||||
status?: string;
|
||||
urlSrc: string;
|
||||
}
|
||||
const ProfilePopoverAvatar = ({
|
||||
username,
|
||||
hideStatus,
|
||||
status,
|
||||
urlSrc,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className='user-popover-image'>
|
||||
<Avatar
|
||||
id='userAvatar'
|
||||
size='xxl'
|
||||
username={username}
|
||||
url={urlSrc}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<StatusIcon
|
||||
className='status user-popover-status'
|
||||
status={hideStatus ? undefined : status}
|
||||
button={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverAvatar;
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {EmoticonHappyOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
import {CustomStatusDuration} from '@mattermost/types/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled as getIsCustomStatusEnabled, isCustomStatusExpired as getIsCustomStatusExpired} from 'selectors/views/custom_status';
|
||||
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import CustomStatusModal from 'components/custom_status/custom_status_modal';
|
||||
import CustomStatusText from 'components/custom_status/custom_status_text';
|
||||
import ExpiryTime from 'components/custom_status/expiry_time';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
currentUserId: string;
|
||||
hideStatus?: boolean;
|
||||
hide?: () => void;
|
||||
returnFocus?: () => void;
|
||||
currentUserTimezone?: string;
|
||||
haveOverrideProp: boolean;
|
||||
}
|
||||
|
||||
const emojiStyles: React.CSSProperties = {
|
||||
marginRight: 4,
|
||||
marginTop: 1,
|
||||
};
|
||||
const ProfilePopoverCustomStatus = ({
|
||||
currentUserId,
|
||||
hideStatus,
|
||||
user,
|
||||
hide,
|
||||
returnFocus,
|
||||
currentUserTimezone,
|
||||
haveOverrideProp,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const getCustomStatus = useMemo(makeGetCustomStatus, []);
|
||||
const customStatus = useSelector((state: GlobalState) => getCustomStatus(state, user.id));
|
||||
const isCustomStatusExpired = useSelector((state: GlobalState) => getIsCustomStatusExpired(state, customStatus));
|
||||
const isCustomStatusEnabled = useSelector((state: GlobalState) => getIsCustomStatusEnabled(state));
|
||||
|
||||
const showCustomStatusModal = useCallback(() => {
|
||||
hide?.();
|
||||
const customStatusInputModalData = {
|
||||
modalId: ModalIdentifiers.CUSTOM_STATUS,
|
||||
dialogType: CustomStatusModal,
|
||||
dialogProps: {onExited: returnFocus},
|
||||
};
|
||||
dispatch(openModal(customStatusInputModalData));
|
||||
}, [hide, returnFocus]);
|
||||
|
||||
const customStatusSet = (customStatus?.text || customStatus?.emoji) && !isCustomStatusExpired;
|
||||
const showExpiryTime = customStatusSet && customStatus.expires_at && customStatus.duration !== CustomStatusDuration.DONT_CLEAR;
|
||||
const canSetCustomStatus = user.id === currentUserId;
|
||||
const shouldShowCustomStatus = isCustomStatusEnabled && !haveOverrideProp && !hideStatus && (customStatusSet || canSetCustomStatus);
|
||||
|
||||
if (!shouldShowCustomStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let customStatusContent;
|
||||
if (customStatusSet) {
|
||||
customStatusContent = (
|
||||
<div className='d-flex align-items-center'>
|
||||
<CustomStatusEmoji
|
||||
userID={user.id}
|
||||
showTooltip={false}
|
||||
emojiStyle={emojiStyles}
|
||||
/>
|
||||
<CustomStatusText
|
||||
tooltipDirection='top'
|
||||
text={customStatus.text || ''}
|
||||
className='user-popover__email'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (canSetCustomStatus) {
|
||||
customStatusContent = (
|
||||
<button
|
||||
className='user-popover__set-custom-status-btn'
|
||||
onClick={showCustomStatusModal}
|
||||
>
|
||||
<EmoticonHappyOutlineIcon size={14}/>
|
||||
<FormattedMessage
|
||||
id='user_profile.custom_status.set_status'
|
||||
defaultMessage='Set a status'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id='user-popover-status'
|
||||
className='user-popover__time-status-container'
|
||||
>
|
||||
<span className='user-popover__subtitle'>
|
||||
<FormattedMessage
|
||||
id='user_profile.custom_status'
|
||||
defaultMessage='Status'
|
||||
/>
|
||||
{showExpiryTime && (
|
||||
<ExpiryTime
|
||||
time={customStatus.expires_at!} // has to be defined since showExpiryTime is true
|
||||
timezone={currentUserTimezone}
|
||||
className='ml-1'
|
||||
withinBrackets={true}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{customStatusContent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverCustomStatus;
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {AccountOutlineIcon, SendIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
import UserSettingsModal from 'components/user_settings/modal';
|
||||
|
||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
currentUserId: string;
|
||||
haveOverrideProp: boolean;
|
||||
hide?: () => void;
|
||||
returnFocus: () => void;
|
||||
handleCloseModals: () => void;
|
||||
handleShowDirectChannel: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
const ProfilePopoverEdit = ({
|
||||
userId,
|
||||
currentUserId,
|
||||
haveOverrideProp,
|
||||
hide,
|
||||
returnFocus,
|
||||
handleCloseModals,
|
||||
handleShowDirectChannel,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleEditAccountSettings = useCallback(() => {
|
||||
hide?.();
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.USER_SETTINGS,
|
||||
dialogType: UserSettingsModal,
|
||||
dialogProps: {isContentProductSettings: false, onExited: returnFocus},
|
||||
}));
|
||||
handleCloseModals();
|
||||
}, [hide, returnFocus, handleCloseModals]);
|
||||
|
||||
if (userId !== currentUserId || haveOverrideProp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sendMessageTooltip = (
|
||||
<Tooltip id='sendMessageTooltip'>
|
||||
<FormattedMessage
|
||||
id='user_profile.send.dm.yourself'
|
||||
defaultMessage='Send yourself a message'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
className='popover__row first'
|
||||
>
|
||||
<button
|
||||
id='editProfileButton'
|
||||
type='button'
|
||||
className='btn'
|
||||
onClick={handleEditAccountSettings}
|
||||
>
|
||||
<AccountOutlineIcon
|
||||
size={16}
|
||||
aria-label={formatMessage({
|
||||
id: 'generic_icons.edit',
|
||||
defaultMessage: 'Edit Icon',
|
||||
})}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='user_profile.account.editProfile'
|
||||
defaultMessage='Edit Profile'
|
||||
/>
|
||||
</button>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={sendMessageTooltip}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='btn icon-btn'
|
||||
onClick={handleShowDirectChannel}
|
||||
>
|
||||
<SendIcon
|
||||
size={18}
|
||||
aria-label={formatMessage({
|
||||
id: 'user_profile.send.dm.icon',
|
||||
defaultMessage: 'Send Message Icon',
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverEdit;
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
haveOverrideProp: boolean;
|
||||
isBot: boolean;
|
||||
}
|
||||
const ProfilePopoverEmail = ({
|
||||
email,
|
||||
haveOverrideProp,
|
||||
isBot,
|
||||
}: Props) => {
|
||||
if (!email || isBot || haveOverrideProp) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
title={email}
|
||||
>
|
||||
<a
|
||||
href={'mailto:' + email}
|
||||
className='text-nowrap text-lowercase user-popover__email pb-1'
|
||||
>
|
||||
{email}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverEmail;
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {displayLastActiveLabel, getLastActiveTimestampUnits, getLastActivityForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import Timestamp from 'components/timestamp';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
}
|
||||
const ProfilePopoverLastActive = ({
|
||||
userId,
|
||||
}: Props) => {
|
||||
const lastActivityTimestamp = useSelector((state: GlobalState) => getLastActivityForUserId(state, userId));
|
||||
const enableLastActiveTime = useSelector<GlobalState>((state) => displayLastActiveLabel(state, userId));
|
||||
const timestampUnits = useSelector((state: GlobalState) => getLastActiveTimestampUnits(state, userId));
|
||||
|
||||
if (!enableLastActiveTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='user-popover-last-active'>
|
||||
<FormattedMessage
|
||||
id='channel_header.lastOnline'
|
||||
defaultMessage='Last online {timestamp}'
|
||||
values={{
|
||||
timestamp: (
|
||||
<Timestamp
|
||||
value={lastActivityTimestamp}
|
||||
units={timestampUnits}
|
||||
useTime={false}
|
||||
style={'short'}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverLastActive;
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
isBot: boolean;
|
||||
haveOverrideProp: boolean;
|
||||
botDescription: string;
|
||||
}
|
||||
const BotDescription = ({
|
||||
haveOverrideProp,
|
||||
isBot,
|
||||
botDescription,
|
||||
}: Props) => {
|
||||
if (!isBot || haveOverrideProp) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className='overflow--ellipsis text-nowrap pb-1'>
|
||||
{botDescription}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BotDescription;
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import SharedUserIndicator from 'components/shared_user_indicator';
|
||||
|
||||
type Props = {
|
||||
fullname: string;
|
||||
username: string;
|
||||
remoteId?: string;
|
||||
}
|
||||
const FullName = ({
|
||||
fullname,
|
||||
username,
|
||||
remoteId,
|
||||
}: Props) => {
|
||||
if (!fullname) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let sharedIcon;
|
||||
if (remoteId) {
|
||||
sharedIcon = (
|
||||
<SharedUserIndicator
|
||||
className='shared-user-icon'
|
||||
withTooltip={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-testid={`popover-fullname-${username}`}
|
||||
className='overflow--ellipsis pb-1'
|
||||
>
|
||||
<span className='user-profile-popover__heading'>{fullname}</span>
|
||||
{sharedIcon}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default FullName;
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import BotDescription from './bot_description';
|
||||
import FullName from './full_name';
|
||||
import Position from './position';
|
||||
import UserName from './user_name';
|
||||
|
||||
type Props = {
|
||||
haveOverrideProp: boolean;
|
||||
user: UserProfile;
|
||||
fullname: string;
|
||||
}
|
||||
const ProfilePopoverName = ({
|
||||
user,
|
||||
haveOverrideProp,
|
||||
fullname,
|
||||
}: Props) => {
|
||||
return (
|
||||
<>
|
||||
<FullName
|
||||
fullname={fullname}
|
||||
remoteId={user.remote_id}
|
||||
username={user.username}
|
||||
/>
|
||||
<BotDescription
|
||||
botDescription={user.bot_description}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
isBot={user.is_bot}
|
||||
/>
|
||||
<UserName
|
||||
hasFullName={Boolean(fullname)}
|
||||
username={user.username}
|
||||
/>
|
||||
<Position
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
position={user.position}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverName;
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
position: string;
|
||||
haveOverrideProp: boolean;
|
||||
}
|
||||
const Position = ({
|
||||
position,
|
||||
haveOverrideProp,
|
||||
}: Props) => {
|
||||
if (!position || haveOverrideProp) {
|
||||
return null;
|
||||
}
|
||||
const positionToRender = (position).substring(
|
||||
0,
|
||||
Constants.MAX_POSITION_LENGTH,
|
||||
);
|
||||
return (
|
||||
<div className='text-center'>
|
||||
{positionToRender}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Position;
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
hasFullName: boolean;
|
||||
username: string;
|
||||
}
|
||||
|
||||
const UserName = ({
|
||||
hasFullName,
|
||||
username,
|
||||
}: Props) => {
|
||||
const userNameClass = classNames('overflow--ellipsis pb-1', {'user-profile-popover__heading': hasFullName});
|
||||
return (
|
||||
<div
|
||||
id='userPopoverUsername'
|
||||
className={userNameClass}
|
||||
>
|
||||
{`@${username}`}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserName;
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
type Props = {
|
||||
username: string;
|
||||
haveOverrideProp: boolean;
|
||||
}
|
||||
const ProfilePopoverOverrideDisclaimer = ({
|
||||
username,
|
||||
haveOverrideProp,
|
||||
}: Props) => {
|
||||
if (!haveOverrideProp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
className='popover__row first'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='user_profile.account.post_was_created'
|
||||
defaultMessage='This post was created by an integration from @{username}'
|
||||
values={{
|
||||
username,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverOverrideDisclaimer;
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {CloseIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import {getChannelMember} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeam, getTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {isGuest, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {getRhsState} from 'selectors/rhs';
|
||||
|
||||
import BotTag from 'components/widgets/tag/bot_tag';
|
||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||
import Tag from 'components/widgets/tag/tag';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
type Props = {
|
||||
isBot: boolean;
|
||||
roles: string;
|
||||
username: string;
|
||||
returnFocus: () => void;
|
||||
hide?: () => void;
|
||||
userId: string;
|
||||
channelId?: string;
|
||||
closeButtonRef: React.RefObject<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
function getIsTeamAdmin(state: GlobalState, userId: string) {
|
||||
const team = getCurrentTeam(state);
|
||||
const teamMember = getTeamMember(state, team.id, userId);
|
||||
|
||||
return Boolean(teamMember && teamMember.scheme_admin);
|
||||
}
|
||||
|
||||
function getIsChannelAdmin(state: GlobalState, userId: string, channelId?: string) {
|
||||
if (!channelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const channelMember = getChannelMember(state, channelId, userId);
|
||||
|
||||
if (getRhsState(state) !== 'search' && channelMember != null && channelMember.scheme_admin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const ProfilePopoverTitle = ({
|
||||
isBot,
|
||||
roles,
|
||||
username,
|
||||
returnFocus,
|
||||
hide,
|
||||
userId,
|
||||
channelId,
|
||||
closeButtonRef,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const isTeamAdmin = useSelector((state: GlobalState) => getIsTeamAdmin(state, userId));
|
||||
const isChannelAdmin = useSelector((state: GlobalState) => getIsChannelAdmin(state, userId, channelId));
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
hide?.();
|
||||
returnFocus();
|
||||
}, [hide, returnFocus]);
|
||||
|
||||
let roleTitle;
|
||||
if (isBot) {
|
||||
roleTitle = (
|
||||
<BotTag
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
/>
|
||||
);
|
||||
} else if (isGuest(roles)) {
|
||||
roleTitle = (
|
||||
<GuestTag
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
/>
|
||||
);
|
||||
} else if (isSystemAdmin(roles)) {
|
||||
roleTitle = (
|
||||
<Tag
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
text={formatMessage({
|
||||
id: 'admin.permissions.roles.system_admin.name',
|
||||
defaultMessage: 'System Admin',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
} else if (isTeamAdmin) {
|
||||
roleTitle = (
|
||||
<Tag
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
text={formatMessage({
|
||||
id: 'admin.permissions.roles.team_admin.name',
|
||||
defaultMessage: 'Team Admin',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
} else if (isChannelAdmin) {
|
||||
roleTitle = (
|
||||
<Tag
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
text={formatMessage({
|
||||
id: 'admin.permissions.roles.channel_admin.name',
|
||||
defaultMessage: 'Channel Admin',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const titleClassName = classNames('popover-title', {'popover-title_height': !roleTitle});
|
||||
|
||||
return (
|
||||
<div className={titleClassName}>
|
||||
<span data-testid={`profilePopoverTitle_${username}`}>
|
||||
{roleTitle}
|
||||
<button
|
||||
ref={closeButtonRef}
|
||||
className='user-popover__close'
|
||||
onClick={handleClose}
|
||||
>
|
||||
<CloseIcon
|
||||
size={18}
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverTitle;
|
||||
@@ -14,6 +14,7 @@ import Timestamp from 'components/timestamp';
|
||||
type ProfileTimezoneProps = {
|
||||
profileUserTimezone?: UserTimezone;
|
||||
currentUserTimezone: string | undefined;
|
||||
haveOverrideProp: boolean;
|
||||
}
|
||||
|
||||
const returnTimeDiff = (
|
||||
@@ -51,12 +52,15 @@ const returnTimeDiff = (
|
||||
);
|
||||
};
|
||||
|
||||
export const ProfileTimezone = (
|
||||
{
|
||||
currentUserTimezone,
|
||||
profileUserTimezone,
|
||||
}: ProfileTimezoneProps,
|
||||
) => {
|
||||
const ProfileTimezone = ({
|
||||
currentUserTimezone,
|
||||
profileUserTimezone,
|
||||
haveOverrideProp,
|
||||
}: ProfileTimezoneProps) => {
|
||||
if (haveOverrideProp || !profileUserTimezone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileTimezone = getUserCurrentTimezone(profileUserTimezone) || 'UTC';
|
||||
const profileTimezoneShort = profileTimezone ? DateTime.now().setZone(profileTimezone).offsetNameShort : undefined;
|
||||
|
||||
@@ -98,3 +102,5 @@ export const ProfileTimezone = (
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileTimezone;
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
@@ -15,7 +15,14 @@ type Props = {
|
||||
};
|
||||
|
||||
const SharedUserIndicator: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
const sharedIcon = (<i className={`${props.className || ''} icon-circle-multiple-outline`}/>);
|
||||
const intl = useIntl();
|
||||
|
||||
const sharedIcon = (
|
||||
<i
|
||||
className={`${props.className || ''} icon-circle-multiple-outline`}
|
||||
aria-label={intl.formatMessage({id: 'shared_user_indicator.aria_label', defaultMessage: 'shared user indicator'})}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!props.withTooltip) {
|
||||
return sharedIcon;
|
||||
|
||||
@@ -86,7 +86,7 @@ exports[`components/threading/channel_threads/thread_footer should report total
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/5/image?_=0"
|
||||
@@ -312,7 +312,7 @@ exports[`components/threading/channel_threads/thread_footer should report total
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/4/image?_=0"
|
||||
@@ -538,7 +538,7 @@ exports[`components/threading/channel_threads/thread_footer should report total
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/3/image?_=0"
|
||||
@@ -1241,7 +1241,7 @@ exports[`components/threading/channel_threads/thread_footer should show unread i
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/5/image?_=0"
|
||||
@@ -1467,7 +1467,7 @@ exports[`components/threading/channel_threads/thread_footer should show unread i
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/4/image?_=0"
|
||||
@@ -1693,7 +1693,7 @@ exports[`components/threading/channel_threads/thread_footer should show unread i
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/3/image?_=0"
|
||||
|
||||
@@ -130,7 +130,6 @@ const UserListRow = ({user, status, extraInfo = [], actions = [], actionProps, a
|
||||
>
|
||||
<UserProfile
|
||||
userId={user.id}
|
||||
hasMention={true}
|
||||
displayUsername={true}
|
||||
/>
|
||||
{
|
||||
|
||||
@@ -5,12 +5,10 @@ exports[`components/UserProfile should match snapshot 1`] = `
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
hideStatus={false}
|
||||
isBusy={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
userId="user_id"
|
||||
@@ -39,12 +37,10 @@ exports[`components/UserProfile should match snapshot, when displayUsername is e
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
hideStatus={false}
|
||||
isBusy={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
userId="user_id"
|
||||
@@ -81,12 +77,10 @@ exports[`components/UserProfile should match snapshot, when user is shared 1`] =
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
hideStatus={false}
|
||||
isBusy={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
userId="user_id"
|
||||
@@ -119,12 +113,10 @@ exports[`components/UserProfile should match snapshot, with colorization 1`] = `
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hasMention={false}
|
||||
hide={[Function]}
|
||||
hideStatus={false}
|
||||
isBusy={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
userId="user_id"
|
||||
|
||||
@@ -23,14 +23,12 @@ export type Props = {
|
||||
user?: UserProfileType;
|
||||
userId: string;
|
||||
displayName?: string;
|
||||
isBusy?: boolean;
|
||||
isShared?: boolean;
|
||||
overwriteName?: React.ReactNode;
|
||||
overwriteName?: string;
|
||||
overwriteIcon?: string;
|
||||
disablePopover?: boolean;
|
||||
displayUsername?: boolean;
|
||||
colorize?: boolean;
|
||||
hasMention?: boolean;
|
||||
hideStatus?: boolean;
|
||||
isMobileView: boolean;
|
||||
isRHS?: boolean;
|
||||
@@ -44,7 +42,6 @@ export default class UserProfile extends PureComponent<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
disablePopover: false,
|
||||
displayUsername: false,
|
||||
hasMention: false,
|
||||
hideStatus: false,
|
||||
isRHS: false,
|
||||
overwriteName: '',
|
||||
@@ -66,11 +63,9 @@ export default class UserProfile extends PureComponent<Props> {
|
||||
disablePopover,
|
||||
displayName,
|
||||
displayUsername,
|
||||
isBusy,
|
||||
isMobileView,
|
||||
isRHS,
|
||||
isShared,
|
||||
hasMention,
|
||||
hideStatus,
|
||||
overwriteName,
|
||||
overwriteIcon,
|
||||
@@ -142,10 +137,8 @@ export default class UserProfile extends PureComponent<Props> {
|
||||
userId={userId}
|
||||
channelId={channelId}
|
||||
src={profileImg}
|
||||
isBusy={isBusy}
|
||||
hide={this.hideProfilePopover}
|
||||
hideStatus={hideStatus}
|
||||
hasMention={hasMention}
|
||||
overwriteName={overwriteName}
|
||||
overwriteIcon={overwriteIcon}
|
||||
/>
|
||||
|
||||
@@ -40,7 +40,7 @@ exports[`components/widgets/users/Avatars should fetch missing users 1`] = `
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/1/image?_=0"
|
||||
@@ -266,7 +266,7 @@ exports[`components/widgets/users/Avatars should fetch missing users 1`] = `
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/6/image?_=0"
|
||||
@@ -492,7 +492,7 @@ exports[`components/widgets/users/Avatars should fetch missing users 1`] = `
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/7/image?_=0"
|
||||
@@ -862,7 +862,7 @@ exports[`components/widgets/users/Avatars should properly count overflow 1`] = `
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/1/image?_=0"
|
||||
@@ -1088,7 +1088,7 @@ exports[`components/widgets/users/Avatars should properly count overflow 1`] = `
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/2/image?_=0"
|
||||
@@ -1314,7 +1314,7 @@ exports[`components/widgets/users/Avatars should properly count overflow 1`] = `
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/3/image?_=0"
|
||||
@@ -1682,7 +1682,7 @@ exports[`components/widgets/users/Avatars should support userIds 1`] = `
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/1/image?_=0"
|
||||
@@ -1908,7 +1908,7 @@ exports[`components/widgets/users/Avatars should support userIds 1`] = `
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/2/image?_=0"
|
||||
@@ -2134,7 +2134,7 @@ exports[`components/widgets/users/Avatars should support userIds 1`] = `
|
||||
defaultOverlayShown={false}
|
||||
disabled={false}
|
||||
overlay={
|
||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/3/image?_=0"
|
||||
|
||||
@@ -4798,6 +4798,7 @@
|
||||
"setting_picture.select": "Select",
|
||||
"setting_picture.uploading": "Uploading...",
|
||||
"shared_channel_indicator.tooltip": "Shared with trusted organizations",
|
||||
"shared_user_indicator.aria_label": "shared user indicator",
|
||||
"shared_user_indicator.tooltip": "From a trusted organization",
|
||||
"shortcuts.browser.channel_next": "Forward in history:\tAlt|Right",
|
||||
"shortcuts.browser.channel_next.mac": "Forward in history:\t⌘|]",
|
||||
|
||||
@@ -20,7 +20,7 @@ import {isDateWithinDaysRange, TimeInformation} from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export function makeGetCustomStatus(): (state: GlobalState, userID?: string) => UserCustomStatus {
|
||||
export function makeGetCustomStatus(): (state: GlobalState, userID?: string) => UserCustomStatus | undefined {
|
||||
return createSelector(
|
||||
'makeGetCustomStatus',
|
||||
(state: GlobalState, userID?: string) => (userID ? getUser(state, userID) : getCurrentUser(state)),
|
||||
|
||||
@@ -22,9 +22,10 @@ window.Observable = Observable;
|
||||
|
||||
const localForage = extendPrototype(baseLocalForage);
|
||||
|
||||
export default function configureStore(preloadedState) {
|
||||
export default function configureStore(preloadedState, additionalReducers) {
|
||||
const reducers = additionalReducers ? {...appReducers, ...additionalReducers} : appReducers;
|
||||
const store = configureServiceStore({
|
||||
appReducers,
|
||||
appReducers: reducers,
|
||||
getAppReducers,
|
||||
preloadedState,
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import React from 'react';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
import {Provider} from 'react-redux';
|
||||
import {Router} from 'react-router-dom';
|
||||
import type {Reducer} from 'redux';
|
||||
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
@@ -26,6 +27,7 @@ export type FullContextOptions = {
|
||||
intlMessages?: Record<string, string>;
|
||||
locale?: string;
|
||||
useMockedStore?: boolean;
|
||||
pluginReducers?: string[];
|
||||
}
|
||||
|
||||
export const renderWithContext = (
|
||||
@@ -39,7 +41,7 @@ export const renderWithContext = (
|
||||
useMockedStore: partialOptions?.useMockedStore ?? false,
|
||||
};
|
||||
|
||||
const testStore = configureOrMockStore(initialState, options.useMockedStore);
|
||||
const testStore = configureOrMockStore(initialState, options.useMockedStore, partialOptions?.pluginReducers);
|
||||
|
||||
// Store these in an object so that they can be maintained through rerenders
|
||||
const renderState = {
|
||||
@@ -82,7 +84,7 @@ export const renderWithContext = (
|
||||
* Rerenders the component after replacing the entire store state with the provided one.
|
||||
*/
|
||||
replaceStoreState: (newInitialState: DeepPartial<GlobalState>) => {
|
||||
renderState.store = configureOrMockStore(newInitialState, renderState.options.useMockedStore);
|
||||
renderState.store = configureOrMockStore(newInitialState, renderState.options.useMockedStore, partialOptions?.pluginReducers);
|
||||
|
||||
results.rerender(renderState.component);
|
||||
},
|
||||
@@ -92,15 +94,25 @@ export const renderWithContext = (
|
||||
*/
|
||||
updateStoreState: (stateDiff: DeepPartial<GlobalState>) => {
|
||||
const newInitialState = mergeObjects(renderState.store.getState(), stateDiff);
|
||||
renderState.store = configureOrMockStore(newInitialState, renderState.options.useMockedStore);
|
||||
renderState.store = configureOrMockStore(newInitialState, renderState.options.useMockedStore, partialOptions?.pluginReducers);
|
||||
|
||||
results.rerender(renderState.component);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function configureOrMockStore<T>(initialState: DeepPartial<T>, useMockedStore: boolean) {
|
||||
let testStore = configureStore(initialState);
|
||||
function configureOrMockStore<T>(initialState: DeepPartial<T>, useMockedStore: boolean, extraReducersKeys?: string[]) {
|
||||
let testReducers;
|
||||
if (extraReducersKeys) {
|
||||
const newReducers: Record<string, Reducer> = {};
|
||||
extraReducersKeys.forEach((v) => {
|
||||
newReducers[v] = (state = null) => state;
|
||||
});
|
||||
|
||||
testReducers = newReducers;
|
||||
}
|
||||
|
||||
let testStore = configureStore(initialState, testReducers);
|
||||
if (useMockedStore) {
|
||||
testStore = mockStore(testStore.getState());
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ exports[`messageHtmlToComponent At mention 1`] = `
|
||||
<Memo(Connect(Component))
|
||||
disableGroupHighlight={false}
|
||||
disableHighlight={false}
|
||||
hasMention={true}
|
||||
mentionName="joram"
|
||||
>
|
||||
@joram
|
||||
@@ -29,7 +28,6 @@ exports[`messageHtmlToComponent At mention 2`] = `
|
||||
<Memo(Connect(Component))
|
||||
disableGroupHighlight={false}
|
||||
disableHighlight={true}
|
||||
hasMention={true}
|
||||
mentionName="joram"
|
||||
>
|
||||
@joram
|
||||
@@ -46,7 +44,6 @@ exports[`messageHtmlToComponent At mention with group highlight disabled 1`] = `
|
||||
<Memo(Connect(Component))
|
||||
disableGroupHighlight={false}
|
||||
disableHighlight={false}
|
||||
hasMention={true}
|
||||
mentionName="developers"
|
||||
>
|
||||
@developers
|
||||
@@ -63,7 +60,6 @@ exports[`messageHtmlToComponent At mention with group highlight disabled 2`] = `
|
||||
<Memo(Connect(Component))
|
||||
disableGroupHighlight={true}
|
||||
disableHighlight={false}
|
||||
hasMention={true}
|
||||
mentionName="developers"
|
||||
>
|
||||
@developers
|
||||
|
||||
@@ -129,7 +129,6 @@ export function messageHtmlToComponent(html: string, options: Options = {}) {
|
||||
const callAtMention = (
|
||||
<AtMention
|
||||
mentionName={mentionName}
|
||||
hasMention={true}
|
||||
disableHighlight={!mentionHighlight}
|
||||
disableGroupHighlight={disableGroupHighlight}
|
||||
channelId={options.channelId}
|
||||
|
||||
Ссылка в новой задаче
Block a user