[MM-57383] User profile popover performance improved and redesigned (#26420)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
4fbc96ec02
Коммит
0911e4dee7
@@ -50,15 +50,18 @@ describe('Verify Accessibility Support in Different Images', () => {
|
||||
// # Open profile popover
|
||||
cy.getLastPostId().then((postId) => {
|
||||
cy.get(`#post_${postId}`).within(() => {
|
||||
cy.get('.user-popover').click();
|
||||
cy.get('.status-wrapper').click();
|
||||
});
|
||||
|
||||
// * Verify image alt in profile popover
|
||||
cy.get('#user-profile-popover').within(() => {
|
||||
cy.get('.user-profile-popover').within(() => {
|
||||
cy.get('.Avatar').should('have.attr', 'alt', `${otherUser.username} profile image`);
|
||||
});
|
||||
});
|
||||
|
||||
// # Close the profile popover
|
||||
cy.get('body').click();
|
||||
|
||||
// # Open Settings > Display > Themes
|
||||
cy.uiOpenSettingsModal('Display').within(() => {
|
||||
cy.get('#displayButton').click();
|
||||
|
||||
@@ -167,7 +167,7 @@ describe('Verify Accessibility Support in Post', () => {
|
||||
cy.focused().tab();
|
||||
|
||||
// * Verify focus is on the username
|
||||
cy.get('button.user-popover').should('be.focused').and('have.attr', 'aria-label', otherUser.username);
|
||||
cy.get('button.user-popover').should('be.focused');
|
||||
cy.focused().tab();
|
||||
|
||||
// * Verify focus is on the time
|
||||
@@ -188,10 +188,6 @@ describe('Verify Accessibility Support in Post', () => {
|
||||
cy.get(`#CENTER_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save message');
|
||||
cy.focused().tab();
|
||||
|
||||
// * Verify focus is on message actions button
|
||||
cy.get(`#CENTER_actions_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'actions');
|
||||
cy.focused().tab();
|
||||
|
||||
// * Verify focus is on the comment button
|
||||
cy.get(`#CENTER_commentIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'reply');
|
||||
cy.focused().tab();
|
||||
@@ -238,10 +234,6 @@ describe('Verify Accessibility Support in Post', () => {
|
||||
cy.get(`#RHS_COMMENT_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'more');
|
||||
cy.focused().tab({shift: true});
|
||||
|
||||
// * Verify focus is on message actions button
|
||||
cy.get(`#RHS_COMMENT_actions_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'actions');
|
||||
cy.focused().tab({shift: true});
|
||||
|
||||
// * Verify focus is on the save icon
|
||||
cy.get(`#RHS_COMMENT_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save message');
|
||||
cy.focused().tab({shift: true});
|
||||
@@ -259,7 +251,7 @@ describe('Verify Accessibility Support in Post', () => {
|
||||
cy.focused().tab({shift: true});
|
||||
|
||||
// * Verify focus is on the username
|
||||
cy.get('button.user-popover').should('be.focused').and('have.attr', 'aria-label', otherUser.username);
|
||||
cy.get('button.user-popover').should('be.focused');
|
||||
cy.focused().tab({shift: true});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('Profile > Profile Settings > Position', () => {
|
||||
cy.get('.profile-icon > img').as('profileIconForPopover').click();
|
||||
|
||||
// # Verify that the popover is visible and contains position
|
||||
cy.contains('#user-profile-popover', position).should('be.visible');
|
||||
cy.contains('div.user-profile-popover', position).should('be.visible');
|
||||
});
|
||||
|
||||
it('MM-T2064 Position / 128 characters', () => {
|
||||
|
||||
@@ -60,10 +60,11 @@ describe('Profile > Profile Settings> Full Name', () => {
|
||||
cy.getLastPostId().then((postId) => {
|
||||
cy.get(`#post_${postId}`).should('be.visible');
|
||||
cy.get(`#post_${postId} img`).click();
|
||||
cy.get('#user-profile-popover').should('be.visible');
|
||||
cy.get('div.user-profile-popover').should('be.visible');
|
||||
cy.get('button.closeButtonRelativePosition').click();
|
||||
|
||||
// * Popover user name should show truncated to 'This Is a Long Name That Should Tr...'
|
||||
cy.findByTestId(`popover-fullname-${firstUser.username}`).should('have.css', 'text-overflow', 'ellipsis');
|
||||
cy.findByTestId(`popover-fullname-${firstUser.username}`).should('have.css', 'text-overflow', 'clip');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ describe('Settings > Sidebar > General > Edit', () => {
|
||||
});
|
||||
|
||||
// * Verify username in profile popover
|
||||
cy.get('#user-profile-popover').within(() => {
|
||||
cy.get('div.user-profile-popover').within(() => {
|
||||
cy.get('#userPopoverUsername').should('be.visible').and('contain', `${testUser.username}`);
|
||||
});
|
||||
});
|
||||
@@ -238,7 +238,7 @@ describe('Settings > Sidebar > General > Edit', () => {
|
||||
});
|
||||
|
||||
// * Verify that new username is in profile popover
|
||||
cy.get('#user-profile-popover').within(() => {
|
||||
cy.get('div.user-profile-popover').within(() => {
|
||||
cy.get('#userPopoverUsername').should('be.visible').and('contain', `${otherUser.username}-${randomId}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('Bot display name', () => {
|
||||
cy.get('@botPost').then((postIdA) => {
|
||||
cy.get(`#post_${postIdA} button.user-popover`).click();
|
||||
|
||||
cy.get('#user-profile-popover').
|
||||
cy.get('div.user-profile-popover').
|
||||
should('be.visible');
|
||||
|
||||
cy.findByTestId(`popover-fullname-${bot.username}`).
|
||||
@@ -83,7 +83,7 @@ describe('Bot display name', () => {
|
||||
|
||||
// * Verify changed display name
|
||||
cy.get('@newBotPost').then(() => {
|
||||
cy.get('#user-profile-popover').
|
||||
cy.get('div.user-profile-popover').
|
||||
should('be.visible');
|
||||
|
||||
cy.findByTestId(`popover-fullname-${bot.username}`).
|
||||
|
||||
@@ -356,10 +356,11 @@ function verifyMentionedUserAndProfilePopover(postId: string) {
|
||||
cy.wrap($el).click();
|
||||
|
||||
// * Profile popover should be visible
|
||||
cy.get('#user-profile-popover').should('be.visible');
|
||||
cy.get('div.user-profile-popover').should('be.visible');
|
||||
|
||||
// * The username in the popover the same as the username link for each user
|
||||
cy.get('#userPopoverUsername').should('contain', userName);
|
||||
cy.get('div.user-profile-popover').should('contain', userName);
|
||||
cy.get('button.closeButtonRelativePosition').click();
|
||||
|
||||
// Click anywhere to close profile popover
|
||||
cy.get('#channelHeaderInfo').click();
|
||||
|
||||
@@ -117,9 +117,9 @@ describe('MM-T4064 Status expiry visibility', () => {
|
||||
it('MM-T4064_7 should show custom status expiry time in the user popover', () => {
|
||||
// # Click on the post header of the last post by the current user and open profile popover
|
||||
cy.get('.post.current--user .post__header .user-popover').first().click();
|
||||
cy.get('#user-profile-popover').should('exist');
|
||||
cy.get('div.user-profile-popover').should('exist');
|
||||
|
||||
// * Check if the profile popover contains custom status expiry time in the Status heading
|
||||
cy.get('#user-profile-popover #user-popover-status .user-popover__subtitle time').should('have.text', expiresAt.format(expiryTimeFormat));
|
||||
cy.get('div.user-profile-popover #user-popover-status .user-popover__subtitle time').should('have.text', expiresAt.format(expiryTimeFormat));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,9 +108,10 @@ describe('Guest Account - Guest User Experience', () => {
|
||||
});
|
||||
|
||||
// * Verify Guest Badge in Guest User's Profile Popover
|
||||
cy.get('#user-profile-popover').should('be.visible').within(($el) => {
|
||||
cy.get('div.user-profile-popover').should('be.visible').within(($el) => {
|
||||
cy.wrap($el).find('.GuestTag').should('be.visible').and('have.text', 'GUEST');
|
||||
});
|
||||
cy.get('button.closeButtonRelativePosition').click();
|
||||
|
||||
// # Close the profile popover
|
||||
cy.get('#channel-header').click();
|
||||
@@ -168,9 +169,10 @@ describe('Guest Account - Guest User Experience', () => {
|
||||
});
|
||||
|
||||
// * Verify Guest Badge is not displayed in User's Profile Popover
|
||||
cy.get('#user-profile-popover').should('be.visible').within(($el) => {
|
||||
cy.get('div.user-profile-popover').should('be.visible').within(($el) => {
|
||||
cy.wrap($el).find('.user-popover__role').should('not.exist');
|
||||
});
|
||||
cy.get('button.closeButtonRelativePosition').click();
|
||||
|
||||
// # Close the profile popover
|
||||
cy.get('#channel-header').click();
|
||||
|
||||
@@ -106,9 +106,10 @@ describe('Verify Guest User Identification in different screens', () => {
|
||||
});
|
||||
|
||||
// * Verify Guest Badge in Guest User's Profile Popover
|
||||
cy.get('#user-profile-popover').should('be.visible').within(($el) => {
|
||||
cy.get('div.user-profile-popover').should('be.visible').within(($el) => {
|
||||
cy.wrap($el).find('.GuestTag').should('be.visible').and('have.text', 'GUEST');
|
||||
});
|
||||
cy.get('button.closeButtonRelativePosition').click();
|
||||
|
||||
// # Close the profile popover
|
||||
cy.get('#channel-header').click();
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('Profile popover User A & B', () => {
|
||||
find(`[data-mention=${otherUser.username}]`).
|
||||
should('be.visible').
|
||||
click();
|
||||
cy.get('#user-profile-popover').should('be.visible');
|
||||
cy.get('div.user-profile-popover').should('be.visible');
|
||||
});
|
||||
|
||||
// # Add to a Channel should not be shown.
|
||||
|
||||
@@ -160,7 +160,7 @@ function verifyLastPost(owner, username, iconUrl) {
|
||||
|
||||
function verifyProfilePopover(owner, username, iconUrl) {
|
||||
// * Verify that the profile popover is shown
|
||||
cy.get('#user-profile-popover').should('be.visible').within(() => {
|
||||
cy.get('div.user-profile-popover').should('be.visible').within(() => {
|
||||
// * Verify username from payload
|
||||
cy.get('.user-profile-popover__heading').should('be.visible').and('have.text', username);
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('Profile popover', () => {
|
||||
cy.get(`#post_${postId}`).find('.profile-icon > img').click({force: true});
|
||||
|
||||
// * Popover should have rendered to screen
|
||||
cy.get('#user-profile-popover').should('be.visible');
|
||||
cy.get('div.user-profile-popover').should('be.visible');
|
||||
cy.get('body').type('{esc}');
|
||||
});
|
||||
});
|
||||
@@ -50,7 +50,7 @@ describe('Profile popover', () => {
|
||||
cy.get(`#post_${postId}`).find('.user-popover').click({force: true});
|
||||
|
||||
// * Popover should have rendered to screen
|
||||
cy.get('#user-profile-popover').should('be.visible');
|
||||
cy.get('div.user-profile-popover').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('Scroll', () => {
|
||||
cy.getLastPostId().as('lastPostId');
|
||||
|
||||
// Getting height of each post before applying 'Fixed width, centered' option and assigning to alias
|
||||
cy.findAllByLabelText('sysadmin').eq(0).invoke('height').then((height) => {
|
||||
cy.get('button.user-popover.style--none').eq(0).invoke('height').then((height) => {
|
||||
cy.wrap(height).as('initialUserNameHeight');
|
||||
});
|
||||
getComponentByText('@firstPostId', firstMessage).invoke('height').then((height) => {
|
||||
@@ -72,10 +72,10 @@ describe('Scroll', () => {
|
||||
getFileThumbnail('mpeg-video-file.mpg').invoke('height').then((height) => {
|
||||
cy.wrap(height).as('initialMpgHeight');
|
||||
});
|
||||
getFileThumbnail('gif-image-file.gif').invoke('height').then((height) => {
|
||||
getImageThumbnail('gif-image-file.gif').invoke('height').then((height) => {
|
||||
cy.wrap(height).as('initialGifHeight');
|
||||
});
|
||||
getFileThumbnail('jpg-image-file.jpg').invoke('height').then((height) => {
|
||||
getImageThumbnail('jpg-image-file.jpg').invoke('height').then((height) => {
|
||||
cy.wrap(height).as('initialJpgHeight');
|
||||
});
|
||||
getComponentBySelector('@linkPreviewPostId', '.PostAttachmentOpenGraph__image').invoke('height').then((height) => {
|
||||
@@ -105,7 +105,7 @@ describe('Scroll', () => {
|
||||
// * Verify there is no scroll pop
|
||||
cy.get('#post-list').should('exist').within(() => {
|
||||
cy.get('@initialUserNameHeight').then((originalHeight) => {
|
||||
cy.findAllByLabelText('sysadmin').eq(0).invoke('height').should('be.equal', originalHeight);
|
||||
cy.get('button.user-popover.style--none').eq(0).invoke('height').should('be.equal', originalHeight);
|
||||
});
|
||||
cy.get('@initialFirstPostHeight').then((originalHeight) => {
|
||||
getComponentByText('@firstPostId', firstMessage).invoke('height').should('be.equal', originalHeight);
|
||||
@@ -120,10 +120,10 @@ describe('Scroll', () => {
|
||||
getFileThumbnail('mpeg-video-file.mpg').invoke('height').should('be.equal', originalHeight);
|
||||
});
|
||||
cy.get('@initialGifHeight').then((originalHeight) => {
|
||||
getFileThumbnail('gif-image-file.gif').invoke('height').should('be.equal', originalHeight);
|
||||
getImageThumbnail('gif-image-file.gif').invoke('height').should('be.equal', originalHeight);
|
||||
});
|
||||
cy.get('@initialJpgHeight').then((originalHeight) => {
|
||||
getFileThumbnail('jpg-image-file.jpg').invoke('height').should('be.equal', originalHeight);
|
||||
getImageThumbnail('jpg-image-file.jpg').invoke('height').should('be.equal', originalHeight);
|
||||
});
|
||||
cy.get('@initialInlineImgHeight').then((originalHeight) => {
|
||||
getComponentBySelector('@gifLinkPostId', 'img[aria-label="file thumbnail"]').invoke('height').should('be.equal', originalHeight);
|
||||
@@ -136,6 +136,13 @@ describe('Scroll', () => {
|
||||
|
||||
// Get thumbnail component based on filename
|
||||
const getFileThumbnail = (filename) => {
|
||||
return cy.get(`@${filename}PostId`).then((postId) => {
|
||||
cy.get(`#${postId}_message a.post-image__name`);
|
||||
});
|
||||
};
|
||||
|
||||
// Get image component based on filename
|
||||
const getImageThumbnail = (filename) => {
|
||||
return cy.get(`@${filename}PostId`).then((postId) => {
|
||||
cy.get(`#${postId}_message`).findByLabelText(`file thumbnail ${filename}`);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match d
|
||||
class="AdminUserCard__header"
|
||||
>
|
||||
<span
|
||||
class="status-wrapper admin-user-card"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
class="status-wrapper admin-user-card"
|
||||
>
|
||||
<button
|
||||
class="RoundButton-dvlhqG gnYSKj style--none"
|
||||
@@ -22,7 +24,6 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match d
|
||||
class="Avatar Avatar-xxl"
|
||||
loading="lazy"
|
||||
src="/api/v4/users/1234/image"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
@@ -69,7 +70,9 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
||||
class="AdminUserCard__header"
|
||||
>
|
||||
<span
|
||||
class="status-wrapper admin-user-card"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
class="status-wrapper admin-user-card"
|
||||
>
|
||||
<button
|
||||
class="RoundButton-dvlhqG gnYSKj style--none"
|
||||
@@ -82,7 +85,6 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
||||
class="Avatar Avatar-xxl"
|
||||
loading="lazy"
|
||||
src="/api/v4/users/1234/image"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
@@ -124,7 +126,9 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
||||
class="AdminUserCard__header"
|
||||
>
|
||||
<span
|
||||
class="status-wrapper admin-user-card"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
class="status-wrapper admin-user-card"
|
||||
>
|
||||
<button
|
||||
class="RoundButton-dvlhqG gnYSKj style--none"
|
||||
@@ -137,7 +141,6 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
||||
class="Avatar Avatar-xxl"
|
||||
loading="lazy"
|
||||
src="/api/v4/users/1234/image"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
@@ -177,7 +180,9 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
||||
class="AdminUserCard__header"
|
||||
>
|
||||
<span
|
||||
class="status-wrapper admin-user-card"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
class="status-wrapper admin-user-card"
|
||||
>
|
||||
<button
|
||||
class="RoundButton-dvlhqG gnYSKj style--none"
|
||||
@@ -190,7 +195,6 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
||||
class="Avatar Avatar-xxl"
|
||||
loading="lazy"
|
||||
src="/api/v4/users/1234/image"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -2,114 +2,68 @@
|
||||
|
||||
exports[`components/AtMention should match snapshot when mentioning a group followed by punctuation 1`] = `
|
||||
<Fragment>
|
||||
<span>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Connect(Component)
|
||||
group={
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "qwerty1",
|
||||
"member_count": 0,
|
||||
"name": "developers",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
}
|
||||
}
|
||||
hide={[Function]}
|
||||
returnFocus={[Function]}
|
||||
showUserOverlay={[Function]}
|
||||
/>
|
||||
</Overlay>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<span />
|
||||
</Overlay>
|
||||
<UserGroupPopoverController
|
||||
group={
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "qwerty1",
|
||||
"member_count": 0,
|
||||
"name": "developers",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
}
|
||||
}
|
||||
returnFocus={[Function]}
|
||||
>
|
||||
<a
|
||||
aria-haspopup="dialog"
|
||||
className="group-mention-link"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@developers
|
||||
</a>
|
||||
</span>
|
||||
</UserGroupPopoverController>
|
||||
.
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/AtMention should match snapshot when mentioning a group that is allowed reference 1`] = `
|
||||
<Fragment>
|
||||
<span>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Connect(Component)
|
||||
group={
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "qwerty1",
|
||||
"member_count": 0,
|
||||
"name": "developers",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
}
|
||||
}
|
||||
hide={[Function]}
|
||||
returnFocus={[Function]}
|
||||
showUserOverlay={[Function]}
|
||||
/>
|
||||
</Overlay>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<span />
|
||||
</Overlay>
|
||||
<UserGroupPopoverController
|
||||
group={
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "qwerty1",
|
||||
"member_count": 0,
|
||||
"name": "developers",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
}
|
||||
}
|
||||
returnFocus={[Function]}
|
||||
>
|
||||
<a
|
||||
aria-haspopup="dialog"
|
||||
className="group-mention-link"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@developers
|
||||
</a>
|
||||
</span>
|
||||
</UserGroupPopoverController>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -139,285 +93,136 @@ exports[`components/AtMention should match snapshot when mentioning all with mix
|
||||
|
||||
exports[`components/AtMention should match snapshot when mentioning current user 1`] = `
|
||||
<Fragment>
|
||||
<span
|
||||
className="mention--highlight"
|
||||
<ProfilePopoverController
|
||||
returnFocus={[Function]}
|
||||
src="/api/v4/users/abc1/image"
|
||||
triggerComponentClass="mention--highlight"
|
||||
userId="abc1"
|
||||
>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc1/image"
|
||||
userId="abc1"
|
||||
/>
|
||||
</Overlay>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<span />
|
||||
</Overlay>
|
||||
<a
|
||||
aria-haspopup="dialog"
|
||||
className="mention-link"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@First Last
|
||||
</a>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/AtMention should match snapshot when mentioning user 1`] = `
|
||||
<Fragment>
|
||||
<span>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
userId="abc2"
|
||||
/>
|
||||
</Overlay>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<span />
|
||||
</Overlay>
|
||||
<ProfilePopoverController
|
||||
returnFocus={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
triggerComponentClass=""
|
||||
userId="abc2"
|
||||
>
|
||||
<a
|
||||
aria-haspopup="dialog"
|
||||
className="mention-link"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@Nick
|
||||
</a>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/AtMention should match snapshot when mentioning user containing and followed by punctuation 1`] = `
|
||||
<Fragment>
|
||||
<span>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc3/image"
|
||||
userId="abc3"
|
||||
/>
|
||||
</Overlay>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<span />
|
||||
</Overlay>
|
||||
<ProfilePopoverController
|
||||
returnFocus={[Function]}
|
||||
src="/api/v4/users/abc3/image"
|
||||
triggerComponentClass=""
|
||||
userId="abc3"
|
||||
>
|
||||
<a
|
||||
aria-haspopup="dialog"
|
||||
className="mention-link"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@Dot Matrix
|
||||
</a>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
.
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/AtMention should match snapshot when mentioning user containing punctuation 1`] = `
|
||||
<Fragment>
|
||||
<span>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc3/image"
|
||||
userId="abc3"
|
||||
/>
|
||||
</Overlay>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<span />
|
||||
</Overlay>
|
||||
<ProfilePopoverController
|
||||
returnFocus={[Function]}
|
||||
src="/api/v4/users/abc3/image"
|
||||
triggerComponentClass=""
|
||||
userId="abc3"
|
||||
>
|
||||
<a
|
||||
aria-haspopup="dialog"
|
||||
className="mention-link"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@Dot Matrix
|
||||
</a>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/AtMention should match snapshot when mentioning user followed by punctuation 1`] = `
|
||||
<Fragment>
|
||||
<span>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
userId="abc2"
|
||||
/>
|
||||
</Overlay>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<span />
|
||||
</Overlay>
|
||||
<ProfilePopoverController
|
||||
returnFocus={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
triggerComponentClass=""
|
||||
userId="abc2"
|
||||
>
|
||||
<a
|
||||
aria-haspopup="dialog"
|
||||
className="mention-link"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@Nick
|
||||
</a>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
...
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/AtMention should match snapshot when mentioning user with different teammate name display setting 1`] = `
|
||||
<Fragment>
|
||||
<span>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
userId="abc2"
|
||||
/>
|
||||
</Overlay>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<span />
|
||||
</Overlay>
|
||||
<ProfilePopoverController
|
||||
returnFocus={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
triggerComponentClass=""
|
||||
userId="abc2"
|
||||
>
|
||||
<a
|
||||
aria-haspopup="dialog"
|
||||
className="mention-link"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@user1
|
||||
</a>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/AtMention should match snapshot when mentioning user with mixed case 1`] = `
|
||||
<Fragment>
|
||||
<span>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
userId="abc2"
|
||||
/>
|
||||
</Overlay>
|
||||
<Overlay
|
||||
animation={[Function]}
|
||||
onHide={[Function]}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
show={false}
|
||||
>
|
||||
<span />
|
||||
</Overlay>
|
||||
<ProfilePopoverController
|
||||
returnFocus={[Function]}
|
||||
src="/api/v4/users/abc2/image"
|
||||
triggerComponentClass=""
|
||||
userId="abc2"
|
||||
>
|
||||
<a
|
||||
aria-haspopup="dialog"
|
||||
className="mention-link"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@Nick
|
||||
</a>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ describe('components/AtMention', () => {
|
||||
marketing: TestHelper.getGroupMock({id: 'qwerty2', name: 'marketing', allow_reference: false}),
|
||||
accounting: TestHelper.getGroupMock({id: 'qwerty3', name: 'accounting', allow_reference: true}),
|
||||
},
|
||||
dispatch: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot when mentioning user', () => {
|
||||
|
||||
@@ -1,108 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useRef, useState, useMemo, type ComponentProps} from 'react';
|
||||
import {Overlay} from 'react-bootstrap';
|
||||
|
||||
import type {Group} from '@mattermost/types/groups';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
import classNames from 'classnames';
|
||||
import React, {useRef, useMemo, memo} from 'react';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import ProfilePopover from 'components/profile_popover';
|
||||
import UserGroupPopover from 'components/user_group_popover';
|
||||
import {MAX_LIST_HEIGHT, getListHeight, VIEWPORT_SCALE_FACTOR} from 'components/user_group_popover/group_member_list/group_member_list';
|
||||
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import Constants, {A11yCustomEventTypes} from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {popOverOverlayPosition, approxGroupPopOverHeight} from 'utils/position_utils';
|
||||
import {A11yCustomEventTypes} from 'utils/constants';
|
||||
import {getUserOrGroupFromMentionName} from 'utils/post_utils';
|
||||
import {getViewportSize} from 'utils/utils';
|
||||
|
||||
const HEADER_HEIGHT_ESTIMATE = 130;
|
||||
import type {PropsFromRedux} from './index';
|
||||
|
||||
type Props = {
|
||||
currentUserId: string;
|
||||
type OwnProps = {
|
||||
mentionName: string;
|
||||
teammateNameDisplay: string;
|
||||
usersByUsername: Record<string, UserProfile>;
|
||||
groupsByName: Record<string, Group>;
|
||||
children?: React.ReactNode;
|
||||
channelId?: string;
|
||||
disableHighlight?: boolean;
|
||||
disableGroupHighlight?: boolean;
|
||||
}
|
||||
|
||||
export const AtMention = (props: Props) => {
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
type Props = OwnProps & PropsFromRedux;
|
||||
|
||||
const [show, setShow] = useState(false);
|
||||
const [groupUser, setGroupUser] = useState<UserProfile | undefined>();
|
||||
const [target, setTarget] = useState<HTMLAnchorElement | undefined>();
|
||||
const [placement, setPlacement] = useState<ComponentProps<typeof Overlay>['placement']>('right');
|
||||
const AtMention = (props: Props) => {
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
const [user, group] = useMemo(
|
||||
() => getUserOrGroupFromMentionName(props.mentionName, props.usersByUsername, props.groupsByName, props.disableGroupHighlight),
|
||||
[props.mentionName, props.usersByUsername, props.groupsByName, props.disableGroupHighlight],
|
||||
);
|
||||
|
||||
const showOverlay = (target?: HTMLAnchorElement) => {
|
||||
const targetBounds = ref.current?.getBoundingClientRect();
|
||||
|
||||
if (targetBounds) {
|
||||
let popOverHeight: number;
|
||||
|
||||
if (group) {
|
||||
popOverHeight = approxGroupPopOverHeight(
|
||||
getListHeight(group.member_count),
|
||||
getViewportSize().h,
|
||||
VIEWPORT_SCALE_FACTOR,
|
||||
HEADER_HEIGHT_ESTIMATE,
|
||||
MAX_LIST_HEIGHT,
|
||||
);
|
||||
} else {
|
||||
popOverHeight = getViewportSize().h - 240;
|
||||
}
|
||||
|
||||
const placement = popOverOverlayPosition(targetBounds, getViewportSize().h, popOverHeight);
|
||||
|
||||
setTarget(target);
|
||||
setShow(!show);
|
||||
setGroupUser(undefined);
|
||||
setPlacement(placement);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
showOverlay(e.target as HTMLAnchorElement);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLAnchorElement>) => {
|
||||
if (isKeyPressed(e, Constants.KeyCodes.ENTER) || isKeyPressed(e, Constants.KeyCodes.SPACE)) {
|
||||
e.preventDefault();
|
||||
|
||||
// Prevent propagation so that the message textbox isn't focused
|
||||
e.stopPropagation();
|
||||
showOverlay(e.target as HTMLAnchorElement);
|
||||
}
|
||||
};
|
||||
|
||||
const hideOverlay = () => {
|
||||
setShow(false);
|
||||
};
|
||||
|
||||
const showGroupUserOverlay = (user: UserProfile) => {
|
||||
hideOverlay();
|
||||
setGroupUser(user);
|
||||
};
|
||||
|
||||
const hideGroupUserOverlay = () => {
|
||||
setGroupUser(undefined);
|
||||
};
|
||||
|
||||
const returnFocus = () => {
|
||||
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
||||
A11yCustomEventTypes.FOCUS, {
|
||||
@@ -114,98 +45,57 @@ export const AtMention = (props: Props) => {
|
||||
));
|
||||
};
|
||||
|
||||
const getPopOver = (user?: UserProfile, group?: Group) => {
|
||||
if (user) {
|
||||
return (
|
||||
if (user) {
|
||||
const userMentionNameSuffix = props.mentionName.substring(user.username.length);
|
||||
const userDisplayName = displayUsername(user, props.teammateNameDisplay);
|
||||
const highlightMention = !props.disableHighlight && user.id === props.currentUserId;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProfilePopover
|
||||
className='user-profile-popover'
|
||||
triggerComponentClass={classNames({'mention--highlight': highlightMention})}
|
||||
userId={user.id}
|
||||
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
|
||||
hide={hideOverlay}
|
||||
channelId={props.channelId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
returnFocus={returnFocus}
|
||||
>
|
||||
<a
|
||||
ref={ref}
|
||||
className='mention-link'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
>
|
||||
{'@' + userDisplayName}
|
||||
</a>
|
||||
</ProfilePopover>
|
||||
{userMentionNameSuffix}
|
||||
</>
|
||||
);
|
||||
} else if (group) {
|
||||
const groupMentionNameSuffix = props.mentionName.substring(group.name.length);
|
||||
const groupDisplayName = group.name;
|
||||
|
||||
if (group) {
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<UserGroupPopover
|
||||
group={group}
|
||||
hide={hideOverlay}
|
||||
showUserOverlay={showGroupUserOverlay}
|
||||
returnFocus={returnFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!user && !group) {
|
||||
return <>{props.children}</>;
|
||||
>
|
||||
<a
|
||||
ref={ref}
|
||||
className='group-mention-link'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
>
|
||||
{'@' + groupDisplayName}
|
||||
</a>
|
||||
</UserGroupPopover>
|
||||
{groupMentionNameSuffix}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let suffix = '';
|
||||
let displayName = '';
|
||||
let highlightMention = false; // only for user
|
||||
|
||||
if (user) {
|
||||
suffix = props.mentionName.substring(user.username.length);
|
||||
displayName = displayUsername(user, props.teammateNameDisplay);
|
||||
highlightMention = !props.disableHighlight && user.id === props.currentUserId;
|
||||
} else if (group) { // if statement needed for union
|
||||
suffix = props.mentionName.substring(group.name.length);
|
||||
displayName = group.name;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={highlightMention ? 'mention--highlight' : undefined}
|
||||
>
|
||||
<Overlay
|
||||
placement={placement}
|
||||
show={show}
|
||||
target={target}
|
||||
rootClose={true}
|
||||
onHide={hideOverlay}
|
||||
>
|
||||
{getPopOver(user, group)}
|
||||
</Overlay>
|
||||
<Overlay
|
||||
placement={placement}
|
||||
show={groupUser !== undefined}
|
||||
target={target}
|
||||
onHide={hideGroupUserOverlay}
|
||||
rootClose={true}
|
||||
>
|
||||
{groupUser ? (
|
||||
<ProfilePopover
|
||||
className='user-profile-popover'
|
||||
userId={groupUser.id}
|
||||
src={Client4.getProfilePictureUrl(groupUser.id, groupUser.last_picture_update)}
|
||||
channelId={props.channelId}
|
||||
hide={hideGroupUserOverlay}
|
||||
returnFocus={returnFocus}
|
||||
/>
|
||||
) : <span/> // prevents blank-screen crash when closing groupUser ProfilePopover
|
||||
}
|
||||
</Overlay>
|
||||
<a
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={group ? 'group-mention-link' : 'mention-link'}
|
||||
ref={ref}
|
||||
aria-haspopup='dialog'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
>
|
||||
{'@' + displayName}
|
||||
</a>
|
||||
</span>
|
||||
{suffix}
|
||||
</>
|
||||
);
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
export default React.memo(AtMention);
|
||||
export default memo(AtMention);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getAllGroupsForReferenceByName} from 'mattermost-redux/selectors/entities/groups';
|
||||
@@ -20,4 +21,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(AtMention);
|
||||
const connector = connect(mapStateToProps);
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connector(AtMention);
|
||||
|
||||
@@ -99,7 +99,6 @@ const AboutAreaDM = ({channel, dmUser, actions}: Props) => {
|
||||
userId={dmUser.user.id}
|
||||
channelId={channel.id}
|
||||
size='xl'
|
||||
popoverPlacement='left'
|
||||
/>
|
||||
</UserAvatar>
|
||||
<UserInfo>
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('channel_info_rhs/about_area_gm', () => {
|
||||
initialState,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('my username')).toBeInTheDocument();
|
||||
expect(screen.getByText('my username')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should display channel header', () => {
|
||||
|
||||
@@ -89,7 +89,6 @@ const AboutAreaGM = ({channel, gmUsers, actions}: Props) => {
|
||||
userId={user.id}
|
||||
username={user.username}
|
||||
channelId={channel.id}
|
||||
popoverPlacement='left'
|
||||
/>
|
||||
</ProfilePictureContainer>
|
||||
))}
|
||||
@@ -99,7 +98,6 @@ const AboutAreaGM = ({channel, gmUsers, actions}: Props) => {
|
||||
<React.Fragment key={user.id}>
|
||||
<UserProfileElement
|
||||
userId={user.id}
|
||||
isRHS={true}
|
||||
channelId={channel.id}
|
||||
/>
|
||||
{(i + 1 !== length) && (<span>{', '}</span>)}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
@@ -14,14 +14,10 @@ import {isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import ChannelMembersDropdown from 'components/channel_members_dropdown';
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import type {BaseOverlayTrigger} from 'components/overlay_trigger';
|
||||
import ProfilePicture from 'components/profile_picture';
|
||||
import ProfilePopover from 'components/profile_popover';
|
||||
import Tooltip from 'components/tooltip';
|
||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import type {ChannelMember} from './channel_members_rhs';
|
||||
|
||||
@@ -30,15 +26,6 @@ const Avatar = styled.div`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const UserInfo = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const DisplayName = styled.span`
|
||||
display: inline;
|
||||
overflow: hidden;
|
||||
@@ -110,19 +97,10 @@ interface Props {
|
||||
};
|
||||
}
|
||||
|
||||
interface MMOverlayTrigger extends BaseOverlayTrigger {
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
const Member = ({className, channel, member, index, totalUsers, editing, actions}: Props) => {
|
||||
const overlay = React.createRef<MMOverlayTrigger>();
|
||||
const profileSrc = Client4.getProfilePictureUrl(member.user.id, member.user.last_picture_update);
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const hideProfilePopover = () => {
|
||||
if (overlay.current) {
|
||||
overlay.current.hide();
|
||||
}
|
||||
};
|
||||
const userProfileSrc = Client4.getProfilePictureUrl(member.user.id, member.user.last_picture_update);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -130,59 +108,46 @@ const Member = ({className, channel, member, index, totalUsers, editing, actions
|
||||
style={{height: '48px'}}
|
||||
data-testid={`memberline-${member.user.id}`}
|
||||
>
|
||||
|
||||
<OverlayTrigger
|
||||
ref={overlay}
|
||||
trigger={['click']}
|
||||
placement={'left'}
|
||||
rootClose={true}
|
||||
overlay={
|
||||
<ProfilePopover
|
||||
className='user-profile-popover'
|
||||
<span className='ProfileSpan'>
|
||||
<Avatar>
|
||||
<ProfilePicture
|
||||
size='sm'
|
||||
status={member.status}
|
||||
isBot={member.user.is_bot}
|
||||
userId={member.user.id}
|
||||
src={profileSrc}
|
||||
hide={hideProfilePopover}
|
||||
hideStatus={member.user.is_bot}
|
||||
username={member.displayName}
|
||||
src={userProfileSrc}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span className='ProfileSpan'>
|
||||
<Avatar>
|
||||
<ProfilePicture
|
||||
popoverPlacement='left'
|
||||
size='sm'
|
||||
status={member.status}
|
||||
isBot={member.user.is_bot}
|
||||
userId={member.user.id}
|
||||
username={member.displayName}
|
||||
src={Client4.getProfilePictureUrl(member.user.id, member.user.last_picture_update)}
|
||||
/>
|
||||
</Avatar>
|
||||
<UserInfo>
|
||||
<DisplayName>
|
||||
{member.displayName}
|
||||
{isGuest(member.user.roles) && <GuestTag/>}
|
||||
</DisplayName>
|
||||
{
|
||||
member.displayName === member.user.username ? null : <Username>{'@'}{member.user.username}</Username>
|
||||
}
|
||||
<CustomStatusEmoji
|
||||
userID={member.user.id}
|
||||
showTooltip={true}
|
||||
emojiSize={16}
|
||||
spanStyle={{
|
||||
display: 'flex',
|
||||
flex: '0 0 auto',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
emojiStyle={{
|
||||
marginLeft: '8px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
/>
|
||||
</UserInfo>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</Avatar>
|
||||
<ProfilePopover
|
||||
triggerComponentClass='profileSpan_userInfo'
|
||||
userId={member.user.id}
|
||||
src={userProfileSrc}
|
||||
hideStatus={member.user.is_bot}
|
||||
>
|
||||
<DisplayName>
|
||||
{member.displayName}
|
||||
{isGuest(member.user.roles) && <GuestTag/>}
|
||||
</DisplayName>
|
||||
{
|
||||
member.displayName === member.user.username ? null : <Username>{'@'}{member.user.username}</Username>
|
||||
}
|
||||
<CustomStatusEmoji
|
||||
userID={member.user.id}
|
||||
showTooltip={true}
|
||||
emojiSize={16}
|
||||
spanStyle={{
|
||||
display: 'flex',
|
||||
flex: '0 0 auto',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
emojiStyle={{
|
||||
marginLeft: '8px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
/>
|
||||
</ProfilePopover>
|
||||
</span>
|
||||
|
||||
<RoleChooser
|
||||
className={classNames({editing}, 'member-role-chooser')}
|
||||
@@ -217,22 +182,18 @@ const Member = ({className, channel, member, index, totalUsers, editing, actions
|
||||
)}
|
||||
</RoleChooser>
|
||||
{!editing && (
|
||||
<SendMessage onClick={() => actions.openDirectMessage(member.user)}>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='left'
|
||||
overlay={
|
||||
<Tooltip>
|
||||
<FormattedMessage
|
||||
id='channel_members_rhs.member.send_message'
|
||||
defaultMessage='Send message'
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<WithTooltip
|
||||
id={`member-tooltip-${member.user.id}`}
|
||||
title={formatMessage({
|
||||
id: 'channel_members_rhs.member.send_message',
|
||||
defaultMessage: 'Send message',
|
||||
})}
|
||||
placement='left'
|
||||
>
|
||||
<SendMessage onClick={() => actions.openDirectMessage(member.user)}>
|
||||
<i className='icon icon-send'/>
|
||||
</OverlayTrigger>
|
||||
</SendMessage>
|
||||
</SendMessage>
|
||||
</WithTooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -256,14 +217,20 @@ export default styled(Member)`
|
||||
}
|
||||
|
||||
.ProfileSpan {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
// This padding is to make sure the status icon doesnt get clipped off because of the overflow
|
||||
padding: 4px 0;
|
||||
margin-right: auto;
|
||||
padding: 4px 0; // This padding is to make sure the status icon doesn't get clipped off because of the overflow
|
||||
|
||||
.profileSpan_userInfo {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.MenuWrapper {
|
||||
|
||||
@@ -31,161 +31,89 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
|
||||
>
|
||||
<ProfilePicture
|
||||
channelId="channel_id"
|
||||
hasMention={false}
|
||||
isEmoji={false}
|
||||
popoverPlacement="right"
|
||||
size="md"
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
status="status"
|
||||
userId="user_id"
|
||||
username="username"
|
||||
wrapperClass=""
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(ProfilePopover)
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<ProfilePopoverController
|
||||
channelId="channel_id"
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
triggerComponentClass="status-wrapper"
|
||||
userId="user_id"
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<OverlayWrapper
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
}
|
||||
}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<span
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
className="status-wrapper"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPointerDown={[Function]}
|
||||
>
|
||||
<span
|
||||
className="status-wrapper "
|
||||
onClick={[Function]}
|
||||
<RoundButton
|
||||
className="style--none"
|
||||
size="md"
|
||||
>
|
||||
<RoundButton
|
||||
className="style--none"
|
||||
<button
|
||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||
size="md"
|
||||
>
|
||||
<button
|
||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||
size="md"
|
||||
<span
|
||||
className="profile-icon "
|
||||
>
|
||||
<span
|
||||
className="profile-icon "
|
||||
<Memo(Avatar)
|
||||
size="md"
|
||||
url="/api/v4/users/user_id/image?_=0"
|
||||
username="username"
|
||||
>
|
||||
<Memo(Avatar)
|
||||
size="md"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/user_id/image?_=0"
|
||||
username="username"
|
||||
>
|
||||
<img
|
||||
alt="username profile image"
|
||||
className="Avatar Avatar-md"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
</button>
|
||||
</RoundButton>
|
||||
<Memo(StatusIcon)
|
||||
status="status"
|
||||
<img
|
||||
alt="username profile image"
|
||||
className="Avatar Avatar-md"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
</button>
|
||||
</RoundButton>
|
||||
<Memo(StatusIcon)
|
||||
status="status"
|
||||
>
|
||||
<StatusOfflineIcon
|
||||
className="status "
|
||||
>
|
||||
<StatusOfflineIcon
|
||||
<span
|
||||
className="status "
|
||||
>
|
||||
<span
|
||||
className="status "
|
||||
>
|
||||
<svg
|
||||
aria-label="Offline Icon"
|
||||
className="offline--icon"
|
||||
height="100%"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"clipRule": "evenodd",
|
||||
"fillRule": "evenodd",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeMiterlimit": 1.41421,
|
||||
}
|
||||
<svg
|
||||
aria-label="Offline Icon"
|
||||
className="offline--icon"
|
||||
height="100%"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"clipRule": "evenodd",
|
||||
"fillRule": "evenodd",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeMiterlimit": 1.41421,
|
||||
}
|
||||
viewBox="0 0 20 20"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M10,0c5.519,0 10,4.481 10,10c0,5.519 -4.481,10 -10,10c-5.519,0 -10,-4.481 -10,-10c0,-5.519 4.481,-10 10,-10Zm0,2c4.415,0 8,3.585 8,8c0,4.415 -3.585,8 -8,8c-4.415,0 -8,-3.585 -8,-8c0,-4.415 3.585,-8 8,-8Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</StatusOfflineIcon>
|
||||
</Memo(StatusIcon)>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</OverlayTrigger>
|
||||
}
|
||||
viewBox="0 0 20 20"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M10,0c5.519,0 10,4.481 10,10c0,5.519 -4.481,10 -10,10c-5.519,0 -10,-4.481 -10,-10c0,-5.519 4.481,-10 10,-10Zm0,2c4.415,0 8,3.585 8,8c0,4.415 -3.585,8 -8,8c-4.415,0 -8,-3.585 -8,-8c0,-4.415 3.585,-8 8,-8Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</StatusOfflineIcon>
|
||||
</Memo(StatusIcon)>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
</ProfilePicture>
|
||||
</div>
|
||||
<div
|
||||
@@ -300,161 +228,89 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
|
||||
>
|
||||
<ProfilePicture
|
||||
channelId="channel_id"
|
||||
hasMention={false}
|
||||
isEmoji={false}
|
||||
popoverPlacement="right"
|
||||
size="md"
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
status="status"
|
||||
userId="user_id"
|
||||
username="username"
|
||||
wrapperClass=""
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(ProfilePopover)
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<ProfilePopoverController
|
||||
channelId="channel_id"
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
triggerComponentClass="status-wrapper"
|
||||
userId="user_id"
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<OverlayWrapper
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
}
|
||||
}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<span
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
className="status-wrapper"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPointerDown={[Function]}
|
||||
>
|
||||
<span
|
||||
className="status-wrapper "
|
||||
onClick={[Function]}
|
||||
<RoundButton
|
||||
className="style--none"
|
||||
size="md"
|
||||
>
|
||||
<RoundButton
|
||||
className="style--none"
|
||||
<button
|
||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||
size="md"
|
||||
>
|
||||
<button
|
||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||
size="md"
|
||||
<span
|
||||
className="profile-icon "
|
||||
>
|
||||
<span
|
||||
className="profile-icon "
|
||||
<Memo(Avatar)
|
||||
size="md"
|
||||
url="/api/v4/users/user_id/image?_=0"
|
||||
username="username"
|
||||
>
|
||||
<Memo(Avatar)
|
||||
size="md"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/user_id/image?_=0"
|
||||
username="username"
|
||||
>
|
||||
<img
|
||||
alt="username profile image"
|
||||
className="Avatar Avatar-md"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
</button>
|
||||
</RoundButton>
|
||||
<Memo(StatusIcon)
|
||||
status="status"
|
||||
<img
|
||||
alt="username profile image"
|
||||
className="Avatar Avatar-md"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
</button>
|
||||
</RoundButton>
|
||||
<Memo(StatusIcon)
|
||||
status="status"
|
||||
>
|
||||
<StatusOfflineIcon
|
||||
className="status "
|
||||
>
|
||||
<StatusOfflineIcon
|
||||
<span
|
||||
className="status "
|
||||
>
|
||||
<span
|
||||
className="status "
|
||||
>
|
||||
<svg
|
||||
aria-label="Offline Icon"
|
||||
className="offline--icon"
|
||||
height="100%"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"clipRule": "evenodd",
|
||||
"fillRule": "evenodd",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeMiterlimit": 1.41421,
|
||||
}
|
||||
<svg
|
||||
aria-label="Offline Icon"
|
||||
className="offline--icon"
|
||||
height="100%"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"clipRule": "evenodd",
|
||||
"fillRule": "evenodd",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeMiterlimit": 1.41421,
|
||||
}
|
||||
viewBox="0 0 20 20"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M10,0c5.519,0 10,4.481 10,10c0,5.519 -4.481,10 -10,10c-5.519,0 -10,-4.481 -10,-10c0,-5.519 4.481,-10 10,-10Zm0,2c4.415,0 8,3.585 8,8c0,4.415 -3.585,8 -8,8c-4.415,0 -8,-3.585 -8,-8c0,-4.415 3.585,-8 8,-8Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</StatusOfflineIcon>
|
||||
</Memo(StatusIcon)>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</OverlayTrigger>
|
||||
}
|
||||
viewBox="0 0 20 20"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M10,0c5.519,0 10,4.481 10,10c0,5.519 -4.481,10 -10,10c-5.519,0 -10,-4.481 -10,-10c0,-5.519 4.481,-10 10,-10Zm0,2c4.415,0 8,3.585 8,8c0,4.415 -3.585,8 -8,8c-4.415,0 -8,-3.585 -8,-8c0,-4.415 3.585,-8 8,-8Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</StatusOfflineIcon>
|
||||
</Memo(StatusIcon)>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
</ProfilePicture>
|
||||
</div>
|
||||
<div
|
||||
@@ -575,161 +431,89 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
|
||||
>
|
||||
<ProfilePicture
|
||||
channelId="channel_id"
|
||||
hasMention={false}
|
||||
isEmoji={false}
|
||||
popoverPlacement="right"
|
||||
size="md"
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
status="status"
|
||||
userId="user_id"
|
||||
username="username"
|
||||
wrapperClass=""
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(ProfilePopover)
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<ProfilePopoverController
|
||||
channelId="channel_id"
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
triggerComponentClass="status-wrapper"
|
||||
userId="user_id"
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<OverlayWrapper
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
}
|
||||
}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<span
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
className="status-wrapper"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPointerDown={[Function]}
|
||||
>
|
||||
<span
|
||||
className="status-wrapper "
|
||||
onClick={[Function]}
|
||||
<RoundButton
|
||||
className="style--none"
|
||||
size="md"
|
||||
>
|
||||
<RoundButton
|
||||
className="style--none"
|
||||
<button
|
||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||
size="md"
|
||||
>
|
||||
<button
|
||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||
size="md"
|
||||
<span
|
||||
className="profile-icon "
|
||||
>
|
||||
<span
|
||||
className="profile-icon "
|
||||
<Memo(Avatar)
|
||||
size="md"
|
||||
url="/api/v4/users/user_id/image?_=0"
|
||||
username="username"
|
||||
>
|
||||
<Memo(Avatar)
|
||||
size="md"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/user_id/image?_=0"
|
||||
username="username"
|
||||
>
|
||||
<img
|
||||
alt="username profile image"
|
||||
className="Avatar Avatar-md"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
</button>
|
||||
</RoundButton>
|
||||
<Memo(StatusIcon)
|
||||
status="status"
|
||||
<img
|
||||
alt="username profile image"
|
||||
className="Avatar Avatar-md"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
</button>
|
||||
</RoundButton>
|
||||
<Memo(StatusIcon)
|
||||
status="status"
|
||||
>
|
||||
<StatusOfflineIcon
|
||||
className="status "
|
||||
>
|
||||
<StatusOfflineIcon
|
||||
<span
|
||||
className="status "
|
||||
>
|
||||
<span
|
||||
className="status "
|
||||
>
|
||||
<svg
|
||||
aria-label="Offline Icon"
|
||||
className="offline--icon"
|
||||
height="100%"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"clipRule": "evenodd",
|
||||
"fillRule": "evenodd",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeMiterlimit": 1.41421,
|
||||
}
|
||||
<svg
|
||||
aria-label="Offline Icon"
|
||||
className="offline--icon"
|
||||
height="100%"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"clipRule": "evenodd",
|
||||
"fillRule": "evenodd",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeMiterlimit": 1.41421,
|
||||
}
|
||||
viewBox="0 0 20 20"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M10,0c5.519,0 10,4.481 10,10c0,5.519 -4.481,10 -10,10c-5.519,0 -10,-4.481 -10,-10c0,-5.519 4.481,-10 10,-10Zm0,2c4.415,0 8,3.585 8,8c0,4.415 -3.585,8 -8,8c-4.415,0 -8,-3.585 -8,-8c0,-4.415 3.585,-8 8,-8Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</StatusOfflineIcon>
|
||||
</Memo(StatusIcon)>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</OverlayTrigger>
|
||||
}
|
||||
viewBox="0 0 20 20"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M10,0c5.519,0 10,4.481 10,10c0,5.519 -4.481,10 -10,10c-5.519,0 -10,-4.481 -10,-10c0,-5.519 4.481,-10 10,-10Zm0,2c4.415,0 8,3.585 8,8c0,4.415 -3.585,8 -8,8c-4.415,0 -8,-3.585 -8,-8c0,-4.415 3.585,-8 8,-8Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</StatusOfflineIcon>
|
||||
</Memo(StatusIcon)>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
</ProfilePicture>
|
||||
</div>
|
||||
<div
|
||||
@@ -912,161 +696,89 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
|
||||
>
|
||||
<ProfilePicture
|
||||
channelId="channel_id"
|
||||
hasMention={false}
|
||||
isEmoji={false}
|
||||
popoverPlacement="right"
|
||||
size="md"
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
status="status"
|
||||
userId="user_id"
|
||||
username="username"
|
||||
wrapperClass=""
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(ProfilePopover)
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<ProfilePopoverController
|
||||
channelId="channel_id"
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
triggerComponentClass="status-wrapper"
|
||||
userId="user_id"
|
||||
>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<OverlayWrapper
|
||||
channelId="channel_id"
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
}
|
||||
}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<span
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
className="status-wrapper"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPointerDown={[Function]}
|
||||
>
|
||||
<span
|
||||
className="status-wrapper "
|
||||
onClick={[Function]}
|
||||
<RoundButton
|
||||
className="style--none"
|
||||
size="md"
|
||||
>
|
||||
<RoundButton
|
||||
className="style--none"
|
||||
<button
|
||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||
size="md"
|
||||
>
|
||||
<button
|
||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||
size="md"
|
||||
<span
|
||||
className="profile-icon "
|
||||
>
|
||||
<span
|
||||
className="profile-icon "
|
||||
<Memo(Avatar)
|
||||
size="md"
|
||||
url="/api/v4/users/user_id/image?_=0"
|
||||
username="username"
|
||||
>
|
||||
<Memo(Avatar)
|
||||
size="md"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/user_id/image?_=0"
|
||||
username="username"
|
||||
>
|
||||
<img
|
||||
alt="username profile image"
|
||||
className="Avatar Avatar-md"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
</button>
|
||||
</RoundButton>
|
||||
<Memo(StatusIcon)
|
||||
status="status"
|
||||
<img
|
||||
alt="username profile image"
|
||||
className="Avatar Avatar-md"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
</button>
|
||||
</RoundButton>
|
||||
<Memo(StatusIcon)
|
||||
status="status"
|
||||
>
|
||||
<StatusOfflineIcon
|
||||
className="status "
|
||||
>
|
||||
<StatusOfflineIcon
|
||||
<span
|
||||
className="status "
|
||||
>
|
||||
<span
|
||||
className="status "
|
||||
>
|
||||
<svg
|
||||
aria-label="Offline Icon"
|
||||
className="offline--icon"
|
||||
height="100%"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"clipRule": "evenodd",
|
||||
"fillRule": "evenodd",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeMiterlimit": 1.41421,
|
||||
}
|
||||
<svg
|
||||
aria-label="Offline Icon"
|
||||
className="offline--icon"
|
||||
height="100%"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"clipRule": "evenodd",
|
||||
"fillRule": "evenodd",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeMiterlimit": 1.41421,
|
||||
}
|
||||
viewBox="0 0 20 20"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M10,0c5.519,0 10,4.481 10,10c0,5.519 -4.481,10 -10,10c-5.519,0 -10,-4.481 -10,-10c0,-5.519 4.481,-10 10,-10Zm0,2c4.415,0 8,3.585 8,8c0,4.415 -3.585,8 -8,8c-4.415,0 -8,-3.585 -8,-8c0,-4.415 3.585,-8 8,-8Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</StatusOfflineIcon>
|
||||
</Memo(StatusIcon)>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</OverlayTrigger>
|
||||
}
|
||||
viewBox="0 0 20 20"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M10,0c5.519,0 10,4.481 10,10c0,5.519 -4.481,10 -10,10c-5.519,0 -10,-4.481 -10,-10c0,-5.519 4.481,-10 10,-10Zm0,2c4.415,0 8,3.585 8,8c0,4.415 -3.585,8 -8,8c-4.415,0 -8,-3.585 -8,-8c0,-4.415 3.585,-8 8,-8Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</StatusOfflineIcon>
|
||||
</Memo(StatusIcon)>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
</ProfilePicture>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -12,7 +12,6 @@ import UserProfile from 'components/user_profile';
|
||||
import BotTag from 'components/widgets/tag/bot_tag';
|
||||
import Tag from 'components/widgets/tag/tag';
|
||||
|
||||
import {Locations} from 'utils/constants';
|
||||
import {fromAutoResponder, isFromWebhook} from 'utils/post_utils';
|
||||
|
||||
type Props = {
|
||||
@@ -24,7 +23,6 @@ type Props = {
|
||||
isBot: boolean;
|
||||
isSystemMessage: boolean;
|
||||
isMobileView: boolean;
|
||||
location: keyof typeof Locations;
|
||||
};
|
||||
|
||||
const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
@@ -32,7 +30,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
const {post, compactDisplay, isMobileView, isConsecutivePost, enablePostUsernameOverride, isBot, isSystemMessage, colorizeUsernames} = props;
|
||||
const isFromAutoResponder = fromAutoResponder(post);
|
||||
const colorize = compactDisplay && colorizeUsernames;
|
||||
const isRHS = props.location === Locations.RHS_COMMENT || props.location === Locations.RHS_ROOT || props.location === Locations.SEARCH;
|
||||
|
||||
let userProfile: ReactNode = null;
|
||||
let botIndicator = null;
|
||||
@@ -55,7 +52,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
<UserProfile
|
||||
userId={post.user_id}
|
||||
channelId={post.channel_id}
|
||||
isRHS={isRHS}
|
||||
colorize={colorize}
|
||||
/>
|
||||
);
|
||||
@@ -66,7 +62,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
<UserProfile
|
||||
userId={post.user_id}
|
||||
channelId={post.channel_id}
|
||||
isRHS={isRHS}
|
||||
colorize={colorize}
|
||||
/>
|
||||
);
|
||||
@@ -75,7 +70,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
<UserProfile
|
||||
userId={post.user_id}
|
||||
channelId={post.channel_id}
|
||||
isRHS={isRHS}
|
||||
colorize={colorize}
|
||||
/>
|
||||
);
|
||||
@@ -105,7 +99,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
||||
userId={post.user_id}
|
||||
channelId={post.channel_id}
|
||||
hideStatus={true}
|
||||
isRHS={isRHS}
|
||||
colorize={colorize}
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -226,7 +226,6 @@ exports[`components/post_edit_history should match snapshot 1`] = `
|
||||
class="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
src="/api/v4/users/user_id/image?_=0"
|
||||
tabindex="0"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
|
||||
@@ -41,11 +41,10 @@ describe('components/PostProfilePicture', () => {
|
||||
expect(screen.getByLabelText('Offline Icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('status and post icon override specified, default props', () => {
|
||||
test('status is specified, default props', () => {
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
status: 'away',
|
||||
postIconOverrideURL: 'http://example.com/image.png',
|
||||
};
|
||||
renderWithContext(
|
||||
<PostProfilePicture {...props}/>,
|
||||
|
||||
@@ -22,7 +22,6 @@ type Props = {
|
||||
status?: string;
|
||||
user: UserProfile;
|
||||
isBot?: boolean;
|
||||
postIconOverrideURL?: string;
|
||||
overwriteIcon?: string;
|
||||
}
|
||||
|
||||
@@ -93,7 +92,6 @@ export default class PostProfilePicture extends React.PureComponent<Props> {
|
||||
}
|
||||
const fromAutoResponder = PostUtils.fromAutoResponder(post);
|
||||
|
||||
const hasMention = !fromAutoResponder && !fromWebhook;
|
||||
const profileSrc = this.getProfilePictureURL();
|
||||
const src = this.getPostIconURL(profileSrc, fromAutoResponder, fromWebhook);
|
||||
|
||||
@@ -104,7 +102,6 @@ export default class PostProfilePicture extends React.PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<ProfilePicture
|
||||
hasMention={hasMention}
|
||||
size='md'
|
||||
src={src}
|
||||
profileSrc={profileSrc}
|
||||
|
||||
@@ -192,11 +192,11 @@ describe('components/post_view/ChannelIntroMessages', () => {
|
||||
);
|
||||
expect(screen.getByText('This is the start of your direct message history with my teammate.', {exact: false})).toBeInTheDocument();
|
||||
|
||||
const teammate = screen.getByLabelText('my teammate');
|
||||
const teammate = screen.getByText('my teammate');
|
||||
|
||||
expect(teammate).toBeInTheDocument();
|
||||
expect(teammate).toHaveTextContent('my teammate');
|
||||
expect(teammate).toHaveClass('user-popover style--none');
|
||||
expect(teammate).toHaveClass('style--none');
|
||||
|
||||
const image = screen.getByRole('img');
|
||||
|
||||
|
||||
@@ -277,13 +277,11 @@ function createDMIntroMessage(
|
||||
status={teammate.is_bot ? '' : channel.status}
|
||||
userId={teammate?.id}
|
||||
username={teammate?.username}
|
||||
hasMention={true}
|
||||
/>
|
||||
</div>
|
||||
<h2 className='channel-intro__title'>
|
||||
<UserProfile
|
||||
userId={teammate?.id}
|
||||
disablePopover={false}
|
||||
/>
|
||||
</h2>
|
||||
<p className='channel-intro__text'>
|
||||
|
||||
@@ -15,7 +15,6 @@ exports[`components/post_view/CommentedOn should match snapshot 1`] = `
|
||||
className="theme user_name"
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -47,7 +46,6 @@ exports[`components/post_view/CommentedOn should match snapshot 2`] = `
|
||||
className="theme user_name"
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -79,7 +77,6 @@ exports[`components/post_view/CommentedOn should match snapshot 3`] = `
|
||||
className="theme user_name"
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -113,7 +110,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
||||
className="theme user_name"
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -145,7 +141,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
||||
className="theme user_name"
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -177,7 +172,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
||||
className="theme user_name"
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
@@ -209,7 +203,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
||||
className="theme user_name"
|
||||
>
|
||||
<Memo(Connect(UserProfile))
|
||||
disablePopover={false}
|
||||
userId=""
|
||||
/>
|
||||
</a>,
|
||||
|
||||
@@ -44,7 +44,6 @@ function CommentedOn({post, parentPostUser, onCommentClick}: Props) {
|
||||
const parentUserProfile = (
|
||||
<UserProfile
|
||||
userId={parentPostUserId}
|
||||
disablePopover={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`components/ProfilePicture should match snapshot, no user specified, default props 1`] = `
|
||||
<span
|
||||
className="status-wrapper style--none "
|
||||
className="status-wrapper style--none"
|
||||
>
|
||||
<span
|
||||
className="profile-icon "
|
||||
@@ -20,7 +20,7 @@ exports[`components/ProfilePicture should match snapshot, no user specified, def
|
||||
|
||||
exports[`components/ProfilePicture should match snapshot, no user specified, overridden props 1`] = `
|
||||
<span
|
||||
className="status-wrapper style--none "
|
||||
className="status-wrapper style--none"
|
||||
>
|
||||
<span
|
||||
className="profile-icon "
|
||||
@@ -37,51 +37,32 @@ exports[`components/ProfilePicture should match snapshot, no user specified, ove
|
||||
`;
|
||||
|
||||
exports[`components/ProfilePicture should match snapshot, profile and src, default props 1`] = `
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
src="http://example.com/image.png"
|
||||
userId="uid"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<ProfilePopoverController
|
||||
src="http://example.com/image.png"
|
||||
triggerComponentClass="status-wrapper"
|
||||
userId="uid"
|
||||
>
|
||||
<span
|
||||
className="status-wrapper "
|
||||
<RoundButton
|
||||
className="style--none"
|
||||
size="md"
|
||||
>
|
||||
<RoundButton
|
||||
className="style--none"
|
||||
size="md"
|
||||
<span
|
||||
className="profile-icon "
|
||||
>
|
||||
<span
|
||||
className="profile-icon "
|
||||
>
|
||||
<Memo(Avatar)
|
||||
size="md"
|
||||
tabIndex={-1}
|
||||
url="http://example.com/emoji.png"
|
||||
/>
|
||||
</span>
|
||||
</RoundButton>
|
||||
<Memo(StatusIcon)
|
||||
status="away"
|
||||
/>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
<Memo(Avatar)
|
||||
url="http://example.com/emoji.png"
|
||||
/>
|
||||
</span>
|
||||
</RoundButton>
|
||||
<Memo(StatusIcon)
|
||||
status="away"
|
||||
/>
|
||||
</ProfilePopoverController>
|
||||
`;
|
||||
|
||||
exports[`components/ProfilePicture should match snapshot, user specified 1`] = `
|
||||
<span
|
||||
className="status-wrapper style--none "
|
||||
className="status-wrapper style--none"
|
||||
>
|
||||
<span
|
||||
className="profile-icon "
|
||||
@@ -99,7 +80,7 @@ exports[`components/ProfilePicture should match snapshot, user specified 1`] = `
|
||||
|
||||
exports[`components/ProfilePicture should match snapshot, user specified, overridden props 1`] = `
|
||||
<span
|
||||
className="status-wrapper style--none "
|
||||
className="status-wrapper style--none"
|
||||
>
|
||||
<span
|
||||
className="profile-icon "
|
||||
|
||||
101
webapp/channels/src/components/profile_picture/index.tsx
Обычный файл
101
webapp/channels/src/components/profile_picture/index.tsx
Обычный файл
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import ProfilePopover from 'components/profile_popover';
|
||||
import StatusIcon from 'components/status_icon';
|
||||
import StatusIconNew from 'components/status_icon_new';
|
||||
import Avatar, {getAvatarWidth} from 'components/widgets/users/avatar';
|
||||
import type {TAvatarSizeToken} from 'components/widgets/users/avatar';
|
||||
|
||||
type Props = {
|
||||
size?: TAvatarSizeToken;
|
||||
isEmoji?: boolean;
|
||||
wrapperClass?: string;
|
||||
profileSrc?: string;
|
||||
src: string;
|
||||
isBot?: boolean;
|
||||
fromAutoResponder?: boolean;
|
||||
status?: string;
|
||||
fromWebhook?: boolean;
|
||||
userId?: string;
|
||||
channelId?: string;
|
||||
username?: string;
|
||||
overwriteIcon?: string;
|
||||
overwriteName?: string;
|
||||
newStatusIcon?: boolean;
|
||||
statusClass?: string;
|
||||
}
|
||||
|
||||
function ProfilePicture(props: Props) {
|
||||
// profileSrc will, if possible, be the original user profile picture even if the icon
|
||||
// for the post is overriden, so that the popup shows the user identity
|
||||
const profileSrc = typeof props.profileSrc === 'string' && props.profileSrc !== '' ? props.profileSrc : props.src;
|
||||
|
||||
const profileIconClass = `profile-icon ${props.isEmoji ? 'emoji' : ''}`;
|
||||
|
||||
const hideStatus = props.isBot || props.fromAutoResponder || props.fromWebhook;
|
||||
|
||||
if (props.userId) {
|
||||
return (
|
||||
<ProfilePopover
|
||||
triggerComponentClass={classNames('status-wrapper', props.wrapperClass)}
|
||||
userId={props.userId}
|
||||
src={profileSrc}
|
||||
channelId={props.channelId}
|
||||
hideStatus={hideStatus}
|
||||
overwriteIcon={props.overwriteIcon}
|
||||
overwriteName={props.overwriteName}
|
||||
fromWebhook={props.fromWebhook}
|
||||
>
|
||||
<>
|
||||
<RoundButton
|
||||
className='style--none'
|
||||
size={props?.size ?? 'md'}
|
||||
>
|
||||
<span className={profileIconClass}>
|
||||
<Avatar
|
||||
username={props.username}
|
||||
size={props.size}
|
||||
url={props.src}
|
||||
/>
|
||||
</span>
|
||||
</RoundButton>
|
||||
<StatusIcon status={props.status}/>
|
||||
</>
|
||||
</ProfilePopover>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames('status-wrapper', 'style--none', props.wrapperClass)}
|
||||
>
|
||||
<span className={profileIconClass}>
|
||||
<Avatar
|
||||
size={props?.size ?? 'md'}
|
||||
url={props.src}
|
||||
/>
|
||||
</span>
|
||||
{props.newStatusIcon ? (
|
||||
<StatusIconNew
|
||||
className={props.statusClass}
|
||||
status={props.status}
|
||||
/>
|
||||
) : (
|
||||
<StatusIcon status={props.status}/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const RoundButton = styled.button<{size: TAvatarSizeToken}>`
|
||||
border-radius: 50%;
|
||||
width: ${(p) => getAvatarWidth(p.size)}px;
|
||||
height: ${(p) => getAvatarWidth(p.size)}px;
|
||||
`;
|
||||
|
||||
export default ProfilePicture;
|
||||
@@ -1,3 +0,0 @@
|
||||
.ProfilePicture {
|
||||
border-radius: 50%;
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import type {ComponentProps} from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import type {BaseOverlayTrigger} from 'components/overlay_trigger';
|
||||
import ProfilePopover from 'components/profile_popover';
|
||||
import StatusIcon from 'components/status_icon';
|
||||
import StatusIconNew from 'components/status_icon_new';
|
||||
import Avatar, {getAvatarWidth} from 'components/widgets/users/avatar';
|
||||
import type {TAvatarSizeToken} from 'components/widgets/users/avatar';
|
||||
|
||||
import './profile_picture.scss';
|
||||
|
||||
interface MMOverlayTrigger extends BaseOverlayTrigger {
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
isEmoji?: boolean;
|
||||
profileSrc?: string;
|
||||
size?: ComponentProps<typeof Avatar>['size'];
|
||||
src: string;
|
||||
status?: string;
|
||||
userId?: string;
|
||||
channelId?: string;
|
||||
username?: string;
|
||||
wrapperClass?: string;
|
||||
overwriteIcon?: string;
|
||||
overwriteName?: string;
|
||||
newStatusIcon?: boolean;
|
||||
statusClass?: string;
|
||||
isBot?: boolean;
|
||||
fromWebhook?: boolean;
|
||||
fromAutoResponder?: boolean;
|
||||
popoverPlacement?: string;
|
||||
}
|
||||
|
||||
export default class ProfilePicture extends React.PureComponent<Props> {
|
||||
public static defaultProps = {
|
||||
size: 'md',
|
||||
isEmoji: false,
|
||||
hasMention: false,
|
||||
wrapperClass: '',
|
||||
popoverPlacement: 'right',
|
||||
};
|
||||
|
||||
overlay = React.createRef<MMOverlayTrigger>();
|
||||
buttonRef = React.createRef<HTMLButtonElement>();
|
||||
|
||||
public hideProfilePopover = () => {
|
||||
if (this.overlay.current) {
|
||||
this.overlay.current.hide();
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
// profileSrc will, if possible, be the original user profile picture even if the icon
|
||||
// for the post is overriden, so that the popup shows the user identity
|
||||
const profileSrc = (typeof this.props.profileSrc === 'string' && this.props.profileSrc !== '') ? this.props.profileSrc : this.props.src;
|
||||
|
||||
const profileIconClass = `profile-icon ${this.props.isEmoji ? 'emoji' : ''}`;
|
||||
|
||||
const hideStatus = this.props.isBot || this.props.fromAutoResponder || this.props.fromWebhook;
|
||||
|
||||
if (this.props.userId) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
ref={this.overlay}
|
||||
trigger={['click']}
|
||||
placement={this.props.popoverPlacement}
|
||||
rootClose={true}
|
||||
overlay={
|
||||
<ProfilePopover
|
||||
className='user-profile-popover'
|
||||
userId={this.props.userId}
|
||||
src={profileSrc}
|
||||
hide={this.hideProfilePopover}
|
||||
channelId={this.props.channelId}
|
||||
overwriteIcon={this.props.overwriteIcon}
|
||||
overwriteName={this.props.overwriteName}
|
||||
fromWebhook={this.props.fromWebhook}
|
||||
hideStatus={hideStatus}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span className={`status-wrapper ${this.props.wrapperClass}`}>
|
||||
<RoundButton
|
||||
className='style--none'
|
||||
size={this.props.size ?? 'md'}
|
||||
ref={this.buttonRef}
|
||||
>
|
||||
<span className={profileIconClass}>
|
||||
|
||||
<Avatar
|
||||
username={this.props.username}
|
||||
size={this.props.size}
|
||||
url={this.props.src}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</span>
|
||||
</RoundButton>
|
||||
|
||||
<StatusIcon status={this.props.status}/>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className={`status-wrapper style--none ${this.props.wrapperClass}`}>
|
||||
<span className={profileIconClass}>
|
||||
<Avatar
|
||||
size={this.props.size}
|
||||
url={this.props.src}
|
||||
/>
|
||||
</span>
|
||||
{this.props.newStatusIcon ? (
|
||||
<StatusIconNew
|
||||
className={this.props.statusClass}
|
||||
status={this.props.status}
|
||||
/>
|
||||
) : <StatusIcon status={this.props.status}/>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RoundButton = styled.button<{size: TAvatarSizeToken}>`
|
||||
border-radius: 50%;
|
||||
|
||||
width: ${(p) => getAvatarWidth(p.size)}px;
|
||||
height: ${(p) => getAvatarWidth(p.size)}px;
|
||||
`;
|
||||
@@ -1,4 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {default} from './profile_picture';
|
||||
import {ProfilePopoverController} from './profile_popover_controller';
|
||||
|
||||
export default ProfilePopoverController;
|
||||
@@ -1,335 +0,0 @@
|
||||
// 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 {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();
|
||||
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
|
||||
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,139 +1,226 @@
|
||||
@import 'utils/mixins';
|
||||
|
||||
.user-popover__set-custom-status-btn {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--button-bg);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
gap: 6px;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.user-profile-popover__heading {
|
||||
font-family: Metropolis, sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-popover-last-active {
|
||||
display: block;
|
||||
margin: 8px 0 4px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
.user-profile-popover-floating-overlay {
|
||||
// 99 being the z-index of the global header
|
||||
// 1060 being the z-index of the user group popover
|
||||
z-index: 1070;
|
||||
}
|
||||
|
||||
.user-profile-popover {
|
||||
width: 240px;
|
||||
max-height: calc(100vh - 240px);
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 4px;
|
||||
background: var(--center-channel-bg);
|
||||
box-shadow: var(--elevation-4);
|
||||
overflow-y: auto;
|
||||
|
||||
.popover-title {
|
||||
overflow: unset;
|
||||
max-width: 110%;
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
margin-top: 8px;
|
||||
margin-right: -8px;
|
||||
|
||||
&.popover-title_height {
|
||||
overflow: visible;
|
||||
height: 22px;
|
||||
}
|
||||
.user-profile-popover-title {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
padding: 12px 8px 12px 8px;
|
||||
|
||||
.user-popover__role {
|
||||
padding: 0 4px;
|
||||
margin-left: 0;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
color: var(--center-channel-color-rgb);
|
||||
font-family: Open Sans, sans-serif;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 16px;
|
||||
flex-grow: 1;
|
||||
background: unset;
|
||||
padding-inline-start: 8px;
|
||||
|
||||
> span {
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
color: var(--center-channel-color-rgb);
|
||||
font-family: Open Sans, sans-serif;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.2px;
|
||||
line-height: 16px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.closeButtonRelativePosition {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 8px 8px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
padding: 0 16px;
|
||||
.user-profile-popover-content {
|
||||
.user-popover-image {
|
||||
position: relative;
|
||||
width: 128px;
|
||||
margin: 0 auto 8px auto;
|
||||
|
||||
.overflow--ellipsis {
|
||||
#userAvatar {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.user-popover-status {
|
||||
position: absolute;
|
||||
top: auto;
|
||||
right: 8px;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 2px;
|
||||
border-radius: 50px;
|
||||
background: rgba(var(--center-channel-bg-rgb), 1);
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-popover-last-active {
|
||||
display: block;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
font-family: "Open Sans";
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 16px;
|
||||
margin-block-end: 8px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-popover__subtitle {
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
.user-profile-popover__heading {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: 16px;
|
||||
text-align: center;
|
||||
|
||||
> h5 {
|
||||
margin:0;
|
||||
font-family: Metropolis, sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
@include textEllipsis;
|
||||
}
|
||||
|
||||
i.shared-user-icon {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-profile-popover__non-heading {
|
||||
font-family: "Open Sans";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
margin-block-end: 4px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: 16px;
|
||||
text-align: center;
|
||||
|
||||
@include textEllipsis;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
margin-block-end: 16px;
|
||||
margin-block-start: 16px;
|
||||
|
||||
&.user-popover__bottom-row-hr {
|
||||
margin-block-start: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-profile-popover__email {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 16px 10px 16px;
|
||||
justify-self: start;
|
||||
|
||||
i.icon-email-outline {
|
||||
margin-inline-end: 8px;
|
||||
|
||||
&::before {
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-family: "Open Sans";
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
@include textEllipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.user-popover__custom-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: "Open Sans";
|
||||
font-size: 14px;
|
||||
|
||||
span.emoticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-popover__time-status-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 16px 12px 16px;
|
||||
|
||||
@include textEllipsis;
|
||||
|
||||
.user-popover__subtitle {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
gap: 4px;
|
||||
line-height: 16px;
|
||||
@include textEllipsis;
|
||||
}
|
||||
|
||||
.user-popover__subtitle-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-popover__set-status {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.popover__row {
|
||||
.user-popover__bottom-row-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
padding: 0 16px 16px 16px;
|
||||
margin: 0;
|
||||
gap: 4px;
|
||||
row-gap: 8px;
|
||||
|
||||
&.first {
|
||||
border-top-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
|
||||
.btn {
|
||||
@include primary-button;
|
||||
|
||||
height: 32px;
|
||||
padding: 8px 12px;
|
||||
gap: 6px;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
background-color: unset;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
|
||||
&:hover {
|
||||
border-radius: 4px;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-btn-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.popover_row-controlContainer {
|
||||
.user-popover__bottom-row-end {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.callButtonContainer {
|
||||
height: unset;
|
||||
margin-top: 0;
|
||||
}
|
||||
gap: 4px;
|
||||
justify-items: flex-end;
|
||||
row-gap: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {getDirectChannelName} from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ProfilePopover from '.';
|
||||
import ProfilePopover from './profile_popover';
|
||||
|
||||
jest.mock('@mattermost/client', () => ({
|
||||
...jest.requireActual('@mattermost/client'),
|
||||
242
webapp/channels/src/components/profile_popover/profile_popover.tsx
Обычный файл
242
webapp/channels/src/components/profile_popover/profile_popover.tsx
Обычный файл
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {getCurrentChannelId, getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
|
||||
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 {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 Pluggable from 'plugins/pluggable';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {A11yCustomEventTypes, UserStatuses} from 'utils/constants';
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ProfilePopoverAvatar from './profile_popover_avatar';
|
||||
import ProfilePopoverCustomStatus from './profile_popover_custom_status';
|
||||
import ProfilePopoverEmail from './profile_popover_email';
|
||||
import ProfilePopoverLastActive from './profile_popover_last_active';
|
||||
import ProfilePopoverName from './profile_popover_name';
|
||||
import ProfilePopoverOtherUserRow from './profile_popover_other_user_row';
|
||||
import ProfilePopoverOverrideDisclaimer from './profile_popover_override_disclaimer';
|
||||
import ProfilePopoverSelfUserRow from './profile_popover_self_user_row';
|
||||
import ProfilePopoverTimezone from './profile_popover_timezone';
|
||||
import ProfilePopoverTitle from './profile_popover_title';
|
||||
|
||||
import './profile_popover.scss';
|
||||
|
||||
export interface Props {
|
||||
userId: string;
|
||||
src: string;
|
||||
channelId?: string;
|
||||
hideStatus?: boolean;
|
||||
fromWebhook?: boolean;
|
||||
hide?: () => void;
|
||||
returnFocus?: () => void;
|
||||
overwriteIcon?: string;
|
||||
overwriteName?: string;
|
||||
}
|
||||
|
||||
function getDefaultChannelId(state: GlobalState) {
|
||||
const selectedPost = getSelectedPost(state);
|
||||
return selectedPost.exists ? selectedPost.channel_id : getCurrentChannelId(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* The profile popover, or hover card, that appears with user information when clicking
|
||||
* on the username, profile picture of a user, or others.
|
||||
* However this component should not be used directly, instead use the `ProfilePopoverController` which is
|
||||
* what is default exported from 'components/profile_popover'.
|
||||
*/
|
||||
const ProfilePopover = ({
|
||||
userId,
|
||||
src,
|
||||
channelId: channelIdProp,
|
||||
hideStatus,
|
||||
fromWebhook,
|
||||
hide,
|
||||
returnFocus,
|
||||
overwriteIcon,
|
||||
overwriteName,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector((state: GlobalState) => getUser(state, userId));
|
||||
const currentTeamId = useSelector((state: GlobalState) => getCurrentTeamId(state));
|
||||
const channelId = useSelector((state: GlobalState) => (channelIdProp || getDefaultChannelId(state)));
|
||||
const isMobileView = useSelector(getIsMobileView);
|
||||
const teamUrl = useSelector(getCurrentRelativeTeamUrl);
|
||||
const modals = useSelector((state: GlobalState) => state.views.modals);
|
||||
const status = useSelector((state: GlobalState) => getStatusForUserId(state, userId) || UserStatuses.OFFLINE);
|
||||
const currentUserTimezone = useSelector(getCurrentTimezone);
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
|
||||
const [loadingDMChannel, setLoadingDMChannel] = useState<string>();
|
||||
|
||||
const handleReturnFocus = useMemo(() => {
|
||||
if (returnFocus) {
|
||||
return returnFocus;
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTeamId && userId) {
|
||||
dispatch(getMembershipForEntities(
|
||||
currentTeamId,
|
||||
userId,
|
||||
channelId,
|
||||
));
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlSrc = overwriteIcon || src;
|
||||
const haveOverrideProp = Boolean(overwriteIcon || overwriteName);
|
||||
const fullname = overwriteName || Utils.getFullName(user);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProfilePopoverTitle
|
||||
channelId={channelId}
|
||||
isBot={user.is_bot}
|
||||
returnFocus={handleReturnFocus}
|
||||
roles={user.roles}
|
||||
userId={user.id}
|
||||
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/>
|
||||
<ProfilePopoverEmail
|
||||
email={user.email}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
isBot={user.is_bot}
|
||||
/>
|
||||
<Pluggable
|
||||
pluggableName='PopoverUserAttributes'
|
||||
user={user}
|
||||
hide={hide}
|
||||
status={hideStatus ? null : status}
|
||||
fromWebhook={fromWebhook}
|
||||
/>
|
||||
<ProfilePopoverTimezone
|
||||
currentUserTimezone={currentUserTimezone}
|
||||
profileUserTimezone={user.timezone}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
/>
|
||||
<ProfilePopoverCustomStatus
|
||||
currentUserId={currentUserId}
|
||||
currentUserTimezone={currentUserTimezone}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
hideStatus={hideStatus}
|
||||
user={user}
|
||||
returnFocus={handleReturnFocus}
|
||||
hide={hide}
|
||||
/>
|
||||
<hr className='user-popover__bottom-row-hr'/>
|
||||
<ProfilePopoverOverrideDisclaimer
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
username={user.username}
|
||||
/>
|
||||
<ProfilePopoverSelfUserRow
|
||||
currentUserId={currentUserId}
|
||||
handleCloseModals={handleCloseModals}
|
||||
handleShowDirectChannel={handleShowDirectChannel}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
returnFocus={handleReturnFocus}
|
||||
userId={user.id}
|
||||
hide={hide}
|
||||
/>
|
||||
<ProfilePopoverOtherUserRow
|
||||
currentUserId={currentUserId}
|
||||
fullname={fullname}
|
||||
handleCloseModals={handleCloseModals}
|
||||
handleShowDirectChannel={handleShowDirectChannel}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
returnFocus={handleReturnFocus}
|
||||
user={user}
|
||||
hide={hide}
|
||||
/>
|
||||
<Pluggable
|
||||
pluggableName='PopoverUserActions'
|
||||
user={user}
|
||||
hide={hide}
|
||||
status={hideStatus ? null : status}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ProfilePopover);
|
||||
@@ -5,18 +5,16 @@ 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 WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
@@ -33,7 +31,7 @@ function getIsInCurrentTeam(state: GlobalState, userId: string) {
|
||||
return Boolean(teamMember) && teamMember?.delete_at === 0;
|
||||
}
|
||||
|
||||
const AddToChannel = ({
|
||||
const ProfilePopoverAddToChannel = ({
|
||||
handleCloseModals,
|
||||
returnFocus,
|
||||
user,
|
||||
@@ -52,42 +50,38 @@ const AddToChannel = ({
|
||||
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}
|
||||
<WithTooltip
|
||||
id='user_profile.add_user_to_channel.icon'
|
||||
title={formatMessage({
|
||||
id: 'user_profile.add_user_to_channel',
|
||||
defaultMessage: 'Add to a Channel',
|
||||
})}
|
||||
placement='top'
|
||||
overlay={
|
||||
<Tooltip id='addToChannelTooltip'>
|
||||
{addToChannelMessage}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{/* This span is necessary as tooltip is not able to pass trigger props to a custom component */}
|
||||
<span>
|
||||
<ToggleModalButton
|
||||
id='addToChannelButton'
|
||||
className='btn icon-btn'
|
||||
ariaLabel={addToChannelMessage}
|
||||
className='btn btn-icon btn-sm'
|
||||
ariaLabel={formatMessage({
|
||||
id: 'user_profile.add_user_to_channel',
|
||||
defaultMessage: 'Add to a Channel',
|
||||
})}
|
||||
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',
|
||||
})}
|
||||
<i
|
||||
className='icon icon-account-plus-outline'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</span>
|
||||
</WithTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddToChannel;
|
||||
export default ProfilePopoverAddToChannel;
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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';
|
||||
|
||||
type Props = {
|
||||
botDescription: UserProfile['bot_description'];
|
||||
}
|
||||
|
||||
const ProfilePopoverBotDescription = ({
|
||||
botDescription,
|
||||
}: Props) => {
|
||||
return (
|
||||
<p
|
||||
className='user-profile-popover__non-heading'
|
||||
title={botDescription}
|
||||
>
|
||||
{botDescription}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverBotDescription;
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {isUserInCall} from './call_button';
|
||||
import {isUserInCall} from './index';
|
||||
|
||||
describe('isUserInCall', () => {
|
||||
test('missing state', () => {
|
||||
@@ -14,7 +14,7 @@ import {getChannelByName} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {isCallsEnabled as getIsCallsEnabled, getSessionsInCalls} from 'selectors/calls';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import ProfilePopoverCallButton from 'components/profile_popover_call_button';
|
||||
import ProfilePopoverCallButton from 'components/profile_popover/profile_popover_calls_button';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
@@ -98,7 +98,7 @@ const CallButton = ({
|
||||
id: 'webapp.mattermost.feature.start_call',
|
||||
defaultMessage: 'Start Call',
|
||||
});
|
||||
const iconButtonClassname = classNames('btn icon-btn', {'icon-btn-disabled': disabled});
|
||||
const iconButtonClassname = classNames('style--none btn btn-icon btn-sm', {'icon-btn-disabled': disabled});
|
||||
const callButton = (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
@@ -0,0 +1,6 @@
|
||||
button#startCallButton {
|
||||
&.btn-disabled-styled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,6 @@ export default function ProfilePopoverCallButton({pluginCallComponents, channelM
|
||||
|
||||
return (
|
||||
<div
|
||||
className='callButtonContainer flex-child'
|
||||
onClick={clickHandler}
|
||||
onTouchEnd={clickHandler}
|
||||
>
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
useFloating,
|
||||
autoUpdate,
|
||||
autoPlacement,
|
||||
useTransitionStyles,
|
||||
useClick,
|
||||
useDismiss,
|
||||
useInteractions,
|
||||
useRole,
|
||||
FloatingFocusManager,
|
||||
FloatingOverlay,
|
||||
FloatingPortal,
|
||||
} from '@floating-ui/react';
|
||||
import classNames from 'classnames';
|
||||
import type {HtmlHTMLAttributes, ReactNode} from 'react';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {A11yClassNames} from 'utils/constants';
|
||||
|
||||
import ProfilePopover from './profile_popover';
|
||||
|
||||
const PROFILE_POPOVER_OPENING_DELAY = 300;
|
||||
const PROFILE_POPOVER_CLOSING_DELAY = 500;
|
||||
|
||||
interface Props<TriggerComponentType> {
|
||||
|
||||
/**
|
||||
* The Props for the trigger component
|
||||
*/
|
||||
triggerComponentAs?: React.ElementType;
|
||||
triggerComponentId?: HtmlHTMLAttributes<TriggerComponentType>['id'];
|
||||
triggerComponentClass?: HtmlHTMLAttributes<TriggerComponentType>['className'];
|
||||
triggerComponentStyle?: HtmlHTMLAttributes<TriggerComponentType>['style'];
|
||||
|
||||
/**
|
||||
* Source URL from the image to display in the popover
|
||||
*/
|
||||
src: string;
|
||||
|
||||
/**
|
||||
* This should be the trigger button for the popover, Do note that the root element of the trigger component should be passed in triggerComponentRoot
|
||||
*/
|
||||
children: ReactNode;
|
||||
userId: UserProfile['id'];
|
||||
channelId?: Channel['id'];
|
||||
|
||||
/**
|
||||
* The overwritten username that should be shown at the top of the popover
|
||||
*/
|
||||
overwriteName?: 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;
|
||||
hideStatus?: boolean;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
onToggle?: (isMounted: boolean) => void;
|
||||
}
|
||||
|
||||
export function ProfilePopoverController<TriggerComponentType = HTMLSpanElement>(props: Props<TriggerComponentType>) {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const {refs, floatingStyles, context: floatingContext} = useFloating({
|
||||
open: isOpen,
|
||||
onOpenChange: setOpen,
|
||||
whileElementsMounted: autoUpdate,
|
||||
middleware: [autoPlacement()],
|
||||
});
|
||||
|
||||
const {isMounted, styles: transitionStyles} = useTransitionStyles(floatingContext, {
|
||||
duration: {
|
||||
open: PROFILE_POPOVER_OPENING_DELAY,
|
||||
close: PROFILE_POPOVER_CLOSING_DELAY,
|
||||
},
|
||||
});
|
||||
|
||||
const combinedFloatingStyles = Object.assign({}, floatingStyles, transitionStyles);
|
||||
|
||||
const clickInteractions = useClick(floatingContext);
|
||||
|
||||
const dismissInteraction = useDismiss(floatingContext);
|
||||
|
||||
const role = useRole(floatingContext);
|
||||
|
||||
const {getReferenceProps, getFloatingProps} = useInteractions([
|
||||
clickInteractions,
|
||||
dismissInteraction,
|
||||
role,
|
||||
]);
|
||||
|
||||
const handleHide = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
const TriggerComponent = props.triggerComponentAs ?? 'span';
|
||||
|
||||
return (
|
||||
<>
|
||||
<TriggerComponent
|
||||
id={props.triggerComponentId}
|
||||
ref={refs.setReference}
|
||||
className={props.triggerComponentClass}
|
||||
style={props.triggerComponentStyle}
|
||||
{...getReferenceProps()}
|
||||
>
|
||||
{props.children}
|
||||
</TriggerComponent>
|
||||
|
||||
{isMounted && (
|
||||
<FloatingPortal id='user-profile-popover-portal'>
|
||||
<FloatingOverlay
|
||||
id='user-profile-popover-floating-overlay'
|
||||
className='user-profile-popover-floating-overlay'
|
||||
lockScroll={true}
|
||||
>
|
||||
<FloatingFocusManager context={floatingContext}>
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
style={combinedFloatingStyles}
|
||||
className={classNames('user-profile-popover', A11yClassNames.POPUP)}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<ProfilePopover
|
||||
userId={props.userId}
|
||||
src={props.src}
|
||||
channelId={props.channelId}
|
||||
hideStatus={props.hideStatus}
|
||||
fromWebhook={props.fromWebhook}
|
||||
hide={handleHide}
|
||||
returnFocus={props.returnFocus}
|
||||
overwriteIcon={props.overwriteIcon}
|
||||
overwriteName={props.overwriteName}
|
||||
/>
|
||||
</div>
|
||||
</FloatingFocusManager>
|
||||
</FloatingOverlay>
|
||||
</FloatingPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
|
||||
@@ -32,8 +31,9 @@ type Props = {
|
||||
}
|
||||
|
||||
const emojiStyles: React.CSSProperties = {
|
||||
marginRight: 4,
|
||||
marginTop: 1,
|
||||
marginRight: 8,
|
||||
width: 16,
|
||||
height: 16,
|
||||
};
|
||||
const ProfilePopoverCustomStatus = ({
|
||||
currentUserId,
|
||||
@@ -72,7 +72,10 @@ const ProfilePopoverCustomStatus = ({
|
||||
let customStatusContent;
|
||||
if (customStatusSet) {
|
||||
customStatusContent = (
|
||||
<div className='d-flex align-items-center'>
|
||||
<div
|
||||
className='user-popover__custom-status'
|
||||
aria-labelledby='user-popover__custom-status-title'
|
||||
>
|
||||
<CustomStatusEmoji
|
||||
userID={user.id}
|
||||
showTooltip={false}
|
||||
@@ -81,17 +84,16 @@ const ProfilePopoverCustomStatus = ({
|
||||
<CustomStatusText
|
||||
tooltipDirection='top'
|
||||
text={customStatus.text || ''}
|
||||
className='user-popover__email'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (canSetCustomStatus) {
|
||||
customStatusContent = (
|
||||
<button
|
||||
className='user-popover__set-custom-status-btn'
|
||||
className='btn btn-sm btn-quaternary user-popover__set-status'
|
||||
onClick={showCustomStatusModal}
|
||||
>
|
||||
<EmoticonHappyOutlineIcon size={14}/>
|
||||
<i className='icon icon-emoticon-plus-outline'/>
|
||||
<FormattedMessage
|
||||
id='user_profile.custom_status.set_status'
|
||||
defaultMessage='Set a status'
|
||||
@@ -105,7 +107,10 @@ const ProfilePopoverCustomStatus = ({
|
||||
id='user-popover-status'
|
||||
className='user-popover__time-status-container'
|
||||
>
|
||||
<span className='user-popover__subtitle'>
|
||||
<strong
|
||||
id='user-popover__custom-status-title'
|
||||
className='user-popover__subtitle'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='user_profile.custom_status'
|
||||
defaultMessage='Status'
|
||||
@@ -114,11 +119,10 @@ const ProfilePopoverCustomStatus = ({
|
||||
<ExpiryTime
|
||||
time={customStatus.expires_at!} // has to be defined since showExpiryTime is true
|
||||
timezone={currentUserTimezone}
|
||||
className='ml-1'
|
||||
withinBrackets={true}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</strong>
|
||||
{customStatusContent}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
haveOverrideProp: boolean;
|
||||
isBot: boolean;
|
||||
email?: string;
|
||||
haveOverrideProp?: boolean;
|
||||
isBot?: boolean;
|
||||
}
|
||||
const ProfilePopoverEmail = ({
|
||||
email,
|
||||
@@ -16,14 +16,19 @@ const ProfilePopoverEmail = ({
|
||||
if (!email || isBot || haveOverrideProp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
title={email}
|
||||
className='user-profile-popover__email'
|
||||
|
||||
>
|
||||
<i
|
||||
className='icon icon-email-outline'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<a
|
||||
href={'mailto:' + email}
|
||||
className='text-nowrap text-lowercase user-popover__email pb-1'
|
||||
>
|
||||
{email}
|
||||
</a>
|
||||
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
username: string;
|
||||
remoteId?: string;
|
||||
}
|
||||
const FullName = ({
|
||||
const ProfilePopoverFullName = ({
|
||||
fullname,
|
||||
username,
|
||||
remoteId,
|
||||
@@ -19,7 +19,7 @@ const FullName = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
let sharedIcon;
|
||||
let sharedIcon = null;
|
||||
if (remoteId) {
|
||||
sharedIcon = (
|
||||
<SharedUserIndicator
|
||||
@@ -29,14 +29,16 @@ const FullName = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid={`popover-fullname-${username}`}
|
||||
className='overflow--ellipsis pb-1'
|
||||
className='user-profile-popover__heading'
|
||||
>
|
||||
<span className='user-profile-popover__heading'>{fullname}</span>
|
||||
<h5 title={fullname}>{fullname}</h5>
|
||||
{sharedIcon}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default FullName;
|
||||
|
||||
export default ProfilePopoverFullName;
|
||||
@@ -5,10 +5,10 @@ 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';
|
||||
import BotDescription from 'components/profile_popover/profile_popover_bot_description';
|
||||
import FullName from 'components/profile_popover/profile_popover_full_name';
|
||||
import Position from 'components/profile_popover/profile_popover_position';
|
||||
import UserName from 'components/profile_popover/profile_popover_user_name';
|
||||
|
||||
type Props = {
|
||||
haveOverrideProp: boolean;
|
||||
@@ -27,19 +27,20 @@ const ProfilePopoverName = ({
|
||||
remoteId={user.remote_id}
|
||||
username={user.username}
|
||||
/>
|
||||
<BotDescription
|
||||
botDescription={user.bot_description}
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
isBot={user.is_bot}
|
||||
/>
|
||||
{(user.is_bot && !haveOverrideProp) && (
|
||||
<BotDescription
|
||||
botDescription={user.bot_description}
|
||||
/>
|
||||
)}
|
||||
<UserName
|
||||
hasFullName={Boolean(fullname)}
|
||||
username={user.username}
|
||||
/>
|
||||
<Position
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
position={user.position}
|
||||
/>
|
||||
{(user.position && !haveOverrideProp) && (
|
||||
<Position
|
||||
position={user.position}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,30 +0,0 @@
|
||||
// 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;
|
||||
@@ -2,13 +2,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {FormattedMessage} 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';
|
||||
import ProfilePopoverAddToChannel from 'components/profile_popover/profile_popover_add_to_channel';
|
||||
import ProfilePopoverCallButtonWrapper from 'components/profile_popover/profile_popover_call_button_wrapper';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
@@ -21,7 +20,7 @@ type Props = {
|
||||
hide?: () => void;
|
||||
};
|
||||
|
||||
const ProfilePopoverActions = ({
|
||||
const ProfilePopoverOtherUserRow = ({
|
||||
currentUserId,
|
||||
haveOverrideProp,
|
||||
user,
|
||||
@@ -31,42 +30,34 @@ const ProfilePopoverActions = ({
|
||||
hide,
|
||||
fullname,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
if (user.id === currentUserId || haveOverrideProp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
className='popover__row first'
|
||||
>
|
||||
<div className='user-popover__bottom-row-container'>
|
||||
<button
|
||||
id='messageButton'
|
||||
type='button'
|
||||
className='btn'
|
||||
className='btn btn-primary btn-sm'
|
||||
onClick={handleShowDirectChannel}
|
||||
>
|
||||
<SendIcon
|
||||
size={16}
|
||||
aria-label={formatMessage({
|
||||
id: 'user_profile.send.dm.icon',
|
||||
defaultMessage: 'Send Message Icon',
|
||||
})}
|
||||
<i
|
||||
className='icon icon-send'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='user_profile.send.dm'
|
||||
defaultMessage='Message'
|
||||
/>
|
||||
</button>
|
||||
<div className='popover_row-controlContainer'>
|
||||
<AddToChannel
|
||||
<div className='user-popover__bottom-row-end'>
|
||||
<ProfilePopoverAddToChannel
|
||||
handleCloseModals={handleCloseModals}
|
||||
returnFocus={returnFocus}
|
||||
user={user}
|
||||
hide={hide}
|
||||
/>
|
||||
<CallButton
|
||||
<ProfilePopoverCallButtonWrapper
|
||||
currentUserId={currentUserId}
|
||||
fullname={fullname}
|
||||
userId={user.id}
|
||||
@@ -77,4 +68,4 @@ const ProfilePopoverActions = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverActions;
|
||||
export default ProfilePopoverOtherUserRow;
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
type Props = {
|
||||
username: string;
|
||||
@@ -12,23 +12,24 @@ const ProfilePopoverOverrideDisclaimer = ({
|
||||
username,
|
||||
haveOverrideProp,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
if (!haveOverrideProp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
className='popover__row first'
|
||||
<p
|
||||
className='user-popover__bottom-row-container'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='user_profile.account.post_was_created'
|
||||
defaultMessage='This post was created by an integration from @{username}'
|
||||
values={{
|
||||
username,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{formatMessage({
|
||||
id: 'user_profile.account.post_was_created',
|
||||
defaultMessage: 'This post was created by an integration from @{username}',
|
||||
},
|
||||
{
|
||||
username,
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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 Constants from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
position: UserProfile['position'];
|
||||
}
|
||||
|
||||
const ProfilePopoverPosition = ({
|
||||
position,
|
||||
}: Props) => {
|
||||
const positionSubstringed = (position).substring(0, Constants.MAX_POSITION_LENGTH);
|
||||
|
||||
return (
|
||||
<p
|
||||
className='user-profile-popover__non-heading'
|
||||
title={position}
|
||||
>
|
||||
{positionSubstringed}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverPosition;
|
||||
@@ -5,15 +5,12 @@ 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 WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
@@ -25,7 +22,7 @@ type Props = {
|
||||
handleShowDirectChannel: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
const ProfilePopoverEdit = ({
|
||||
const ProfilePopoverSelfUserRow = ({
|
||||
userId,
|
||||
currentUserId,
|
||||
haveOverrideProp,
|
||||
@@ -51,59 +48,43 @@ const ProfilePopoverEdit = ({
|
||||
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'
|
||||
className='user-popover__bottom-row-container'
|
||||
>
|
||||
<button
|
||||
id='editProfileButton'
|
||||
type='button'
|
||||
className='btn'
|
||||
className='btn btn-primary btn-sm'
|
||||
onClick={handleEditAccountSettings}
|
||||
>
|
||||
<AccountOutlineIcon
|
||||
size={16}
|
||||
aria-label={formatMessage({
|
||||
id: 'generic_icons.edit',
|
||||
defaultMessage: 'Edit Icon',
|
||||
})}
|
||||
<i
|
||||
className='icon icon-account-outline'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='user_profile.account.editProfile'
|
||||
defaultMessage='Edit Profile'
|
||||
/>
|
||||
</button>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
<WithTooltip
|
||||
id='user_profile.send.dm.yourself'
|
||||
title={formatMessage({id: 'user_profile.send.dm.yourself', defaultMessage: 'Send yourself a message'})}
|
||||
placement='top'
|
||||
overlay={sendMessageTooltip}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='btn icon-btn'
|
||||
className='btn btn-icon btn-sm'
|
||||
onClick={handleShowDirectChannel}
|
||||
aria-label={formatMessage({id: 'user_profile.send.dm.yourself', defaultMessage: 'Send yourself a message'})}
|
||||
>
|
||||
<SendIcon
|
||||
size={18}
|
||||
aria-label={formatMessage({
|
||||
id: 'user_profile.send.dm.icon',
|
||||
defaultMessage: 'Send Message Icon',
|
||||
})}
|
||||
<i
|
||||
className='icon icon-send'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</WithTooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePopoverEdit;
|
||||
export default ProfilePopoverSelfUserRow;
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {DateTime, Duration} from 'luxon';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import type {UserTimezone} from '@mattermost/types/users';
|
||||
|
||||
@@ -17,10 +17,16 @@ type ProfileTimezoneProps = {
|
||||
haveOverrideProp: boolean;
|
||||
}
|
||||
|
||||
const returnTimeDiff = (
|
||||
currentUserTimezone: string | undefined | null,
|
||||
profileUserTimezone: string,
|
||||
const TimeZoneDifference = ({
|
||||
currentUserTimezone,
|
||||
profileUserTimezone,
|
||||
}: {
|
||||
currentUserTimezone: string | undefined | null;
|
||||
profileUserTimezone: string;
|
||||
},
|
||||
) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
if (!currentUserTimezone) {
|
||||
return null;
|
||||
}
|
||||
@@ -32,23 +38,33 @@ const returnTimeDiff = (
|
||||
});
|
||||
|
||||
if (!offset.valueOf()) {
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeOffset = offset.toHuman({unitDisplay: 'short', signDisplay: 'never'});
|
||||
|
||||
return offset.valueOf() > 0 ? (
|
||||
<FormattedMessage
|
||||
id='user_profile.account.hoursAhead'
|
||||
defaultMessage='({timeOffset} ahead)'
|
||||
values={{timeOffset}}
|
||||
/>
|
||||
<>
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'user_profile.account.hoursAhead',
|
||||
defaultMessage: '({timeOffset} ahead)',
|
||||
},
|
||||
{timeOffset},
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='user_profile.account.hoursBehind'
|
||||
defaultMessage='({timeOffset} behind)'
|
||||
values={{timeOffset}}
|
||||
/>
|
||||
<>
|
||||
{
|
||||
formatMessage(
|
||||
{
|
||||
id: 'user_profile.account.hoursBehind',
|
||||
defaultMessage: '({timeOffset} behind)',
|
||||
},
|
||||
{timeOffset},
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -57,6 +73,8 @@ const ProfileTimezone = ({
|
||||
profileUserTimezone,
|
||||
haveOverrideProp,
|
||||
}: ProfileTimezoneProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
if (haveOverrideProp || !profileUserTimezone) {
|
||||
return null;
|
||||
}
|
||||
@@ -68,24 +86,27 @@ const ProfileTimezone = ({
|
||||
<div
|
||||
className='user-popover__time-status-container'
|
||||
>
|
||||
<span className='user-popover__subtitle'>
|
||||
{profileTimezoneShort ? (
|
||||
<FormattedMessage
|
||||
id='user_profile.account.localTimeWithTimezone'
|
||||
defaultMessage='Local Time ({timezone})'
|
||||
values={{
|
||||
timezone: profileTimezoneShort,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='user_profile.account.localTime'
|
||||
defaultMessage='Local Time'
|
||||
/>
|
||||
)}
|
||||
|
||||
</span>
|
||||
<span>
|
||||
<strong
|
||||
id='user-popover__timezone-title'
|
||||
className='user-popover__subtitle'
|
||||
>
|
||||
{profileTimezoneShort ? formatMessage(
|
||||
{
|
||||
id: 'user_profile.account.localTimeWithTimezone',
|
||||
defaultMessage: 'Local Time ({timezone})',
|
||||
},
|
||||
{
|
||||
timezone: profileTimezoneShort,
|
||||
},
|
||||
) : formatMessage({
|
||||
id: 'user_profile.account.localTime',
|
||||
defaultMessage: 'Local Time',
|
||||
})}
|
||||
</strong>
|
||||
<p
|
||||
aria-labelledby='user-popover__timezone-title'
|
||||
className='user-popover__subtitle-text'
|
||||
>
|
||||
<Timestamp
|
||||
useRelative={false}
|
||||
useDate={false}
|
||||
@@ -96,9 +117,11 @@ const ProfileTimezone = ({
|
||||
}}
|
||||
/>
|
||||
{' '}
|
||||
{returnTimeDiff(currentUserTimezone, profileTimezone)}
|
||||
</span>
|
||||
|
||||
<TimeZoneDifference
|
||||
currentUserTimezone={currentUserTimezone}
|
||||
profileUserTimezone={profileTimezone}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,10 @@
|
||||
// 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 React, {useEffect, useRef} 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';
|
||||
@@ -18,17 +15,18 @@ 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 {A11yFocusEventDetail} from 'utils/constants';
|
||||
import {A11yCustomEventTypes} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
type Props = {
|
||||
isBot: boolean;
|
||||
isBot?: boolean;
|
||||
roles: string;
|
||||
username: string;
|
||||
returnFocus: () => void;
|
||||
hide?: () => void;
|
||||
userId: string;
|
||||
channelId?: string;
|
||||
closeButtonRef: React.RefObject<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
function getIsTeamAdmin(state: GlobalState, userId: string) {
|
||||
@@ -55,22 +53,34 @@ function getIsChannelAdmin(state: GlobalState, userId: string, channelId?: strin
|
||||
const ProfilePopoverTitle = ({
|
||||
isBot,
|
||||
roles,
|
||||
username,
|
||||
returnFocus,
|
||||
hide,
|
||||
userId,
|
||||
channelId,
|
||||
closeButtonRef,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const closeRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const isTeamAdmin = useSelector((state: GlobalState) => getIsTeamAdmin(state, userId));
|
||||
const isChannelAdmin = useSelector((state: GlobalState) => getIsChannelAdmin(state, userId, channelId));
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
useEffect(() => {
|
||||
// Focus the close button when the popover first opens
|
||||
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
||||
A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: closeRef.current,
|
||||
keyboardOnly: true,
|
||||
},
|
||||
},
|
||||
));
|
||||
}, []);
|
||||
|
||||
function handleClose() {
|
||||
hide?.();
|
||||
returnFocus();
|
||||
}, [hide, returnFocus]);
|
||||
}
|
||||
|
||||
let roleTitle;
|
||||
if (isBot) {
|
||||
@@ -93,7 +103,7 @@ const ProfilePopoverTitle = ({
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
text={formatMessage({
|
||||
id: 'admin.permissions.roles.system_admin.name',
|
||||
id: 'user_profile.roleTitle.system_admin',
|
||||
defaultMessage: 'System Admin',
|
||||
})}
|
||||
/>
|
||||
@@ -104,7 +114,7 @@ const ProfilePopoverTitle = ({
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
text={formatMessage({
|
||||
id: 'admin.permissions.roles.team_admin.name',
|
||||
id: 'user_profile.roleTitle.team_admin',
|
||||
defaultMessage: 'Team Admin',
|
||||
})}
|
||||
/>
|
||||
@@ -115,28 +125,24 @@ const ProfilePopoverTitle = ({
|
||||
className='user-popover__role'
|
||||
size={'sm'}
|
||||
text={formatMessage({
|
||||
id: 'admin.permissions.roles.channel_admin.name',
|
||||
id: 'user_profile.roleTitle.channel_admin',
|
||||
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 className='user-profile-popover-title'>
|
||||
{roleTitle}
|
||||
<button
|
||||
ref={closeRef}
|
||||
className='btn btn-icon btn-sm closeButtonRelativePosition'
|
||||
onClick={handleClose}
|
||||
aria-label={formatMessage({id: 'user_profile.close', defaultMessage: 'Close user profile popover'})}
|
||||
>
|
||||
<i className='icon icon-close'/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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 = {
|
||||
@@ -9,19 +8,21 @@ type Props = {
|
||||
username: string;
|
||||
}
|
||||
|
||||
const UserName = ({
|
||||
const ProfilePopoverUserName = ({
|
||||
hasFullName,
|
||||
username,
|
||||
}: Props) => {
|
||||
const userNameClass = classNames('overflow--ellipsis pb-1', {'user-profile-popover__heading': !hasFullName});
|
||||
return (
|
||||
<div
|
||||
<p
|
||||
id='userPopoverUsername'
|
||||
className={userNameClass}
|
||||
className={
|
||||
hasFullName ? 'user-profile-popover__non-heading' : 'user-profile-popover__heading'
|
||||
}
|
||||
title={username}
|
||||
>
|
||||
{`@${username}`}
|
||||
</div>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserName;
|
||||
export default ProfilePopoverUserName;
|
||||
@@ -24,10 +24,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match
|
||||
channelLeaveHandler={[Function]}
|
||||
icon={
|
||||
<ProfilePicture
|
||||
hasMention={false}
|
||||
isEmoji={false}
|
||||
newStatusIcon={true}
|
||||
popoverPlacement="right"
|
||||
size="xs"
|
||||
src="/api/v4/users/user_id/image"
|
||||
statusClass="DirectChannel__status-icon "
|
||||
@@ -64,10 +61,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match
|
||||
channelLeaveHandler={[Function]}
|
||||
icon={
|
||||
<ProfilePicture
|
||||
hasMention={false}
|
||||
isEmoji={false}
|
||||
newStatusIcon={true}
|
||||
popoverPlacement="right"
|
||||
size="xs"
|
||||
src="/api/v4/users/user_id/image"
|
||||
status=""
|
||||
@@ -105,10 +99,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match
|
||||
channelLeaveHandler={[Function]}
|
||||
icon={
|
||||
<ProfilePicture
|
||||
hasMention={false}
|
||||
isEmoji={false}
|
||||
newStatusIcon={true}
|
||||
popoverPlacement="right"
|
||||
size="xs"
|
||||
src="/api/v4/users/user_id/image"
|
||||
statusClass="DirectChannel__status-icon "
|
||||
|
||||
@@ -59,7 +59,6 @@ exports[`at mention suggestion Should display nick name of non signed in user 1`
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/userid2/image?_=0"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
@@ -155,7 +154,6 @@ exports[`at mention suggestion Should not display nick name of the signed in use
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/userid1/image?_=0"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
|
||||
@@ -151,7 +151,6 @@ const AtMentionSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<Ite
|
||||
<SharedUserIndicator
|
||||
id={`sharedUserIndicator-${item.id}`}
|
||||
className='shared-user-icon'
|
||||
withTooltip={true}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ const SearchUserSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<Us
|
||||
<SharedUserIndicator
|
||||
id={`sharedUserIndicator-${item.id}`}
|
||||
className='mention__shared-user-icon'
|
||||
withTooltip={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -6,3 +6,6 @@ export enum Load {
|
||||
LOADING,
|
||||
FAILED,
|
||||
}
|
||||
|
||||
export const USER_GROUP_POPOVER_OPENING_DELAY = 300;
|
||||
export const USER_GROUP_POPOVER_CLOSING_DELAY = 500;
|
||||
|
||||
@@ -385,49 +385,60 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
|
||||
}
|
||||
}
|
||||
>
|
||||
<UserButton
|
||||
aria-haspopup="dialog"
|
||||
onClick={[Function]}
|
||||
<ProfilePopoverController
|
||||
hideStatus={false}
|
||||
src="/api/v4/users/id0/image?_=0"
|
||||
userId="id0"
|
||||
>
|
||||
<button
|
||||
<span
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
className="UserButton-bZNeGd lLsMM"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPointerDown={[Function]}
|
||||
>
|
||||
<Memo(Avatar)
|
||||
className="avatar-post-preview"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/id0/image?_=0"
|
||||
username="username0"
|
||||
>
|
||||
<img
|
||||
alt="username0 profile image"
|
||||
className="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/id0/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Username
|
||||
className="overflow--ellipsis text-nowrap"
|
||||
>
|
||||
<span
|
||||
className="Username-hebqVM cKlgOV overflow--ellipsis text-nowrap"
|
||||
<UserButton>
|
||||
<button
|
||||
className="UserButton-bZNeGd lLsMM"
|
||||
>
|
||||
Name0 Surname0
|
||||
</span>
|
||||
</Username>
|
||||
<Gap
|
||||
className="group-member-list_gap"
|
||||
>
|
||||
<span
|
||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
||||
/>
|
||||
</Gap>
|
||||
</button>
|
||||
</UserButton>
|
||||
<Memo(Avatar)
|
||||
className="avatar-post-preview"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/id0/image?_=0"
|
||||
username="username0"
|
||||
>
|
||||
<img
|
||||
alt="username0 profile image"
|
||||
className="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/id0/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Username
|
||||
className="overflow--ellipsis text-nowrap"
|
||||
>
|
||||
<span
|
||||
className="Username-hebqVM cKlgOV overflow--ellipsis text-nowrap"
|
||||
>
|
||||
Name0 Surname0
|
||||
</span>
|
||||
</Username>
|
||||
<Gap
|
||||
className="group-member-list_gap"
|
||||
>
|
||||
<span
|
||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
||||
/>
|
||||
</Gap>
|
||||
</button>
|
||||
</UserButton>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
<DMContainer
|
||||
className="group-member-list_dm-button"
|
||||
>
|
||||
@@ -593,49 +604,60 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
|
||||
}
|
||||
}
|
||||
>
|
||||
<UserButton
|
||||
aria-haspopup="dialog"
|
||||
onClick={[Function]}
|
||||
<ProfilePopoverController
|
||||
hideStatus={false}
|
||||
src="/api/v4/users/id1/image?_=0"
|
||||
userId="id1"
|
||||
>
|
||||
<button
|
||||
<span
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
className="UserButton-bZNeGd lLsMM"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPointerDown={[Function]}
|
||||
>
|
||||
<Memo(Avatar)
|
||||
className="avatar-post-preview"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/id1/image?_=0"
|
||||
username="username1"
|
||||
>
|
||||
<img
|
||||
alt="username1 profile image"
|
||||
className="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/id1/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Username
|
||||
className="overflow--ellipsis text-nowrap"
|
||||
>
|
||||
<span
|
||||
className="Username-hebqVM cKlgOV overflow--ellipsis text-nowrap"
|
||||
<UserButton>
|
||||
<button
|
||||
className="UserButton-bZNeGd lLsMM"
|
||||
>
|
||||
Name1 Surname1
|
||||
</span>
|
||||
</Username>
|
||||
<Gap
|
||||
className="group-member-list_gap"
|
||||
>
|
||||
<span
|
||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
||||
/>
|
||||
</Gap>
|
||||
</button>
|
||||
</UserButton>
|
||||
<Memo(Avatar)
|
||||
className="avatar-post-preview"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/id1/image?_=0"
|
||||
username="username1"
|
||||
>
|
||||
<img
|
||||
alt="username1 profile image"
|
||||
className="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/id1/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Username
|
||||
className="overflow--ellipsis text-nowrap"
|
||||
>
|
||||
<span
|
||||
className="Username-hebqVM cKlgOV overflow--ellipsis text-nowrap"
|
||||
>
|
||||
Name1 Surname1
|
||||
</span>
|
||||
</Username>
|
||||
<Gap
|
||||
className="group-member-list_gap"
|
||||
>
|
||||
<span
|
||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
||||
/>
|
||||
</Gap>
|
||||
</button>
|
||||
</UserButton>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
<DMContainer
|
||||
className="group-member-list_dm-button"
|
||||
>
|
||||
@@ -801,49 +823,60 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
|
||||
}
|
||||
}
|
||||
>
|
||||
<UserButton
|
||||
aria-haspopup="dialog"
|
||||
onClick={[Function]}
|
||||
<ProfilePopoverController
|
||||
hideStatus={false}
|
||||
src="/api/v4/users/id2/image?_=0"
|
||||
userId="id2"
|
||||
>
|
||||
<button
|
||||
<span
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
className="UserButton-bZNeGd lLsMM"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPointerDown={[Function]}
|
||||
>
|
||||
<Memo(Avatar)
|
||||
className="avatar-post-preview"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/id2/image?_=0"
|
||||
username="username2"
|
||||
>
|
||||
<img
|
||||
alt="username2 profile image"
|
||||
className="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/id2/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Username
|
||||
className="overflow--ellipsis text-nowrap"
|
||||
>
|
||||
<span
|
||||
className="Username-hebqVM cKlgOV overflow--ellipsis text-nowrap"
|
||||
<UserButton>
|
||||
<button
|
||||
className="UserButton-bZNeGd lLsMM"
|
||||
>
|
||||
Name2 Surname2
|
||||
</span>
|
||||
</Username>
|
||||
<Gap
|
||||
className="group-member-list_gap"
|
||||
>
|
||||
<span
|
||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
||||
/>
|
||||
</Gap>
|
||||
</button>
|
||||
</UserButton>
|
||||
<Memo(Avatar)
|
||||
className="avatar-post-preview"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/id2/image?_=0"
|
||||
username="username2"
|
||||
>
|
||||
<img
|
||||
alt="username2 profile image"
|
||||
className="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/id2/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Username
|
||||
className="overflow--ellipsis text-nowrap"
|
||||
>
|
||||
<span
|
||||
className="Username-hebqVM cKlgOV overflow--ellipsis text-nowrap"
|
||||
>
|
||||
Name2 Surname2
|
||||
</span>
|
||||
</Username>
|
||||
<Gap
|
||||
className="group-member-list_gap"
|
||||
>
|
||||
<span
|
||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
||||
/>
|
||||
</Gap>
|
||||
</button>
|
||||
</UserButton>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
<DMContainer
|
||||
className="group-member-list_dm-button"
|
||||
>
|
||||
@@ -1009,49 +1042,60 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
|
||||
}
|
||||
}
|
||||
>
|
||||
<UserButton
|
||||
aria-haspopup="dialog"
|
||||
onClick={[Function]}
|
||||
<ProfilePopoverController
|
||||
hideStatus={false}
|
||||
src="/api/v4/users/id3/image?_=0"
|
||||
userId="id3"
|
||||
>
|
||||
<button
|
||||
<span
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
className="UserButton-bZNeGd lLsMM"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPointerDown={[Function]}
|
||||
>
|
||||
<Memo(Avatar)
|
||||
className="avatar-post-preview"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/id3/image?_=0"
|
||||
username="username3"
|
||||
>
|
||||
<img
|
||||
alt="username3 profile image"
|
||||
className="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/id3/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Username
|
||||
className="overflow--ellipsis text-nowrap"
|
||||
>
|
||||
<span
|
||||
className="Username-hebqVM cKlgOV overflow--ellipsis text-nowrap"
|
||||
<UserButton>
|
||||
<button
|
||||
className="UserButton-bZNeGd lLsMM"
|
||||
>
|
||||
Name3 Surname3
|
||||
</span>
|
||||
</Username>
|
||||
<Gap
|
||||
className="group-member-list_gap"
|
||||
>
|
||||
<span
|
||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
||||
/>
|
||||
</Gap>
|
||||
</button>
|
||||
</UserButton>
|
||||
<Memo(Avatar)
|
||||
className="avatar-post-preview"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/id3/image?_=0"
|
||||
username="username3"
|
||||
>
|
||||
<img
|
||||
alt="username3 profile image"
|
||||
className="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/id3/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Username
|
||||
className="overflow--ellipsis text-nowrap"
|
||||
>
|
||||
<span
|
||||
className="Username-hebqVM cKlgOV overflow--ellipsis text-nowrap"
|
||||
>
|
||||
Name3 Surname3
|
||||
</span>
|
||||
</Username>
|
||||
<Gap
|
||||
className="group-member-list_gap"
|
||||
>
|
||||
<span
|
||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
||||
/>
|
||||
</Gap>
|
||||
</button>
|
||||
</UserButton>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
<DMContainer
|
||||
className="group-member-list_dm-button"
|
||||
>
|
||||
@@ -1217,49 +1261,60 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
|
||||
}
|
||||
}
|
||||
>
|
||||
<UserButton
|
||||
aria-haspopup="dialog"
|
||||
onClick={[Function]}
|
||||
<ProfilePopoverController
|
||||
hideStatus={false}
|
||||
src="/api/v4/users/id4/image?_=0"
|
||||
userId="id4"
|
||||
>
|
||||
<button
|
||||
<span
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
className="UserButton-bZNeGd lLsMM"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPointerDown={[Function]}
|
||||
>
|
||||
<Memo(Avatar)
|
||||
className="avatar-post-preview"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/id4/image?_=0"
|
||||
username="username4"
|
||||
>
|
||||
<img
|
||||
alt="username4 profile image"
|
||||
className="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/id4/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Username
|
||||
className="overflow--ellipsis text-nowrap"
|
||||
>
|
||||
<span
|
||||
className="Username-hebqVM cKlgOV overflow--ellipsis text-nowrap"
|
||||
<UserButton>
|
||||
<button
|
||||
className="UserButton-bZNeGd lLsMM"
|
||||
>
|
||||
Name4 Surname4
|
||||
</span>
|
||||
</Username>
|
||||
<Gap
|
||||
className="group-member-list_gap"
|
||||
>
|
||||
<span
|
||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
||||
/>
|
||||
</Gap>
|
||||
</button>
|
||||
</UserButton>
|
||||
<Memo(Avatar)
|
||||
className="avatar-post-preview"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
url="/api/v4/users/id4/image?_=0"
|
||||
username="username4"
|
||||
>
|
||||
<img
|
||||
alt="username4 profile image"
|
||||
className="Avatar Avatar-sm avatar-post-preview"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/id4/image?_=0"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Username
|
||||
className="overflow--ellipsis text-nowrap"
|
||||
>
|
||||
<span
|
||||
className="Username-hebqVM cKlgOV overflow--ellipsis text-nowrap"
|
||||
>
|
||||
Name4 Surname4
|
||||
</span>
|
||||
</Username>
|
||||
<Gap
|
||||
className="group-member-list_gap"
|
||||
>
|
||||
<span
|
||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
||||
/>
|
||||
</Gap>
|
||||
</button>
|
||||
</UserButton>
|
||||
</span>
|
||||
</ProfilePopoverController>
|
||||
<DMContainer
|
||||
className="group-member-list_dm-button"
|
||||
>
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import NoResultsIndicator from 'components/no_results_indicator';
|
||||
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
||||
import ProfilePopover from 'components/profile_popover';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
import SimpleTooltip from 'components/widgets/simple_tooltip';
|
||||
import Avatar from 'components/widgets/users/avatar';
|
||||
@@ -55,11 +56,6 @@ export type Props = {
|
||||
*/
|
||||
hide: () => void;
|
||||
|
||||
/**
|
||||
* Function to call to show a profile popover and hide parent popover
|
||||
*/
|
||||
showUserOverlay: (user: UserProfile) => void;
|
||||
|
||||
/**
|
||||
* State of current search
|
||||
*/
|
||||
@@ -88,7 +84,6 @@ const GroupMemberList = (props: Props) => {
|
||||
teamUrl,
|
||||
searchTerm,
|
||||
searchState,
|
||||
showUserOverlay,
|
||||
} = props;
|
||||
|
||||
const history = useHistory();
|
||||
@@ -170,20 +165,23 @@ const GroupMemberList = (props: Props) => {
|
||||
key={user.id}
|
||||
role='listitem'
|
||||
>
|
||||
<UserButton
|
||||
onClick={() => showUserOverlay(user)}
|
||||
aria-haspopup='dialog'
|
||||
<ProfilePopover
|
||||
userId={user.id}
|
||||
src={Utils.imageURLForUser(user?.id ?? '')}
|
||||
hideStatus={user.is_bot}
|
||||
>
|
||||
<Avatar
|
||||
username={user.username}
|
||||
size={'sm'}
|
||||
url={Utils.imageURLForUser(user?.id ?? '')}
|
||||
className={'avatar-post-preview'}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<Username className='overflow--ellipsis text-nowrap'>{name}</Username>
|
||||
<Gap className='group-member-list_gap'/>
|
||||
</UserButton>
|
||||
<UserButton>
|
||||
<Avatar
|
||||
username={user.username}
|
||||
size={'sm'}
|
||||
url={Utils.imageURLForUser(user?.id ?? '')}
|
||||
className={'avatar-post-preview'}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<Username className='overflow--ellipsis text-nowrap'>{name}</Username>
|
||||
<Gap className='group-member-list_gap'/>
|
||||
</UserButton>
|
||||
</ProfilePopover>
|
||||
<DMContainer className='group-member-list_dm-button'>
|
||||
<SimpleTooltip
|
||||
id={`name-${user.id}`}
|
||||
|
||||
@@ -1,35 +1,6 @@
|
||||
// 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 {UserGroupPopoverController} from './user_group_popover_controller';
|
||||
|
||||
import {searchProfiles} from 'mattermost-redux/actions/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {setPopoverSearchTerm} from 'actions/views/search';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import UserGroupPopover from './user_group_popover';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
searchTerm: state.views.search.popoverSearch,
|
||||
isMobileView: getIsMobileView(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
setPopoverSearchTerm,
|
||||
openModal,
|
||||
searchProfiles,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UserGroupPopover);
|
||||
export default UserGroupPopoverController;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {isAnyModalOpen} from 'selectors/views/modals';
|
||||
|
||||
export default function useShouldClose(): boolean {
|
||||
const [shouldClose, setShouldClose] = useState(false);
|
||||
const [initialHasOpenModals, setInitialHasOpenModals] = useState(false);
|
||||
const hasOpenModals = useSelector(isAnyModalOpen);
|
||||
|
||||
useEffect(() => {
|
||||
setInitialHasOpenModals(hasOpenModals);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialHasOpenModals !== hasOpenModals) {
|
||||
setShouldClose(true);
|
||||
}
|
||||
}, [initialHasOpenModals, hasOpenModals]);
|
||||
|
||||
return shouldClose;
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
#user-group-popover .popover-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.user-group-popover-floating-overlay {
|
||||
// 99 being the z-index of the global header
|
||||
z-index: 1060;
|
||||
}
|
||||
|
||||
@@ -102,16 +102,9 @@ describe('component/user_group_popover', () => {
|
||||
};
|
||||
|
||||
const baseProps: ComponentProps<typeof UserGroupPopover> = {
|
||||
searchTerm: '',
|
||||
group: group1,
|
||||
showUserOverlay: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
returnFocus: jest.fn(),
|
||||
actions: {
|
||||
setPopoverSearchTerm: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
searchProfiles: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
},
|
||||
};
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
@@ -129,25 +122,6 @@ describe('component/user_group_popover', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should open modal', async () => {
|
||||
const store = await mockStore(initialState);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<UserGroupPopover
|
||||
{...baseProps}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
);
|
||||
await actImmediate(wrapper);
|
||||
|
||||
expect(wrapper.find('button.user-group-popover_header-button').exists()).toBe(true);
|
||||
wrapper.find('button.user-group-popover_header-button').simulate('click');
|
||||
expect(baseProps.actions.openModal).toBeCalled();
|
||||
expect(baseProps.hide).toBeCalled();
|
||||
});
|
||||
|
||||
test('should not show search bar', async () => {
|
||||
const store = await mockStore(initialState);
|
||||
const wrapper = mountWithIntl(
|
||||
@@ -165,24 +139,6 @@ describe('component/user_group_popover', () => {
|
||||
expect(wrapper.find('.user-group-popover_search-bar').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should show and set search term', async () => {
|
||||
const store = await mockStore(initialState);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<UserGroupPopover
|
||||
{...baseProps}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
);
|
||||
await actImmediate(wrapper);
|
||||
|
||||
expect(wrapper.find('.user-group-popover_search-bar input').exists()).toBe(true);
|
||||
wrapper.find('.user-group-popover_search-bar input').simulate('change', {target: {value: 'a'}});
|
||||
expect(baseProps.actions.setPopoverSearchTerm).toHaveBeenCalledWith('a');
|
||||
});
|
||||
|
||||
test('should show users', async () => {
|
||||
const store = await mockStore(initialState);
|
||||
const wrapper = mountWithIntl(
|
||||
|
||||
@@ -2,97 +2,62 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
import type {ChangeEvent} from 'react';
|
||||
import React, {useEffect, useCallback, useState, useRef} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {MagnifyIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Group} from '@mattermost/types/groups';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {searchProfiles} from 'mattermost-redux/actions/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {setPopoverSearchTerm} from 'actions/views/search';
|
||||
|
||||
import {QuickInput} from 'components/quick_input/quick_input';
|
||||
import GroupMemberList from 'components/user_group_popover/group_member_list';
|
||||
import UserGroupsModal from 'components/user_groups_modal';
|
||||
import ViewUserGroupModal from 'components/view_user_group_modal';
|
||||
import Popover from 'components/widgets/popover';
|
||||
|
||||
import Constants, {A11yClassNames, A11yCustomEventTypes, ModalIdentifiers} from 'utils/constants';
|
||||
import Constants, {A11yCustomEventTypes, ModalIdentifiers} from 'utils/constants';
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {shouldFocusMainTextbox} from 'utils/post_utils';
|
||||
|
||||
import type {ModalData} from 'types/actions';
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import {Load} from './constants';
|
||||
import useShouldClose from './useShouldClose';
|
||||
|
||||
import './user_group_popover.scss';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The group corresponding to the parent popover
|
||||
*/
|
||||
group: Group;
|
||||
|
||||
/**
|
||||
* Function to call if parent popover should be hidden
|
||||
*/
|
||||
hide: () => void;
|
||||
|
||||
/**
|
||||
* Function to call if focus should be returned to triggering element
|
||||
*/
|
||||
returnFocus: () => void;
|
||||
|
||||
/**
|
||||
* Function to call to show a profile popover and hide parent popover
|
||||
*/
|
||||
showUserOverlay: (user: UserProfile) => void;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
searchTerm: string;
|
||||
|
||||
actions: {
|
||||
setPopoverSearchTerm: (term: string) => void;
|
||||
searchProfiles: (term: string, options: any) => Promise<ActionResult>;
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const UserGroupPopover = ({
|
||||
actions,
|
||||
group,
|
||||
hide,
|
||||
returnFocus,
|
||||
searchTerm,
|
||||
showUserOverlay,
|
||||
|
||||
// These props are not passed explictly to this component, but
|
||||
// they are added when this component is passed as a child to Overlay.
|
||||
// They are not typed in the component because they will cause more confusion.
|
||||
...popoverProps
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const closeRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const searchTerm = useSelector((state: GlobalState) => state.views.search.popoverSearch);
|
||||
|
||||
const [searchState, setSearchState] = useState(Load.DONE);
|
||||
|
||||
const shouldClose = useShouldClose();
|
||||
|
||||
const doSearch = useCallback(debounce(async (term) => {
|
||||
const res = await actions.searchProfiles(term, {in_group_id: group.id});
|
||||
const res = await dispatch(searchProfiles(term, {in_group_id: group.id}));
|
||||
if (res.data) {
|
||||
setSearchState(Load.DONE);
|
||||
} else {
|
||||
setSearchState(Load.FAILED);
|
||||
}
|
||||
}, Constants.SEARCH_TIMEOUT_MILLISECONDS), [actions.searchProfiles]);
|
||||
}, Constants.SEARCH_TIMEOUT_MILLISECONDS), []);
|
||||
|
||||
useEffect(() => {
|
||||
// Focus the close button when the popover first opens
|
||||
@@ -107,9 +72,9 @@ const UserGroupPopover = ({
|
||||
|
||||
// Unset the popover search term on mount and unmount
|
||||
// This is to prevent some odd rendering issues when quickly opening and closing popovers
|
||||
actions.setPopoverSearchTerm('');
|
||||
dispatch(setPopoverSearchTerm(''));
|
||||
return () => {
|
||||
actions.setPopoverSearchTerm('');
|
||||
dispatch(setPopoverSearchTerm(''));
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -123,26 +88,20 @@ const UserGroupPopover = ({
|
||||
}
|
||||
}, [searchTerm, doSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldClose) {
|
||||
hide();
|
||||
}
|
||||
}, [hide, shouldClose]);
|
||||
|
||||
const openGroupsModal = () => {
|
||||
actions.openModal({
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.USER_GROUPS,
|
||||
dialogType: UserGroupsModal,
|
||||
dialogProps: {
|
||||
backButtonAction: openGroupsModal,
|
||||
onExited: returnFocus,
|
||||
},
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
const openViewGroupModal = () => {
|
||||
hide();
|
||||
actions.openModal({
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.VIEW_USER_GROUP,
|
||||
dialogType: ViewUserGroupModal,
|
||||
dialogProps: {
|
||||
@@ -151,7 +110,7 @@ const UserGroupPopover = ({
|
||||
backButtonAction: openViewGroupModal,
|
||||
onExited: returnFocus,
|
||||
},
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -159,107 +118,81 @@ const UserGroupPopover = ({
|
||||
returnFocus();
|
||||
};
|
||||
|
||||
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
actions.setPopoverSearchTerm(event.target.value);
|
||||
const handleSearch = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setPopoverSearchTerm(event.target.value));
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
actions.setPopoverSearchTerm('');
|
||||
dispatch(setPopoverSearchTerm(''));
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (shouldFocusMainTextbox(e, document.activeElement)) {
|
||||
hide();
|
||||
} else if (Keyboard.isKeyPressed(e, Constants.KeyCodes.ESCAPE)) {
|
||||
returnFocus();
|
||||
}
|
||||
};
|
||||
|
||||
const tabCatcher = (
|
||||
<span
|
||||
tabIndex={0}
|
||||
onFocus={(e) => {
|
||||
(e.relatedTarget as HTMLElement)?.focus();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
id='user-group-popover'
|
||||
{...popoverProps}
|
||||
>
|
||||
{tabCatcher}
|
||||
<Body
|
||||
role='dialog'
|
||||
aria-modal={true}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={A11yClassNames.POPUP}
|
||||
aria-label={group.display_name}
|
||||
>
|
||||
<Header>
|
||||
<Heading>
|
||||
<Title
|
||||
className='overflow--ellipsis text-nowrap'
|
||||
>
|
||||
{group.display_name}
|
||||
</Title>
|
||||
<CloseButton
|
||||
className='btn btn-sm btn-compact btn-icon'
|
||||
aria-label={formatMessage({id: 'user_group_popover.close', defaultMessage: 'Close'})}
|
||||
onClick={handleClose}
|
||||
ref={closeRef}
|
||||
>
|
||||
<i
|
||||
className='icon icon-close'
|
||||
/>
|
||||
</CloseButton>
|
||||
</Heading>
|
||||
<Subtitle>
|
||||
<span className='overflow--ellipsis text-nowrap'>{'@'}{group.name}</span>
|
||||
<Dot>{'•'}</Dot>
|
||||
<FormattedMessage
|
||||
id='user_group_popover.memberCount'
|
||||
defaultMessage='{member_count} {member_count, plural, one {Member} other {Members}}'
|
||||
values={{
|
||||
member_count: group.member_count,
|
||||
}}
|
||||
tagName={NoShrink}
|
||||
<Body>
|
||||
<Header>
|
||||
<Heading>
|
||||
<Title
|
||||
className='overflow--ellipsis text-nowrap'
|
||||
>
|
||||
{group.display_name}
|
||||
</Title>
|
||||
<CloseButton
|
||||
className='btn btn-sm btn-compact btn-icon'
|
||||
aria-label={formatMessage({id: 'user_group_popover.close', defaultMessage: 'Close user group popover'})}
|
||||
onClick={handleClose}
|
||||
ref={closeRef}
|
||||
>
|
||||
<i
|
||||
className='icon icon-close'
|
||||
/>
|
||||
</Subtitle>
|
||||
<HeaderButton
|
||||
aria-label={`${group.display_name} @${group.name} ${formatMessage({id: 'user_group_popover.memberCount', defaultMessage: '{member_count} {member_count, plural, one {Member} other {Members}}'}, {member_count: group.member_count})} ${formatMessage({id: 'user_group_popover.openGroupModal', defaultMessage: 'View full group info'})}`}
|
||||
onClick={openViewGroupModal}
|
||||
className='user-group-popover_header-button'
|
||||
</CloseButton>
|
||||
</Heading>
|
||||
<Subtitle>
|
||||
<span className='overflow--ellipsis text-nowrap'>{'@'}{group.name}</span>
|
||||
<Dot>{'•'}</Dot>
|
||||
<FormattedMessage
|
||||
id='user_group_popover.memberCount'
|
||||
defaultMessage='{member_count} {member_count, plural, one {Member} other {Members}}'
|
||||
values={{
|
||||
member_count: group.member_count,
|
||||
}}
|
||||
tagName={NoShrink}
|
||||
/>
|
||||
</Header>
|
||||
{group.member_count > 10 ? (
|
||||
<SearchBar>
|
||||
<MagnifyIcon/>
|
||||
<QuickInput
|
||||
type='text'
|
||||
className='user-group-popover_search-bar'
|
||||
placeholder={formatMessage({id: 'user_group_popover.searchGroupMembers', defaultMessage: 'Search members'})}
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
clearable={true}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</SearchBar>
|
||||
) : null}
|
||||
<GroupMemberList
|
||||
group={group}
|
||||
hide={hide}
|
||||
searchState={searchState}
|
||||
showUserOverlay={showUserOverlay}
|
||||
</Subtitle>
|
||||
<HeaderButton
|
||||
aria-label={`${group.display_name} @${group.name} ${formatMessage({id: 'user_group_popover.memberCount', defaultMessage: '{member_count} {member_count, plural, one {Member} other {Members}}'}, {member_count: group.member_count})} ${formatMessage({id: 'user_group_popover.openGroupModal', defaultMessage: 'View full group info'})}`}
|
||||
onClick={openViewGroupModal}
|
||||
className='user-group-popover_header-button'
|
||||
/>
|
||||
</Body>
|
||||
{tabCatcher}
|
||||
</Popover>);
|
||||
</Header>
|
||||
{group.member_count > 10 ? (
|
||||
<SearchBar>
|
||||
<MagnifyIcon/>
|
||||
<QuickInput
|
||||
type='text'
|
||||
className='user-group-popover_search-bar'
|
||||
placeholder={formatMessage({id: 'user_group_popover.searchGroupMembers', defaultMessage: 'Search members'})}
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
clearable={true}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</SearchBar>
|
||||
) : null}
|
||||
<GroupMemberList
|
||||
group={group}
|
||||
hide={hide}
|
||||
searchState={searchState}
|
||||
/>
|
||||
</Body>
|
||||
);
|
||||
};
|
||||
|
||||
const Body = styled.div`
|
||||
width: 264px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 4px;
|
||||
background: var(--center-channel-bg);
|
||||
box-shadow: var(--elevation-4);
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
useFloating,
|
||||
autoUpdate,
|
||||
autoPlacement,
|
||||
useTransitionStyles,
|
||||
useClick,
|
||||
useDismiss,
|
||||
useInteractions,
|
||||
useRole,
|
||||
FloatingFocusManager,
|
||||
FloatingOverlay,
|
||||
FloatingPortal,
|
||||
} from '@floating-ui/react';
|
||||
import type {ReactNode} from 'react';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
|
||||
import type {Group} from '@mattermost/types/groups';
|
||||
|
||||
import {A11yClassNames} from 'utils/constants';
|
||||
|
||||
import {USER_GROUP_POPOVER_CLOSING_DELAY, USER_GROUP_POPOVER_OPENING_DELAY} from './constants';
|
||||
import UserGroupPopover from './user_group_popover';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
||||
/**
|
||||
* The group corresponding to the parent popover
|
||||
*/
|
||||
group: Group;
|
||||
|
||||
/**
|
||||
* Function to call if focus should be returned to triggering element
|
||||
*/
|
||||
returnFocus: () => void;
|
||||
}
|
||||
|
||||
export function UserGroupPopoverController(props: Props) {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const {refs, floatingStyles, context: floatingContext} = useFloating({
|
||||
open: isOpen,
|
||||
onOpenChange: setOpen,
|
||||
whileElementsMounted: autoUpdate,
|
||||
middleware: [autoPlacement()],
|
||||
});
|
||||
|
||||
const {isMounted, styles: transitionStyles} = useTransitionStyles(floatingContext, {
|
||||
duration: {
|
||||
open: USER_GROUP_POPOVER_OPENING_DELAY,
|
||||
close: USER_GROUP_POPOVER_CLOSING_DELAY,
|
||||
},
|
||||
});
|
||||
|
||||
const combinedFloatingStyles = Object.assign({}, floatingStyles, transitionStyles);
|
||||
|
||||
const clickInteractions = useClick(floatingContext);
|
||||
|
||||
const dismissInteraction = useDismiss(floatingContext);
|
||||
|
||||
const role = useRole(floatingContext);
|
||||
|
||||
const {getReferenceProps, getFloatingProps} = useInteractions([
|
||||
clickInteractions,
|
||||
dismissInteraction,
|
||||
role,
|
||||
]);
|
||||
|
||||
const handleHide = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
ref={refs.setReference}
|
||||
{...getReferenceProps()}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
|
||||
{isMounted && (
|
||||
<FloatingPortal id='user-group-popover-portal'>
|
||||
<FloatingOverlay
|
||||
id='user-group-popover-floating-overlay'
|
||||
className='user-group-popover-floating-overlay'
|
||||
lockScroll={true}
|
||||
>
|
||||
<FloatingFocusManager context={floatingContext}>
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
style={combinedFloatingStyles}
|
||||
className={A11yClassNames.POPUP}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<UserGroupPopover
|
||||
group={props.group}
|
||||
returnFocus={props.returnFocus}
|
||||
hide={handleHide}
|
||||
/>
|
||||
</div>
|
||||
</FloatingFocusManager>
|
||||
</FloatingOverlay>
|
||||
</FloatingPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,6 @@ const UserListRow = ({user, status, extraInfo = [], actions = [], actionProps, a
|
||||
status={statusProp}
|
||||
size='md'
|
||||
userId={user.id}
|
||||
hasMention={true}
|
||||
username={user.username}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -2,65 +2,31 @@
|
||||
|
||||
exports[`components/UserProfile should match snapshot 1`] = `
|
||||
<Fragment>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
hideStatus={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<ProfilePopoverController
|
||||
hideStatus={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
triggerComponentAs="button"
|
||||
triggerComponentClass="user-popover style--none"
|
||||
userId="user_id"
|
||||
>
|
||||
<button
|
||||
aria-label="nickname"
|
||||
className="user-popover style--none"
|
||||
>
|
||||
nickname
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
nickname
|
||||
</ProfilePopoverController>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/UserProfile should match snapshot, when displayUsername is enabled 1`] = `
|
||||
<Fragment>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
hideStatus={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<ProfilePopoverController
|
||||
hideStatus={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
triggerComponentAs="button"
|
||||
triggerComponentClass="user-popover style--none"
|
||||
userId="user_id"
|
||||
>
|
||||
<button
|
||||
aria-label="@username"
|
||||
className="user-popover style--none"
|
||||
>
|
||||
@username
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
@username
|
||||
</ProfilePopoverController>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -74,74 +40,39 @@ exports[`components/UserProfile should match snapshot, when popover is disabled
|
||||
|
||||
exports[`components/UserProfile should match snapshot, when user is shared 1`] = `
|
||||
<Fragment>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
hideStatus={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
<ProfilePopoverController
|
||||
hideStatus={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
triggerComponentAs="button"
|
||||
triggerComponentClass="user-popover style--none"
|
||||
userId="user_id"
|
||||
>
|
||||
<button
|
||||
aria-label="nickname"
|
||||
className="user-popover style--none"
|
||||
>
|
||||
nickname
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
nickname
|
||||
</ProfilePopoverController>
|
||||
<SharedUserIndicator
|
||||
className="shared-user-icon"
|
||||
id="sharedUserIndicator-user_id"
|
||||
withTooltip={true}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/UserProfile should match snapshot, with colorization 1`] = `
|
||||
<Fragment>
|
||||
<OverlayTrigger
|
||||
defaultOverlayShown={false}
|
||||
overlay={
|
||||
<Memo(ProfilePopover)
|
||||
className="user-profile-popover"
|
||||
hide={[Function]}
|
||||
hideStatus={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
userId="user_id"
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
rootClose={true}
|
||||
trigger={
|
||||
Array [
|
||||
"click",
|
||||
]
|
||||
}
|
||||
>
|
||||
<button
|
||||
aria-label="nickname"
|
||||
className="user-popover style--none"
|
||||
style={
|
||||
Object {
|
||||
"color": "#bbd279",
|
||||
}
|
||||
<ProfilePopoverController
|
||||
hideStatus={false}
|
||||
overwriteName=""
|
||||
src="/api/v4/users/undefined/image?_=0"
|
||||
triggerComponentAs="button"
|
||||
triggerComponentClass="user-popover style--none"
|
||||
triggerComponentStyle={
|
||||
Object {
|
||||
"color": "#bbd279",
|
||||
}
|
||||
>
|
||||
nickname
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
}
|
||||
userId="user_id"
|
||||
>
|
||||
nickname
|
||||
</ProfilePopoverController>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile as UserProfileType} from '@mattermost/types/users';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getUser, makeGetDisplayName} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import UserProfile from './user_profile';
|
||||
|
||||
type OwnProps = {
|
||||
userId: string;
|
||||
export type OwnProps = {
|
||||
userId: UserProfileType['id'];
|
||||
overwriteName?: string;
|
||||
overwriteIcon?: string;
|
||||
disablePopover?: boolean;
|
||||
displayUsername?: boolean;
|
||||
colorize?: boolean;
|
||||
hideStatus?: boolean;
|
||||
channelId?: Channel['id'];
|
||||
}
|
||||
|
||||
function makeMapStateToProps() {
|
||||
@@ -27,10 +36,13 @@ function makeMapStateToProps() {
|
||||
displayName: getDisplayName(state, ownProps.userId, true),
|
||||
user,
|
||||
theme,
|
||||
isMobileView: getIsMobileView(state),
|
||||
isShared: Boolean(user && user.remote_id),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const connector = connect(makeMapStateToProps);
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connect(makeMapStateToProps)(UserProfile);
|
||||
|
||||
@@ -18,6 +18,8 @@ describe('components/UserProfile', () => {
|
||||
user: {username: 'username'} as UserProfileType,
|
||||
userId: 'user_id',
|
||||
theme: Preferences.THEMES.onyx,
|
||||
isShared: false,
|
||||
dispatch: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type {UserProfile as UserProfileType} from '@mattermost/types/users';
|
||||
|
||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import type {BaseOverlayTrigger} from 'components/overlay_trigger';
|
||||
import ProfilePopover from 'components/profile_popover';
|
||||
import SharedUserIndicator from 'components/shared_user_indicator';
|
||||
import BotTag from 'components/widgets/tag/bot_tag';
|
||||
@@ -19,144 +15,80 @@ import {imageURLForUser} from 'utils/utils';
|
||||
|
||||
import {generateColor} from './utils';
|
||||
|
||||
export type Props = {
|
||||
user?: UserProfileType;
|
||||
userId: string;
|
||||
displayName?: string;
|
||||
isShared?: boolean;
|
||||
overwriteName?: string;
|
||||
overwriteIcon?: string;
|
||||
disablePopover?: boolean;
|
||||
displayUsername?: boolean;
|
||||
colorize?: boolean;
|
||||
hideStatus?: boolean;
|
||||
isMobileView: boolean;
|
||||
isRHS?: boolean;
|
||||
channelId?: string;
|
||||
theme?: Theme;
|
||||
}
|
||||
import type {OwnProps, PropsFromRedux} from './index';
|
||||
|
||||
export default class UserProfile extends PureComponent<Props> {
|
||||
private overlay?: BaseOverlayTrigger;
|
||||
export type Props = PropsFromRedux & OwnProps;
|
||||
|
||||
static defaultProps: Partial<Props> = {
|
||||
disablePopover: false,
|
||||
displayUsername: false,
|
||||
hideStatus: false,
|
||||
isRHS: false,
|
||||
overwriteName: '',
|
||||
colorize: false,
|
||||
};
|
||||
export default function UserProfile({
|
||||
disablePopover = false,
|
||||
displayUsername = false,
|
||||
hideStatus = false,
|
||||
overwriteName = '',
|
||||
colorize = false,
|
||||
user,
|
||||
displayName,
|
||||
theme,
|
||||
userId,
|
||||
channelId,
|
||||
overwriteIcon,
|
||||
isShared,
|
||||
}: Props) {
|
||||
let name: ReactNode;
|
||||
if (user && displayUsername) {
|
||||
name = `@${(user.username)}`;
|
||||
} else {
|
||||
name = overwriteName || displayName || '...';
|
||||
}
|
||||
|
||||
hideProfilePopover = (): void => {
|
||||
if (this.overlay) {
|
||||
this.overlay.hide();
|
||||
}
|
||||
};
|
||||
let userColor = theme?.centerChannelColor;
|
||||
if (user && theme) {
|
||||
userColor = generateColor(user.username, theme.centerChannelBg);
|
||||
}
|
||||
|
||||
setOverlaynRef = (ref: BaseOverlayTrigger): void => {
|
||||
this.overlay = ref;
|
||||
};
|
||||
|
||||
render(): React.ReactNode {
|
||||
const {
|
||||
disablePopover,
|
||||
displayName,
|
||||
displayUsername,
|
||||
isMobileView,
|
||||
isRHS,
|
||||
isShared,
|
||||
hideStatus,
|
||||
overwriteName,
|
||||
overwriteIcon,
|
||||
user,
|
||||
userId,
|
||||
channelId,
|
||||
colorize,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
let name: React.ReactNode;
|
||||
if (user && displayUsername) {
|
||||
name = `@${(user.username)}`;
|
||||
} else {
|
||||
name = overwriteName || displayName || '...';
|
||||
}
|
||||
|
||||
const ariaName: string = typeof name === 'string' ? name.toLowerCase() : '';
|
||||
|
||||
let userColor = theme?.centerChannelColor;
|
||||
if (user && theme) {
|
||||
userColor = generateColor(user.username, theme.centerChannelBg);
|
||||
}
|
||||
|
||||
let userStyle;
|
||||
if (colorize) {
|
||||
userStyle = {color: userColor};
|
||||
}
|
||||
|
||||
if (disablePopover) {
|
||||
return (
|
||||
<div
|
||||
className='user-popover'
|
||||
style={userStyle}
|
||||
>{name}</div>
|
||||
);
|
||||
}
|
||||
|
||||
let placement = 'right';
|
||||
if (isRHS && !isMobileView) {
|
||||
placement = 'left';
|
||||
}
|
||||
|
||||
let profileImg = '';
|
||||
if (user) {
|
||||
profileImg = imageURLForUser(user.id, user.last_picture_update);
|
||||
}
|
||||
|
||||
let sharedIcon;
|
||||
if (isShared) {
|
||||
sharedIcon = (
|
||||
<SharedUserIndicator
|
||||
id={`sharedUserIndicator-${userId}`}
|
||||
className='shared-user-icon'
|
||||
withTooltip={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let userStyle;
|
||||
if (colorize) {
|
||||
userStyle = {color: userColor};
|
||||
}
|
||||
|
||||
if (disablePopover) {
|
||||
return (
|
||||
<>
|
||||
<OverlayTrigger
|
||||
ref={this.setOverlaynRef}
|
||||
trigger={['click']}
|
||||
placement={placement}
|
||||
rootClose={true}
|
||||
overlay={
|
||||
<ProfilePopover
|
||||
className='user-profile-popover'
|
||||
userId={userId}
|
||||
channelId={channelId}
|
||||
src={profileImg}
|
||||
hide={this.hideProfilePopover}
|
||||
hideStatus={hideStatus}
|
||||
overwriteName={overwriteName}
|
||||
overwriteIcon={overwriteIcon}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
aria-label={ariaName}
|
||||
className='user-popover style--none'
|
||||
style={userStyle}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
{sharedIcon}
|
||||
{(user && user.is_bot) && <BotTag/>}
|
||||
{(user && isGuest(user.roles)) && <GuestTag/>}
|
||||
</>
|
||||
<div
|
||||
className='user-popover'
|
||||
style={userStyle}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let profileImg = '';
|
||||
if (user) {
|
||||
profileImg = imageURLForUser(user.id, user.last_picture_update);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProfilePopover<HTMLButtonElement>
|
||||
triggerComponentAs='button'
|
||||
triggerComponentClass='user-popover style--none'
|
||||
triggerComponentStyle={userStyle}
|
||||
userId={userId}
|
||||
src={profileImg}
|
||||
channelId={channelId}
|
||||
hideStatus={hideStatus}
|
||||
overwriteIcon={overwriteIcon}
|
||||
overwriteName={overwriteName}
|
||||
>
|
||||
{name}
|
||||
</ProfilePopover>
|
||||
{(isShared) &&
|
||||
<SharedUserIndicator
|
||||
id={`sharedUserIndicator-${userId}`}
|
||||
className='shared-user-icon'
|
||||
/>
|
||||
}
|
||||
{(user && user.is_bot) && <BotTag/>}
|
||||
{(user && isGuest(user.roles)) && <GuestTag/>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
}
|
||||
|
||||
.group-member-username {
|
||||
padding-left: 6px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
@@ -7,7 +7,6 @@ exports[`components/widgets/users/Avatar should match the snapshot 1`] = `
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="test-url"
|
||||
tabIndex={0}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -25,6 +24,5 @@ exports[`components/widgets/users/Avatar should match the snapshot only with url
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="test-url"
|
||||
tabIndex={0}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {memo} from 'react';
|
||||
import type {HTMLAttributes} from 'react';
|
||||
import type {HTMLAttributes, SyntheticEvent} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
@@ -56,33 +57,38 @@ const Avatar = ({
|
||||
text,
|
||||
...attrs
|
||||
}: Props & Attrs) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const classes = classNames(`Avatar Avatar-${size}`, attrs.className);
|
||||
|
||||
if (text) {
|
||||
return (
|
||||
<div
|
||||
{...attrs}
|
||||
className={classes + ' Avatar-plain'}
|
||||
className={classNames(classes, 'Avatar-plain')}
|
||||
data-content={text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function handleOnError(e: SyntheticEvent<HTMLImageElement, Event>) {
|
||||
const fallbackSrc = (url && isURLForUser(url)) ? replaceURLWithDefaultImageURL(url) : BotDefaultIcon;
|
||||
|
||||
if (e.currentTarget.src !== fallbackSrc) {
|
||||
e.currentTarget.src = fallbackSrc;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
tabIndex={0}
|
||||
{...attrs}
|
||||
className={classes}
|
||||
alt={`${username || 'user'} profile image`}
|
||||
alt={formatMessage({id: 'avatar.alt', defaultMessage: '{username} profile image'}, {
|
||||
username: username || 'user',
|
||||
})}
|
||||
src={url}
|
||||
loading='lazy'
|
||||
onError={(e) => {
|
||||
const fallbackSrc = (url && isURLForUser(url)) ? replaceURLWithDefaultImageURL(url) : BotDefaultIcon;
|
||||
|
||||
if (e.currentTarget.src !== fallbackSrc) {
|
||||
e.currentTarget.src = fallbackSrc;
|
||||
}
|
||||
}}
|
||||
onError={handleOnError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -16,7 +16,7 @@ jest.mock('mattermost-redux/actions/users', () => {
|
||||
};
|
||||
});
|
||||
|
||||
import SimpleTooltip from 'components/widgets/simple_tooltip';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {mockStore} from 'tests/test_store';
|
||||
|
||||
@@ -149,7 +149,7 @@ describe('components/widgets/users/Avatars', () => {
|
||||
mountOptions,
|
||||
);
|
||||
|
||||
expect(wrapper.find(SimpleTooltip).find({id: 'names-overflow'}).prop('content')).toBe('first.last4, first.last5');
|
||||
expect(wrapper.find(WithTooltip).find({id: 'names-overflow'}).prop('title')).toBe('first.last4, first.last5');
|
||||
});
|
||||
|
||||
test('should fetch missing users', () => {
|
||||
@@ -178,6 +178,6 @@ describe('components/widgets/users/Avatars', () => {
|
||||
expect(wrapper.find(Avatar).find({url: '/api/v4/users/1/image?_=1620680333191'}).exists()).toBe(true);
|
||||
expect(wrapper.find(Avatar).find({url: '/api/v4/users/6/image?_=0'}).exists()).toBe(true);
|
||||
expect(wrapper.find(Avatar).find({url: '/api/v4/users/7/image?_=0'}).exists()).toBe(true);
|
||||
expect(wrapper.find(SimpleTooltip).find({id: 'names-overflow'}).prop('content')).toBe('first.last2, Someone, Someone');
|
||||
expect(wrapper.find(WithTooltip).find({id: 'names-overflow'}).prop('title')).toBe('first.last2, Someone, Someone');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useMemo, useEffect, useRef} from 'react';
|
||||
import React, {memo, useMemo, useEffect} from 'react';
|
||||
import type {ComponentProps, CSSProperties} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
@@ -14,13 +13,10 @@ import {getMissingProfilesByIds} from 'mattermost-redux/actions/users';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getUser as selectUser, makeDisplayNameGetter} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import type {BaseOverlayTrigger} from 'components/overlay_trigger';
|
||||
import ProfilePopover from 'components/profile_popover';
|
||||
import SimpleTooltip, {useSynchronizedImmediate} from 'components/widgets/simple_tooltip';
|
||||
import Avatar from 'components/widgets/users/avatar';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {t} from 'utils/i18n';
|
||||
import {imageURLForUser} from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
@@ -30,16 +26,10 @@ import './avatars.scss';
|
||||
type Props = {
|
||||
userIds: Array<UserProfile['id']>;
|
||||
totalUsers?: number;
|
||||
breakAt?: number;
|
||||
size?: ComponentProps<typeof Avatar>['size'];
|
||||
fetchMissingUsers?: boolean;
|
||||
disableProfileOverlay?: boolean;
|
||||
};
|
||||
|
||||
interface MMOverlayTrigger extends BaseOverlayTrigger {
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
const OTHERS_DISPLAY_LIMIT = 99;
|
||||
|
||||
function countMeta<T>(
|
||||
@@ -61,58 +51,34 @@ const displayNameGetter = makeDisplayNameGetter();
|
||||
|
||||
function UserAvatar({
|
||||
userId,
|
||||
overlayProps,
|
||||
disableProfileOverlay,
|
||||
...props
|
||||
}: {
|
||||
userId: UserProfile['id'];
|
||||
overlayProps: Partial<ComponentProps<typeof SimpleTooltip>>;
|
||||
disableProfileOverlay: boolean;
|
||||
} & ComponentProps<typeof Avatar>) {
|
||||
const user = useSelector((state: GlobalState) => selectUser(state, userId)) as UserProfile | undefined;
|
||||
const name = useSelector((state: GlobalState) => displayNameGetter(state, true)(user));
|
||||
|
||||
const profilePictureURL = userId ? imageURLForUser(userId) : '';
|
||||
|
||||
const overlay = useRef<MMOverlayTrigger>(null);
|
||||
|
||||
const hideProfilePopover = () => {
|
||||
overlay.current?.hide();
|
||||
};
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
disabled={disableProfileOverlay}
|
||||
placement='right'
|
||||
rootClose={true}
|
||||
ref={overlay}
|
||||
overlay={
|
||||
<ProfilePopover
|
||||
className='user-profile-popover'
|
||||
userId={userId}
|
||||
src={profilePictureURL}
|
||||
hide={hideProfilePopover}
|
||||
/>
|
||||
}
|
||||
<ProfilePopover<HTMLButtonElement>
|
||||
triggerComponentAs='button'
|
||||
triggerComponentClass='style--none rounded-button'
|
||||
userId={userId}
|
||||
src={profilePictureURL}
|
||||
>
|
||||
<SimpleTooltip
|
||||
id={`name-${userId}`}
|
||||
content={name}
|
||||
{...overlayProps}
|
||||
<WithTooltip
|
||||
id={`tooltip-name-${userId}`}
|
||||
title={name}
|
||||
placement='top'
|
||||
>
|
||||
<RoundButton
|
||||
className={'style--none'}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Avatar
|
||||
url={imageURLForUser(userId, user?.last_picture_update)}
|
||||
tabIndex={-1}
|
||||
{...props}
|
||||
/>
|
||||
</RoundButton>
|
||||
</SimpleTooltip>
|
||||
</OverlayTrigger>
|
||||
<Avatar
|
||||
url={imageURLForUser(userId, user?.last_picture_update)}
|
||||
tabIndex={-1}
|
||||
{...props}
|
||||
/>
|
||||
</WithTooltip>
|
||||
</ProfilePopover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,11 +87,9 @@ function Avatars({
|
||||
userIds,
|
||||
totalUsers,
|
||||
fetchMissingUsers = true,
|
||||
disableProfileOverlay = false,
|
||||
}: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const [overlayProps, setImmediate] = useSynchronizedImmediate();
|
||||
const [displayUserIds, overflowUserIds, {overflowUnnamedCount, nonDisplayCount}] = countMeta(userIds, totalUsers);
|
||||
const overflowNames = useSelector((state: GlobalState) => {
|
||||
return overflowUserIds.map((userId) => displayNameGetter(state, true)(selectUser(state, userId))).join(', ');
|
||||
@@ -142,10 +106,33 @@ function Avatars({
|
||||
}
|
||||
}, [fetchMissingUsers, userIds]);
|
||||
|
||||
let overflowUsersTooltip = '';
|
||||
if (nonDisplayCount) {
|
||||
if (overflowUserIds.length) {
|
||||
overflowUsersTooltip = formatMessage(
|
||||
{
|
||||
id: 'avatars.overflowUsers',
|
||||
defaultMessage: '{overflowUnnamedCount, plural, =0 {{names}} =1 {{names} and one other} other {{names} and # others}}',
|
||||
},
|
||||
{
|
||||
overflowUnnamedCount,
|
||||
names: overflowNames,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
overflowUsersTooltip = formatMessage(
|
||||
{
|
||||
id: 'avatars.overflowUnnamedOnly',
|
||||
defaultMessage: '{overflowUnnamedCount, plural, =1 {one other} other {# others}}',
|
||||
},
|
||||
{overflowUnnamedCount},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`Avatars Avatars___${size}`}
|
||||
onMouseLeave={() => setImmediate(false)}
|
||||
>
|
||||
{displayUserIds.map((id) => (
|
||||
<UserAvatar
|
||||
@@ -153,30 +140,13 @@ function Avatars({
|
||||
key={id}
|
||||
userId={id}
|
||||
size={size}
|
||||
overlayProps={overlayProps}
|
||||
disableProfileOverlay={disableProfileOverlay}
|
||||
/>
|
||||
))}
|
||||
{Boolean(nonDisplayCount) && (
|
||||
<SimpleTooltip
|
||||
<WithTooltip
|
||||
id={'names-overflow'}
|
||||
{...overlayProps}
|
||||
content={overflowUserIds.length ? formatMessage(
|
||||
{
|
||||
id: t('avatars.overflowUsers'),
|
||||
defaultMessage: '{overflowUnnamedCount, plural, =0 {{names}} =1 {{names} and one other} other {{names} and # others}}',
|
||||
},
|
||||
{
|
||||
overflowUnnamedCount,
|
||||
names: overflowNames,
|
||||
},
|
||||
) : formatMessage(
|
||||
{
|
||||
id: t('avatars.overflowUnnamedOnly'),
|
||||
defaultMessage: '{overflowUnnamedCount, plural, =1 {one other} other {# others}}',
|
||||
},
|
||||
{overflowUnnamedCount},
|
||||
)}
|
||||
placement='top'
|
||||
title={overflowUsersTooltip}
|
||||
>
|
||||
<Avatar
|
||||
style={avatarStyle}
|
||||
@@ -184,14 +154,10 @@ function Avatars({
|
||||
tabIndex={0}
|
||||
text={nonDisplayCount > OTHERS_DISPLAY_LIMIT ? `${OTHERS_DISPLAY_LIMIT}+` : `+${nonDisplayCount}`}
|
||||
/>
|
||||
</SimpleTooltip>
|
||||
</WithTooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const RoundButton = styled.button`
|
||||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
export default memo(Avatars);
|
||||
|
||||
@@ -2944,6 +2944,7 @@
|
||||
"authorize.app": "The app **{appName}** would like the ability to access and modify your basic information.",
|
||||
"authorize.deny": "Deny",
|
||||
"authorize.title": "Authorize **{appName}** to Connect to Your **Mattermost** User Account",
|
||||
"avatar.alt": "{username} profile image",
|
||||
"avatars.overflowUnnamedOnly": "{overflowUnnamedCount, plural, =1 {one other} other {# others}}",
|
||||
"avatars.overflowUsers": "{overflowUnnamedCount, plural, =0 {{names}} =1 {{names} and one other} other {{names} and # others}}",
|
||||
"backstage_list.search": "Search",
|
||||
@@ -4590,7 +4591,6 @@
|
||||
"pricing_modal.title": "Select a plan",
|
||||
"pricing_modal.wantToTry": "Want to try? ",
|
||||
"pricing_modal.wantToUpgrade": "Want to upgrade? ",
|
||||
"profile_popover.profileLabel": "Profile for {name}",
|
||||
"promote_to_user_modal.desc": "This action promotes the guest {username} to a member. It will allow the user to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to promote guest {username} to member?",
|
||||
"promote_to_user_modal.promote": "Promote",
|
||||
"promote_to_user_modal.title": "Promote guest {username} to member",
|
||||
@@ -5216,7 +5216,7 @@
|
||||
"url_input.buttonLabel.done": "Done",
|
||||
"url_input.buttonLabel.edit": "Edit",
|
||||
"url_input.label.url": "URL: ",
|
||||
"user_group_popover.close": "Close",
|
||||
"user_group_popover.close": "Close user group popover",
|
||||
"user_group_popover.memberCount": "{member_count} {member_count, plural, one {Member} other {Members}}",
|
||||
"user_group_popover.openGroupModal": "View full group info",
|
||||
"user_group_popover.searchGroupMembers": "Search members",
|
||||
@@ -5260,12 +5260,14 @@
|
||||
"user_profile.account.localTimeWithTimezone": "Local Time ({timezone})",
|
||||
"user_profile.account.post_was_created": "This post was created by an integration from @{username}",
|
||||
"user_profile.add_user_to_channel": "Add to a Channel",
|
||||
"user_profile.add_user_to_channel.icon": "Add User to Channel Icon",
|
||||
"user_profile.call.ongoing": "Call with {user} is ongoing",
|
||||
"user_profile.close": "Close user profile popover",
|
||||
"user_profile.custom_status": "Status",
|
||||
"user_profile.custom_status.set_status": "Set a status",
|
||||
"user_profile.roleTitle.channel_admin": "Channel Admin",
|
||||
"user_profile.roleTitle.system_admin": "System Admin",
|
||||
"user_profile.roleTitle.team_admin": "Team Admin",
|
||||
"user_profile.send.dm": "Message",
|
||||
"user_profile.send.dm.icon": "Send Message Icon",
|
||||
"user_profile.send.dm.yourself": "Send yourself a message",
|
||||
"user.settings.advance.confirmDeactivateAccountTitle": "Confirm Deactivation",
|
||||
"user.settings.advance.confirmDeactivateDesc": "Are you sure you want to deactivate your account? This can only be reversed by your System Administrator.",
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.rounded-button {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
button {
|
||||
.unread-badge {
|
||||
display: inline-block;
|
||||
|
||||
@@ -19,52 +19,6 @@
|
||||
.app__body & {
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.user-profile-popover {
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.2);
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
.popover-title {
|
||||
position: relative;
|
||||
border-color: var(--center-channel-bg);
|
||||
background: none;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
}
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: none;
|
||||
background: v(center-channel-bg);
|
||||
box-shadow: rgba(var(--center-channel-color-rgb), 0.08) 0 17px 50px 0, rgba(var(--center-channel-color-rgb), 0.08) 0 12px 15px 0;
|
||||
|
||||
.shared-user-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
color: rgba(61, 60, 64, 0.75);
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.channel-header__popover {
|
||||
@@ -257,66 +211,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.user-popover-image {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 128px;
|
||||
margin: 0 auto 8px;
|
||||
|
||||
#userAvatar {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.user-popover-status {
|
||||
position: absolute;
|
||||
top: auto;
|
||||
right: 8px;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 2px;
|
||||
border-radius: 50px;
|
||||
background: rgba(var(--center-channel-bg-rgb), 1);
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-profile-popover .Avatar {
|
||||
display: block;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.user-popover__email {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
max-width: 300px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user-popover__close {
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
border: none;
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
|
||||
&:hover {
|
||||
border-radius: 4px;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.hidden-label + .search-autocomplete__divider {
|
||||
&::before {
|
||||
display: none;
|
||||
|
||||
Ссылка в новой задаче
Block a user