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}
|
rootClose={true}
|
||||||
show={false}
|
show={false}
|
||||||
>
|
>
|
||||||
<Connect(injectIntl(ProfilePopover))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/abc1/image"
|
src="/api/v4/users/abc1/image"
|
||||||
@@ -189,7 +189,7 @@ exports[`components/AtMention should match snapshot when mentioning user 1`] = `
|
|||||||
rootClose={true}
|
rootClose={true}
|
||||||
show={false}
|
show={false}
|
||||||
>
|
>
|
||||||
<Connect(injectIntl(ProfilePopover))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/abc2/image"
|
src="/api/v4/users/abc2/image"
|
||||||
@@ -229,7 +229,7 @@ exports[`components/AtMention should match snapshot when mentioning user contain
|
|||||||
rootClose={true}
|
rootClose={true}
|
||||||
show={false}
|
show={false}
|
||||||
>
|
>
|
||||||
<Connect(injectIntl(ProfilePopover))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/abc3/image"
|
src="/api/v4/users/abc3/image"
|
||||||
@@ -270,7 +270,7 @@ exports[`components/AtMention should match snapshot when mentioning user contain
|
|||||||
rootClose={true}
|
rootClose={true}
|
||||||
show={false}
|
show={false}
|
||||||
>
|
>
|
||||||
<Connect(injectIntl(ProfilePopover))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/abc3/image"
|
src="/api/v4/users/abc3/image"
|
||||||
@@ -310,7 +310,7 @@ exports[`components/AtMention should match snapshot when mentioning user followe
|
|||||||
rootClose={true}
|
rootClose={true}
|
||||||
show={false}
|
show={false}
|
||||||
>
|
>
|
||||||
<Connect(injectIntl(ProfilePopover))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/abc2/image"
|
src="/api/v4/users/abc2/image"
|
||||||
@@ -351,7 +351,7 @@ exports[`components/AtMention should match snapshot when mentioning user with di
|
|||||||
rootClose={true}
|
rootClose={true}
|
||||||
show={false}
|
show={false}
|
||||||
>
|
>
|
||||||
<Connect(injectIntl(ProfilePopover))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/abc2/image"
|
src="/api/v4/users/abc2/image"
|
||||||
@@ -391,7 +391,7 @@ exports[`components/AtMention should match snapshot when mentioning user with mi
|
|||||||
rootClose={true}
|
rootClose={true}
|
||||||
show={false}
|
show={false}
|
||||||
>
|
>
|
||||||
<Connect(injectIntl(ProfilePopover))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/abc2/image"
|
src="/api/v4/users/abc2/image"
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ type Props = {
|
|||||||
groupsByName: Record<string, Group>;
|
groupsByName: Record<string, Group>;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
hasMention?: boolean;
|
|
||||||
disableHighlight?: boolean;
|
disableHighlight?: boolean;
|
||||||
disableGroupHighlight?: boolean;
|
disableGroupHighlight?: boolean;
|
||||||
}
|
}
|
||||||
@@ -122,7 +121,6 @@ export const AtMention = (props: Props) => {
|
|||||||
className='user-profile-popover'
|
className='user-profile-popover'
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
|
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
|
||||||
hasMention={props.hasMention}
|
|
||||||
hide={hideOverlay}
|
hide={hideOverlay}
|
||||||
channelId={props.channelId}
|
channelId={props.channelId}
|
||||||
/>
|
/>
|
||||||
@@ -187,7 +185,6 @@ export const AtMention = (props: Props) => {
|
|||||||
userId={groupUser.id}
|
userId={groupUser.id}
|
||||||
src={Client4.getProfilePictureUrl(groupUser.id, groupUser.last_picture_update)}
|
src={Client4.getProfilePictureUrl(groupUser.id, groupUser.last_picture_update)}
|
||||||
channelId={props.channelId}
|
channelId={props.channelId}
|
||||||
hasMention={props.hasMention}
|
|
||||||
hide={hideGroupUserOverlay}
|
hide={hideGroupUserOverlay}
|
||||||
returnFocus={returnFocus}
|
returnFocus={returnFocus}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ function CustomStatusEmoji({
|
|||||||
|
|
||||||
const customStatusExpired = useSelector((state: GlobalState) => isCustomStatusExpired(state, customStatus));
|
const customStatusExpired = useSelector((state: GlobalState) => isCustomStatusExpired(state, customStatus));
|
||||||
const customStatusEnabled = useSelector(isCustomStatusEnabled);
|
const customStatusEnabled = useSelector(isCustomStatusEnabled);
|
||||||
const isCustomStatusSet = Boolean(customStatusEnabled && customStatus?.emoji && !customStatusExpired);
|
if (!customStatusEnabled || !customStatus?.emoji || customStatusExpired) {
|
||||||
if (!isCustomStatusSet) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,10 +44,9 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
|
|||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
@@ -67,7 +66,6 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
|
|||||||
<OverlayWrapper
|
<OverlayWrapper
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
intl={
|
intl={
|
||||||
Object {
|
Object {
|
||||||
@@ -317,10 +315,9 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
|
|||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
@@ -340,7 +337,6 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
|
|||||||
<OverlayWrapper
|
<OverlayWrapper
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
intl={
|
intl={
|
||||||
Object {
|
Object {
|
||||||
@@ -596,10 +592,9 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
|
|||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
@@ -619,7 +614,6 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
|
|||||||
<OverlayWrapper
|
<OverlayWrapper
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
intl={
|
intl={
|
||||||
Object {
|
Object {
|
||||||
@@ -937,10 +931,9 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
|
|||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
@@ -960,7 +953,6 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
|
|||||||
<OverlayWrapper
|
<OverlayWrapper
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
intl={
|
intl={
|
||||||
Object {
|
Object {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type {ReactNode} 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';
|
import type {Post} from '@mattermost/types/posts';
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PostUserProfile = (props: Props): JSX.Element | null => {
|
const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||||
|
const intl = useIntl();
|
||||||
const {post, compactDisplay, isMobileView, isConsecutivePost, enablePostUsernameOverride, isBot, isSystemMessage, colorizeUsernames} = props;
|
const {post, compactDisplay, isMobileView, isConsecutivePost, enablePostUsernameOverride, isBot, isSystemMessage, colorizeUsernames} = props;
|
||||||
const isFromAutoResponder = fromAutoResponder(post);
|
const isFromAutoResponder = fromAutoResponder(post);
|
||||||
const colorize = compactDisplay && colorizeUsernames;
|
const colorize = compactDisplay && colorizeUsernames;
|
||||||
@@ -55,7 +56,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
|||||||
userId={post.user_id}
|
userId={post.user_id}
|
||||||
channelId={post.channel_id}
|
channelId={post.channel_id}
|
||||||
isRHS={isRHS}
|
isRHS={isRHS}
|
||||||
hasMention={true}
|
|
||||||
colorize={colorize}
|
colorize={colorize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -67,7 +67,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
|||||||
userId={post.user_id}
|
userId={post.user_id}
|
||||||
channelId={post.channel_id}
|
channelId={post.channel_id}
|
||||||
isRHS={isRHS}
|
isRHS={isRHS}
|
||||||
hasMention={true}
|
|
||||||
colorize={colorize}
|
colorize={colorize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -77,7 +76,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
|||||||
userId={post.user_id}
|
userId={post.user_id}
|
||||||
channelId={post.channel_id}
|
channelId={post.channel_id}
|
||||||
isRHS={isRHS}
|
isRHS={isRHS}
|
||||||
hasMention={true}
|
|
||||||
colorize={colorize}
|
colorize={colorize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -108,7 +106,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
|||||||
channelId={post.channel_id}
|
channelId={post.channel_id}
|
||||||
hideStatus={true}
|
hideStatus={true}
|
||||||
isRHS={isRHS}
|
isRHS={isRHS}
|
||||||
hasMention={true}
|
|
||||||
colorize={colorize}
|
colorize={colorize}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -135,12 +132,10 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
|||||||
} else if (isSystemMessage) {
|
} else if (isSystemMessage) {
|
||||||
userProfile = (
|
userProfile = (
|
||||||
<UserProfile
|
<UserProfile
|
||||||
overwriteName={
|
overwriteName={intl.formatMessage({
|
||||||
<FormattedMessage
|
id: 'post_info.system',
|
||||||
id='post_info.system'
|
defaultMessage: 'System',
|
||||||
defaultMessage='System'
|
})}
|
||||||
/>
|
|
||||||
}
|
|
||||||
userId={post.user_id}
|
userId={post.user_id}
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
channelId={post.channel_id}
|
channelId={post.channel_id}
|
||||||
|
|||||||
@@ -230,7 +230,6 @@ exports[`components/post_edit_history/edited_post_item should match snapshot whe
|
|||||||
>
|
>
|
||||||
<Connect(UserProfile)
|
<Connect(UserProfile)
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
hasMention={true}
|
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ const EditedPostItem = ({post, isCurrent = false, postCurrentVersion, theme, act
|
|||||||
<div className={'edit-post-history__header__username'}>
|
<div className={'edit-post-history__header__username'}>
|
||||||
<UserProfileComponent
|
<UserProfileComponent
|
||||||
userId={post.user_id}
|
userId={post.user_id}
|
||||||
hasMention={true}
|
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
overwriteName={overwriteName}
|
overwriteName={overwriteName}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ describe('components/PostProfilePicture', () => {
|
|||||||
enablePostIconOverride: true,
|
enablePostIconOverride: true,
|
||||||
compactDisplay: true,
|
compactDisplay: true,
|
||||||
hasImageProxy: true,
|
hasImageProxy: true,
|
||||||
isBusy: true,
|
|
||||||
post,
|
post,
|
||||||
user,
|
user,
|
||||||
isBot: Boolean(user.is_bot),
|
isBot: Boolean(user.is_bot),
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ type Props = {
|
|||||||
compactDisplay?: boolean;
|
compactDisplay?: boolean;
|
||||||
enablePostIconOverride: boolean;
|
enablePostIconOverride: boolean;
|
||||||
hasImageProxy: boolean;
|
hasImageProxy: boolean;
|
||||||
isBusy?: boolean;
|
|
||||||
post: Post;
|
post: Post;
|
||||||
status?: string;
|
status?: string;
|
||||||
user: UserProfile;
|
user: UserProfile;
|
||||||
@@ -81,7 +80,6 @@ export default class PostProfilePicture extends React.PureComponent<Props> {
|
|||||||
const {
|
const {
|
||||||
availabilityStatusOnPosts,
|
availabilityStatusOnPosts,
|
||||||
compactDisplay,
|
compactDisplay,
|
||||||
isBusy,
|
|
||||||
post,
|
post,
|
||||||
user,
|
user,
|
||||||
isBot,
|
isBot,
|
||||||
@@ -107,7 +105,6 @@ export default class PostProfilePicture extends React.PureComponent<Props> {
|
|||||||
return (
|
return (
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
hasMention={hasMention}
|
hasMention={hasMention}
|
||||||
isBusy={isBusy}
|
|
||||||
size='md'
|
size='md'
|
||||||
src={src}
|
src={src}
|
||||||
profileSrc={profileSrc}
|
profileSrc={profileSrc}
|
||||||
|
|||||||
@@ -225,7 +225,6 @@ function createDMIntroMessage(channel: Channel, centeredIntro: string, teammate?
|
|||||||
<UserProfile
|
<UserProfile
|
||||||
userId={teammate?.id}
|
userId={teammate?.id}
|
||||||
disablePopover={false}
|
disablePopover={false}
|
||||||
hasMention={true}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className='channel-intro-text'>
|
<p className='channel-intro-text'>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ exports[`components/post_view/CommentedOn should match snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
disablePopover={false}
|
||||||
hasMention={true}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -49,7 +48,6 @@ exports[`components/post_view/CommentedOn should match snapshot 2`] = `
|
|||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
disablePopover={false}
|
||||||
hasMention={true}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -82,7 +80,6 @@ exports[`components/post_view/CommentedOn should match snapshot 3`] = `
|
|||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
disablePopover={false}
|
||||||
hasMention={true}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -117,7 +114,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
|||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
disablePopover={false}
|
||||||
hasMention={true}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -150,7 +146,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
|||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
disablePopover={false}
|
||||||
hasMention={true}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -183,7 +178,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
|||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
disablePopover={false}
|
||||||
hasMention={true}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -216,7 +210,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
|||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
disablePopover={false}
|
||||||
hasMention={true}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ function CommentedOn({post, parentPostUser, onCommentClick}: Props) {
|
|||||||
const parentUserProfile = (
|
const parentUserProfile = (
|
||||||
<UserProfile
|
<UserProfile
|
||||||
userId={parentPostUserId}
|
userId={parentPostUserId}
|
||||||
hasMention={true}
|
|
||||||
disablePopover={false}
|
disablePopover={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ exports[`PostMessagePreview direct and group messages should render preview for
|
|||||||
>
|
>
|
||||||
<Connect(UserProfile)
|
<Connect(UserProfile)
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
hasMention={true}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
userId="user_1"
|
userId="user_1"
|
||||||
/>
|
/>
|
||||||
@@ -121,7 +120,6 @@ exports[`PostMessagePreview direct and group messages should render preview for
|
|||||||
>
|
>
|
||||||
<Connect(UserProfile)
|
<Connect(UserProfile)
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
hasMention={true}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
userId="user_1"
|
userId="user_1"
|
||||||
/>
|
/>
|
||||||
@@ -208,7 +206,6 @@ exports[`PostMessagePreview nested previews should render file preview 1`] = `
|
|||||||
>
|
>
|
||||||
<Connect(UserProfile)
|
<Connect(UserProfile)
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
hasMention={true}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
userId="user_1"
|
userId="user_1"
|
||||||
/>
|
/>
|
||||||
@@ -315,7 +312,6 @@ exports[`PostMessagePreview nested previews should render opengraph preview 1`]
|
|||||||
>
|
>
|
||||||
<Connect(UserProfile)
|
<Connect(UserProfile)
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
hasMention={true}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
userId="user_1"
|
userId="user_1"
|
||||||
/>
|
/>
|
||||||
@@ -430,7 +426,6 @@ exports[`PostMessagePreview should not render bot icon 1`] = `
|
|||||||
>
|
>
|
||||||
<Connect(UserProfile)
|
<Connect(UserProfile)
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
hasMention={true}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
userId="user_1"
|
userId="user_1"
|
||||||
/>
|
/>
|
||||||
@@ -522,7 +517,6 @@ exports[`PostMessagePreview should render bot icon 1`] = `
|
|||||||
>
|
>
|
||||||
<Connect(UserProfile)
|
<Connect(UserProfile)
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
hasMention={true}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
userId="user_1"
|
userId="user_1"
|
||||||
/>
|
/>
|
||||||
@@ -614,7 +608,6 @@ exports[`PostMessagePreview should render correctly 1`] = `
|
|||||||
>
|
>
|
||||||
<Connect(UserProfile)
|
<Connect(UserProfile)
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
hasMention={true}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
userId="user_1"
|
userId="user_1"
|
||||||
/>
|
/>
|
||||||
@@ -703,7 +696,6 @@ exports[`PostMessagePreview show render without preview when preview posts becom
|
|||||||
>
|
>
|
||||||
<Connect(UserProfile)
|
<Connect(UserProfile)
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
hasMention={true}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
userId="user_1"
|
userId="user_1"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -174,7 +174,6 @@ const PostMessagePreview = (props: Props) => {
|
|||||||
<div className={classNames('col col__name', 'permalink--username')}>
|
<div className={classNames('col col__name', 'permalink--username')}>
|
||||||
<UserProfileComponent
|
<UserProfileComponent
|
||||||
userId={user?.id ?? ''}
|
userId={user?.id ?? ''}
|
||||||
hasMention={true}
|
|
||||||
disablePopover={true}
|
disablePopover={true}
|
||||||
overwriteName={previewPost.props?.override_username || ''}
|
overwriteName={previewPost.props?.override_username || ''}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -44,11 +44,9 @@ exports[`components/ProfilePicture should match snapshot, profile and src, defau
|
|||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
isBusy={true}
|
|
||||||
src="http://example.com/image.png"
|
src="http://example.com/image.png"
|
||||||
userId="uid"
|
userId="uid"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ describe('components/ProfilePicture', () => {
|
|||||||
const baseProps: Props = {
|
const baseProps: Props = {
|
||||||
src: 'http://example.com/image.png',
|
src: 'http://example.com/image.png',
|
||||||
status: 'away',
|
status: 'away',
|
||||||
isBusy: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot, no user specified, default props', () => {
|
test('should match snapshot, no user specified, default props', () => {
|
||||||
@@ -43,7 +42,6 @@ describe('components/ProfilePicture', () => {
|
|||||||
const props: Props = {
|
const props: Props = {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
size: 'xl',
|
size: 'xl',
|
||||||
hasMention: true,
|
|
||||||
};
|
};
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<ProfilePicture {...props}/>,
|
<ProfilePicture {...props}/>,
|
||||||
@@ -69,7 +67,6 @@ describe('components/ProfilePicture', () => {
|
|||||||
...baseProps,
|
...baseProps,
|
||||||
username: 'username',
|
username: 'username',
|
||||||
size: 'xs',
|
size: 'xs',
|
||||||
hasMention: true,
|
|
||||||
};
|
};
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<ProfilePicture {...props}/>,
|
<ProfilePicture {...props}/>,
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ interface MMOverlayTrigger extends BaseOverlayTrigger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hasMention?: boolean;
|
|
||||||
isBusy?: boolean;
|
|
||||||
isEmoji?: boolean;
|
isEmoji?: boolean;
|
||||||
profileSrc?: string;
|
profileSrc?: string;
|
||||||
size?: ComponentProps<typeof Avatar>['size'];
|
size?: ComponentProps<typeof Avatar>['size'];
|
||||||
@@ -80,10 +78,8 @@ export default class ProfilePicture extends React.PureComponent<Props> {
|
|||||||
className='user-profile-popover'
|
className='user-profile-popover'
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
src={profileSrc}
|
src={profileSrc}
|
||||||
isBusy={this.props.isBusy}
|
|
||||||
hide={this.hideProfilePopover}
|
hide={this.hideProfilePopover}
|
||||||
channelId={this.props.channelId}
|
channelId={this.props.channelId}
|
||||||
hasMention={this.props.hasMention}
|
|
||||||
overwriteIcon={this.props.overwriteIcon}
|
overwriteIcon={this.props.overwriteIcon}
|
||||||
overwriteName={this.props.overwriteName}
|
overwriteName={this.props.overwriteName}
|
||||||
fromWebhook={this.props.fromWebhook}
|
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 = {
|
type ProfileTimezoneProps = {
|
||||||
profileUserTimezone?: UserTimezone;
|
profileUserTimezone?: UserTimezone;
|
||||||
currentUserTimezone: string | undefined;
|
currentUserTimezone: string | undefined;
|
||||||
|
haveOverrideProp: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const returnTimeDiff = (
|
const returnTimeDiff = (
|
||||||
@@ -51,12 +52,15 @@ const returnTimeDiff = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProfileTimezone = (
|
const ProfileTimezone = ({
|
||||||
{
|
currentUserTimezone,
|
||||||
currentUserTimezone,
|
profileUserTimezone,
|
||||||
profileUserTimezone,
|
haveOverrideProp,
|
||||||
}: ProfileTimezoneProps,
|
}: ProfileTimezoneProps) => {
|
||||||
) => {
|
if (haveOverrideProp || !profileUserTimezone) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const profileTimezone = getUserCurrentTimezone(profileUserTimezone) || 'UTC';
|
const profileTimezone = getUserCurrentTimezone(profileUserTimezone) || 'UTC';
|
||||||
const profileTimezoneShort = profileTimezone ? DateTime.now().setZone(profileTimezone).offsetNameShort : undefined;
|
const profileTimezoneShort = profileTimezone ? DateTime.now().setZone(profileTimezone).offsetNameShort : undefined;
|
||||||
|
|
||||||
@@ -98,3 +102,5 @@ export const ProfileTimezone = (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default ProfileTimezone;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
|
||||||
import OverlayTrigger from 'components/overlay_trigger';
|
import OverlayTrigger from 'components/overlay_trigger';
|
||||||
import Tooltip from 'components/tooltip';
|
import Tooltip from 'components/tooltip';
|
||||||
@@ -15,7 +15,14 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SharedUserIndicator: React.FC<Props> = (props: Props): JSX.Element => {
|
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) {
|
if (!props.withTooltip) {
|
||||||
return sharedIcon;
|
return sharedIcon;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ exports[`components/threading/channel_threads/thread_footer should report total
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/5/image?_=0"
|
src="/api/v4/users/5/image?_=0"
|
||||||
@@ -312,7 +312,7 @@ exports[`components/threading/channel_threads/thread_footer should report total
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/4/image?_=0"
|
src="/api/v4/users/4/image?_=0"
|
||||||
@@ -538,7 +538,7 @@ exports[`components/threading/channel_threads/thread_footer should report total
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/3/image?_=0"
|
src="/api/v4/users/3/image?_=0"
|
||||||
@@ -1241,7 +1241,7 @@ exports[`components/threading/channel_threads/thread_footer should show unread i
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/5/image?_=0"
|
src="/api/v4/users/5/image?_=0"
|
||||||
@@ -1467,7 +1467,7 @@ exports[`components/threading/channel_threads/thread_footer should show unread i
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/4/image?_=0"
|
src="/api/v4/users/4/image?_=0"
|
||||||
@@ -1693,7 +1693,7 @@ exports[`components/threading/channel_threads/thread_footer should show unread i
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/3/image?_=0"
|
src="/api/v4/users/3/image?_=0"
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ const UserListRow = ({user, status, extraInfo = [], actions = [], actionProps, a
|
|||||||
>
|
>
|
||||||
<UserProfile
|
<UserProfile
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
hasMention={true}
|
|
||||||
displayUsername={true}
|
displayUsername={true}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ exports[`components/UserProfile should match snapshot 1`] = `
|
|||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
hideStatus={false}
|
hideStatus={false}
|
||||||
isBusy={false}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
src="/api/v4/users/undefined/image?_=0"
|
src="/api/v4/users/undefined/image?_=0"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
@@ -39,12 +37,10 @@ exports[`components/UserProfile should match snapshot, when displayUsername is e
|
|||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
hideStatus={false}
|
hideStatus={false}
|
||||||
isBusy={false}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
src="/api/v4/users/undefined/image?_=0"
|
src="/api/v4/users/undefined/image?_=0"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
@@ -81,12 +77,10 @@ exports[`components/UserProfile should match snapshot, when user is shared 1`] =
|
|||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
hideStatus={false}
|
hideStatus={false}
|
||||||
isBusy={false}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
src="/api/v4/users/undefined/image?_=0"
|
src="/api/v4/users/undefined/image?_=0"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
@@ -119,12 +113,10 @@ exports[`components/UserProfile should match snapshot, with colorization 1`] = `
|
|||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hasMention={false}
|
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
hideStatus={false}
|
hideStatus={false}
|
||||||
isBusy={false}
|
|
||||||
overwriteName=""
|
overwriteName=""
|
||||||
src="/api/v4/users/undefined/image?_=0"
|
src="/api/v4/users/undefined/image?_=0"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
|
|||||||
@@ -23,14 +23,12 @@ export type Props = {
|
|||||||
user?: UserProfileType;
|
user?: UserProfileType;
|
||||||
userId: string;
|
userId: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
isBusy?: boolean;
|
|
||||||
isShared?: boolean;
|
isShared?: boolean;
|
||||||
overwriteName?: React.ReactNode;
|
overwriteName?: string;
|
||||||
overwriteIcon?: string;
|
overwriteIcon?: string;
|
||||||
disablePopover?: boolean;
|
disablePopover?: boolean;
|
||||||
displayUsername?: boolean;
|
displayUsername?: boolean;
|
||||||
colorize?: boolean;
|
colorize?: boolean;
|
||||||
hasMention?: boolean;
|
|
||||||
hideStatus?: boolean;
|
hideStatus?: boolean;
|
||||||
isMobileView: boolean;
|
isMobileView: boolean;
|
||||||
isRHS?: boolean;
|
isRHS?: boolean;
|
||||||
@@ -44,7 +42,6 @@ export default class UserProfile extends PureComponent<Props> {
|
|||||||
static defaultProps: Partial<Props> = {
|
static defaultProps: Partial<Props> = {
|
||||||
disablePopover: false,
|
disablePopover: false,
|
||||||
displayUsername: false,
|
displayUsername: false,
|
||||||
hasMention: false,
|
|
||||||
hideStatus: false,
|
hideStatus: false,
|
||||||
isRHS: false,
|
isRHS: false,
|
||||||
overwriteName: '',
|
overwriteName: '',
|
||||||
@@ -66,11 +63,9 @@ export default class UserProfile extends PureComponent<Props> {
|
|||||||
disablePopover,
|
disablePopover,
|
||||||
displayName,
|
displayName,
|
||||||
displayUsername,
|
displayUsername,
|
||||||
isBusy,
|
|
||||||
isMobileView,
|
isMobileView,
|
||||||
isRHS,
|
isRHS,
|
||||||
isShared,
|
isShared,
|
||||||
hasMention,
|
|
||||||
hideStatus,
|
hideStatus,
|
||||||
overwriteName,
|
overwriteName,
|
||||||
overwriteIcon,
|
overwriteIcon,
|
||||||
@@ -142,10 +137,8 @@ export default class UserProfile extends PureComponent<Props> {
|
|||||||
userId={userId}
|
userId={userId}
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
src={profileImg}
|
src={profileImg}
|
||||||
isBusy={isBusy}
|
|
||||||
hide={this.hideProfilePopover}
|
hide={this.hideProfilePopover}
|
||||||
hideStatus={hideStatus}
|
hideStatus={hideStatus}
|
||||||
hasMention={hasMention}
|
|
||||||
overwriteName={overwriteName}
|
overwriteName={overwriteName}
|
||||||
overwriteIcon={overwriteIcon}
|
overwriteIcon={overwriteIcon}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ exports[`components/widgets/users/Avatars should fetch missing users 1`] = `
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/1/image?_=0"
|
src="/api/v4/users/1/image?_=0"
|
||||||
@@ -266,7 +266,7 @@ exports[`components/widgets/users/Avatars should fetch missing users 1`] = `
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/6/image?_=0"
|
src="/api/v4/users/6/image?_=0"
|
||||||
@@ -492,7 +492,7 @@ exports[`components/widgets/users/Avatars should fetch missing users 1`] = `
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/7/image?_=0"
|
src="/api/v4/users/7/image?_=0"
|
||||||
@@ -862,7 +862,7 @@ exports[`components/widgets/users/Avatars should properly count overflow 1`] = `
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/1/image?_=0"
|
src="/api/v4/users/1/image?_=0"
|
||||||
@@ -1088,7 +1088,7 @@ exports[`components/widgets/users/Avatars should properly count overflow 1`] = `
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/2/image?_=0"
|
src="/api/v4/users/2/image?_=0"
|
||||||
@@ -1314,7 +1314,7 @@ exports[`components/widgets/users/Avatars should properly count overflow 1`] = `
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/3/image?_=0"
|
src="/api/v4/users/3/image?_=0"
|
||||||
@@ -1682,7 +1682,7 @@ exports[`components/widgets/users/Avatars should support userIds 1`] = `
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/1/image?_=0"
|
src="/api/v4/users/1/image?_=0"
|
||||||
@@ -1908,7 +1908,7 @@ exports[`components/widgets/users/Avatars should support userIds 1`] = `
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/2/image?_=0"
|
src="/api/v4/users/2/image?_=0"
|
||||||
@@ -2134,7 +2134,7 @@ exports[`components/widgets/users/Avatars should support userIds 1`] = `
|
|||||||
defaultOverlayShown={false}
|
defaultOverlayShown={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
overlay={
|
overlay={
|
||||||
<Memo(Connect(injectIntl(ProfilePopover)))
|
<Memo(ProfilePopover)
|
||||||
className="user-profile-popover"
|
className="user-profile-popover"
|
||||||
hide={[Function]}
|
hide={[Function]}
|
||||||
src="/api/v4/users/3/image?_=0"
|
src="/api/v4/users/3/image?_=0"
|
||||||
|
|||||||
@@ -4798,6 +4798,7 @@
|
|||||||
"setting_picture.select": "Select",
|
"setting_picture.select": "Select",
|
||||||
"setting_picture.uploading": "Uploading...",
|
"setting_picture.uploading": "Uploading...",
|
||||||
"shared_channel_indicator.tooltip": "Shared with trusted organizations",
|
"shared_channel_indicator.tooltip": "Shared with trusted organizations",
|
||||||
|
"shared_user_indicator.aria_label": "shared user indicator",
|
||||||
"shared_user_indicator.tooltip": "From a trusted organization",
|
"shared_user_indicator.tooltip": "From a trusted organization",
|
||||||
"shortcuts.browser.channel_next": "Forward in history:\tAlt|Right",
|
"shortcuts.browser.channel_next": "Forward in history:\tAlt|Right",
|
||||||
"shortcuts.browser.channel_next.mac": "Forward in history:\t⌘|]",
|
"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';
|
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(
|
return createSelector(
|
||||||
'makeGetCustomStatus',
|
'makeGetCustomStatus',
|
||||||
(state: GlobalState, userID?: string) => (userID ? getUser(state, userID) : getCurrentUser(state)),
|
(state: GlobalState, userID?: string) => (userID ? getUser(state, userID) : getCurrentUser(state)),
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ window.Observable = Observable;
|
|||||||
|
|
||||||
const localForage = extendPrototype(baseLocalForage);
|
const localForage = extendPrototype(baseLocalForage);
|
||||||
|
|
||||||
export default function configureStore(preloadedState) {
|
export default function configureStore(preloadedState, additionalReducers) {
|
||||||
|
const reducers = additionalReducers ? {...appReducers, ...additionalReducers} : appReducers;
|
||||||
const store = configureServiceStore({
|
const store = configureServiceStore({
|
||||||
appReducers,
|
appReducers: reducers,
|
||||||
getAppReducers,
|
getAppReducers,
|
||||||
preloadedState,
|
preloadedState,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import React from 'react';
|
|||||||
import {IntlProvider} from 'react-intl';
|
import {IntlProvider} from 'react-intl';
|
||||||
import {Provider} from 'react-redux';
|
import {Provider} from 'react-redux';
|
||||||
import {Router} from 'react-router-dom';
|
import {Router} from 'react-router-dom';
|
||||||
|
import type {Reducer} from 'redux';
|
||||||
|
|
||||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export type FullContextOptions = {
|
|||||||
intlMessages?: Record<string, string>;
|
intlMessages?: Record<string, string>;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
useMockedStore?: boolean;
|
useMockedStore?: boolean;
|
||||||
|
pluginReducers?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const renderWithContext = (
|
export const renderWithContext = (
|
||||||
@@ -39,7 +41,7 @@ export const renderWithContext = (
|
|||||||
useMockedStore: partialOptions?.useMockedStore ?? false,
|
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
|
// Store these in an object so that they can be maintained through rerenders
|
||||||
const renderState = {
|
const renderState = {
|
||||||
@@ -82,7 +84,7 @@ export const renderWithContext = (
|
|||||||
* Rerenders the component after replacing the entire store state with the provided one.
|
* Rerenders the component after replacing the entire store state with the provided one.
|
||||||
*/
|
*/
|
||||||
replaceStoreState: (newInitialState: DeepPartial<GlobalState>) => {
|
replaceStoreState: (newInitialState: DeepPartial<GlobalState>) => {
|
||||||
renderState.store = configureOrMockStore(newInitialState, renderState.options.useMockedStore);
|
renderState.store = configureOrMockStore(newInitialState, renderState.options.useMockedStore, partialOptions?.pluginReducers);
|
||||||
|
|
||||||
results.rerender(renderState.component);
|
results.rerender(renderState.component);
|
||||||
},
|
},
|
||||||
@@ -92,15 +94,25 @@ export const renderWithContext = (
|
|||||||
*/
|
*/
|
||||||
updateStoreState: (stateDiff: DeepPartial<GlobalState>) => {
|
updateStoreState: (stateDiff: DeepPartial<GlobalState>) => {
|
||||||
const newInitialState = mergeObjects(renderState.store.getState(), stateDiff);
|
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);
|
results.rerender(renderState.component);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function configureOrMockStore<T>(initialState: DeepPartial<T>, useMockedStore: boolean) {
|
function configureOrMockStore<T>(initialState: DeepPartial<T>, useMockedStore: boolean, extraReducersKeys?: string[]) {
|
||||||
let testStore = configureStore(initialState);
|
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) {
|
if (useMockedStore) {
|
||||||
testStore = mockStore(testStore.getState());
|
testStore = mockStore(testStore.getState());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ exports[`messageHtmlToComponent At mention 1`] = `
|
|||||||
<Memo(Connect(Component))
|
<Memo(Connect(Component))
|
||||||
disableGroupHighlight={false}
|
disableGroupHighlight={false}
|
||||||
disableHighlight={false}
|
disableHighlight={false}
|
||||||
hasMention={true}
|
|
||||||
mentionName="joram"
|
mentionName="joram"
|
||||||
>
|
>
|
||||||
@joram
|
@joram
|
||||||
@@ -29,7 +28,6 @@ exports[`messageHtmlToComponent At mention 2`] = `
|
|||||||
<Memo(Connect(Component))
|
<Memo(Connect(Component))
|
||||||
disableGroupHighlight={false}
|
disableGroupHighlight={false}
|
||||||
disableHighlight={true}
|
disableHighlight={true}
|
||||||
hasMention={true}
|
|
||||||
mentionName="joram"
|
mentionName="joram"
|
||||||
>
|
>
|
||||||
@joram
|
@joram
|
||||||
@@ -46,7 +44,6 @@ exports[`messageHtmlToComponent At mention with group highlight disabled 1`] = `
|
|||||||
<Memo(Connect(Component))
|
<Memo(Connect(Component))
|
||||||
disableGroupHighlight={false}
|
disableGroupHighlight={false}
|
||||||
disableHighlight={false}
|
disableHighlight={false}
|
||||||
hasMention={true}
|
|
||||||
mentionName="developers"
|
mentionName="developers"
|
||||||
>
|
>
|
||||||
@developers
|
@developers
|
||||||
@@ -63,7 +60,6 @@ exports[`messageHtmlToComponent At mention with group highlight disabled 2`] = `
|
|||||||
<Memo(Connect(Component))
|
<Memo(Connect(Component))
|
||||||
disableGroupHighlight={true}
|
disableGroupHighlight={true}
|
||||||
disableHighlight={false}
|
disableHighlight={false}
|
||||||
hasMention={true}
|
|
||||||
mentionName="developers"
|
mentionName="developers"
|
||||||
>
|
>
|
||||||
@developers
|
@developers
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ export function messageHtmlToComponent(html: string, options: Options = {}) {
|
|||||||
const callAtMention = (
|
const callAtMention = (
|
||||||
<AtMention
|
<AtMention
|
||||||
mentionName={mentionName}
|
mentionName={mentionName}
|
||||||
hasMention={true}
|
|
||||||
disableHighlight={!mentionHighlight}
|
disableHighlight={!mentionHighlight}
|
||||||
disableGroupHighlight={disableGroupHighlight}
|
disableGroupHighlight={disableGroupHighlight}
|
||||||
channelId={options.channelId}
|
channelId={options.channelId}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user