[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
|
// # Open profile popover
|
||||||
cy.getLastPostId().then((postId) => {
|
cy.getLastPostId().then((postId) => {
|
||||||
cy.get(`#post_${postId}`).within(() => {
|
cy.get(`#post_${postId}`).within(() => {
|
||||||
cy.get('.user-popover').click();
|
cy.get('.status-wrapper').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// * Verify image alt in profile popover
|
// * 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`);
|
cy.get('.Avatar').should('have.attr', 'alt', `${otherUser.username} profile image`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// # Close the profile popover
|
||||||
|
cy.get('body').click();
|
||||||
|
|
||||||
// # Open Settings > Display > Themes
|
// # Open Settings > Display > Themes
|
||||||
cy.uiOpenSettingsModal('Display').within(() => {
|
cy.uiOpenSettingsModal('Display').within(() => {
|
||||||
cy.get('#displayButton').click();
|
cy.get('#displayButton').click();
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ describe('Verify Accessibility Support in Post', () => {
|
|||||||
cy.focused().tab();
|
cy.focused().tab();
|
||||||
|
|
||||||
// * Verify focus is on the username
|
// * 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();
|
cy.focused().tab();
|
||||||
|
|
||||||
// * Verify focus is on the time
|
// * 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.get(`#CENTER_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save message');
|
||||||
cy.focused().tab();
|
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
|
// * Verify focus is on the comment button
|
||||||
cy.get(`#CENTER_commentIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'reply');
|
cy.get(`#CENTER_commentIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'reply');
|
||||||
cy.focused().tab();
|
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.get(`#RHS_COMMENT_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'more');
|
||||||
cy.focused().tab({shift: true});
|
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
|
// * Verify focus is on the save icon
|
||||||
cy.get(`#RHS_COMMENT_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save message');
|
cy.get(`#RHS_COMMENT_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save message');
|
||||||
cy.focused().tab({shift: true});
|
cy.focused().tab({shift: true});
|
||||||
@@ -259,7 +251,7 @@ describe('Verify Accessibility Support in Post', () => {
|
|||||||
cy.focused().tab({shift: true});
|
cy.focused().tab({shift: true});
|
||||||
|
|
||||||
// * Verify focus is on the username
|
// * 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});
|
cy.focused().tab({shift: true});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('Profile > Profile Settings > Position', () => {
|
|||||||
cy.get('.profile-icon > img').as('profileIconForPopover').click();
|
cy.get('.profile-icon > img').as('profileIconForPopover').click();
|
||||||
|
|
||||||
// # Verify that the popover is visible and contains position
|
// # 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', () => {
|
it('MM-T2064 Position / 128 characters', () => {
|
||||||
|
|||||||
@@ -60,10 +60,11 @@ describe('Profile > Profile Settings> Full Name', () => {
|
|||||||
cy.getLastPostId().then((postId) => {
|
cy.getLastPostId().then((postId) => {
|
||||||
cy.get(`#post_${postId}`).should('be.visible');
|
cy.get(`#post_${postId}`).should('be.visible');
|
||||||
cy.get(`#post_${postId} img`).click();
|
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...'
|
// * 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
|
// * 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}`);
|
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
|
// * 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}`);
|
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('@botPost').then((postIdA) => {
|
||||||
cy.get(`#post_${postIdA} button.user-popover`).click();
|
cy.get(`#post_${postIdA} button.user-popover`).click();
|
||||||
|
|
||||||
cy.get('#user-profile-popover').
|
cy.get('div.user-profile-popover').
|
||||||
should('be.visible');
|
should('be.visible');
|
||||||
|
|
||||||
cy.findByTestId(`popover-fullname-${bot.username}`).
|
cy.findByTestId(`popover-fullname-${bot.username}`).
|
||||||
@@ -83,7 +83,7 @@ describe('Bot display name', () => {
|
|||||||
|
|
||||||
// * Verify changed display name
|
// * Verify changed display name
|
||||||
cy.get('@newBotPost').then(() => {
|
cy.get('@newBotPost').then(() => {
|
||||||
cy.get('#user-profile-popover').
|
cy.get('div.user-profile-popover').
|
||||||
should('be.visible');
|
should('be.visible');
|
||||||
|
|
||||||
cy.findByTestId(`popover-fullname-${bot.username}`).
|
cy.findByTestId(`popover-fullname-${bot.username}`).
|
||||||
|
|||||||
@@ -356,10 +356,11 @@ function verifyMentionedUserAndProfilePopover(postId: string) {
|
|||||||
cy.wrap($el).click();
|
cy.wrap($el).click();
|
||||||
|
|
||||||
// * Profile popover should be visible
|
// * 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
|
// * 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
|
// Click anywhere to close profile popover
|
||||||
cy.get('#channelHeaderInfo').click();
|
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', () => {
|
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
|
// # 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('.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
|
// * 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
|
// * 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.wrap($el).find('.GuestTag').should('be.visible').and('have.text', 'GUEST');
|
||||||
});
|
});
|
||||||
|
cy.get('button.closeButtonRelativePosition').click();
|
||||||
|
|
||||||
// # Close the profile popover
|
// # Close the profile popover
|
||||||
cy.get('#channel-header').click();
|
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
|
// * 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.wrap($el).find('.user-popover__role').should('not.exist');
|
||||||
});
|
});
|
||||||
|
cy.get('button.closeButtonRelativePosition').click();
|
||||||
|
|
||||||
// # Close the profile popover
|
// # Close the profile popover
|
||||||
cy.get('#channel-header').click();
|
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
|
// * 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.wrap($el).find('.GuestTag').should('be.visible').and('have.text', 'GUEST');
|
||||||
});
|
});
|
||||||
|
cy.get('button.closeButtonRelativePosition').click();
|
||||||
|
|
||||||
// # Close the profile popover
|
// # Close the profile popover
|
||||||
cy.get('#channel-header').click();
|
cy.get('#channel-header').click();
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ describe('Profile popover User A & B', () => {
|
|||||||
find(`[data-mention=${otherUser.username}]`).
|
find(`[data-mention=${otherUser.username}]`).
|
||||||
should('be.visible').
|
should('be.visible').
|
||||||
click();
|
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.
|
// # Add to a Channel should not be shown.
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ function verifyLastPost(owner, username, iconUrl) {
|
|||||||
|
|
||||||
function verifyProfilePopover(owner, username, iconUrl) {
|
function verifyProfilePopover(owner, username, iconUrl) {
|
||||||
// * Verify that the profile popover is shown
|
// * 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
|
// * Verify username from payload
|
||||||
cy.get('.user-profile-popover__heading').should('be.visible').and('have.text', username);
|
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});
|
cy.get(`#post_${postId}`).find('.profile-icon > img').click({force: true});
|
||||||
|
|
||||||
// * Popover should have rendered to screen
|
// * 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}');
|
cy.get('body').type('{esc}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -50,7 +50,7 @@ describe('Profile popover', () => {
|
|||||||
cy.get(`#post_${postId}`).find('.user-popover').click({force: true});
|
cy.get(`#post_${postId}`).find('.user-popover').click({force: true});
|
||||||
|
|
||||||
// * Popover should have rendered to screen
|
// * 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');
|
cy.getLastPostId().as('lastPostId');
|
||||||
|
|
||||||
// Getting height of each post before applying 'Fixed width, centered' option and assigning to alias
|
// 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');
|
cy.wrap(height).as('initialUserNameHeight');
|
||||||
});
|
});
|
||||||
getComponentByText('@firstPostId', firstMessage).invoke('height').then((height) => {
|
getComponentByText('@firstPostId', firstMessage).invoke('height').then((height) => {
|
||||||
@@ -72,10 +72,10 @@ describe('Scroll', () => {
|
|||||||
getFileThumbnail('mpeg-video-file.mpg').invoke('height').then((height) => {
|
getFileThumbnail('mpeg-video-file.mpg').invoke('height').then((height) => {
|
||||||
cy.wrap(height).as('initialMpgHeight');
|
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');
|
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');
|
cy.wrap(height).as('initialJpgHeight');
|
||||||
});
|
});
|
||||||
getComponentBySelector('@linkPreviewPostId', '.PostAttachmentOpenGraph__image').invoke('height').then((height) => {
|
getComponentBySelector('@linkPreviewPostId', '.PostAttachmentOpenGraph__image').invoke('height').then((height) => {
|
||||||
@@ -105,7 +105,7 @@ describe('Scroll', () => {
|
|||||||
// * Verify there is no scroll pop
|
// * Verify there is no scroll pop
|
||||||
cy.get('#post-list').should('exist').within(() => {
|
cy.get('#post-list').should('exist').within(() => {
|
||||||
cy.get('@initialUserNameHeight').then((originalHeight) => {
|
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) => {
|
cy.get('@initialFirstPostHeight').then((originalHeight) => {
|
||||||
getComponentByText('@firstPostId', firstMessage).invoke('height').should('be.equal', 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);
|
getFileThumbnail('mpeg-video-file.mpg').invoke('height').should('be.equal', originalHeight);
|
||||||
});
|
});
|
||||||
cy.get('@initialGifHeight').then((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) => {
|
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) => {
|
cy.get('@initialInlineImgHeight').then((originalHeight) => {
|
||||||
getComponentBySelector('@gifLinkPostId', 'img[aria-label="file thumbnail"]').invoke('height').should('be.equal', 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
|
// Get thumbnail component based on filename
|
||||||
const getFileThumbnail = (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) => {
|
return cy.get(`@${filename}PostId`).then((postId) => {
|
||||||
cy.get(`#${postId}_message`).findByLabelText(`file thumbnail ${filename}`);
|
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"
|
class="AdminUserCard__header"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="status-wrapper admin-user-card"
|
aria-expanded="false"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
class="status-wrapper admin-user-card"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="RoundButton-dvlhqG gnYSKj style--none"
|
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"
|
class="Avatar Avatar-xxl"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src="/api/v4/users/1234/image"
|
src="/api/v4/users/1234/image"
|
||||||
tabindex="-1"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -69,7 +70,9 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
|||||||
class="AdminUserCard__header"
|
class="AdminUserCard__header"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="status-wrapper admin-user-card"
|
aria-expanded="false"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
class="status-wrapper admin-user-card"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="RoundButton-dvlhqG gnYSKj style--none"
|
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"
|
class="Avatar Avatar-xxl"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src="/api/v4/users/1234/image"
|
src="/api/v4/users/1234/image"
|
||||||
tabindex="-1"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -124,7 +126,9 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
|||||||
class="AdminUserCard__header"
|
class="AdminUserCard__header"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="status-wrapper admin-user-card"
|
aria-expanded="false"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
class="status-wrapper admin-user-card"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="RoundButton-dvlhqG gnYSKj style--none"
|
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"
|
class="Avatar Avatar-xxl"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src="/api/v4/users/1234/image"
|
src="/api/v4/users/1234/image"
|
||||||
tabindex="-1"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -177,7 +180,9 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
|||||||
class="AdminUserCard__header"
|
class="AdminUserCard__header"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="status-wrapper admin-user-card"
|
aria-expanded="false"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
class="status-wrapper admin-user-card"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="RoundButton-dvlhqG gnYSKj style--none"
|
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"
|
class="Avatar Avatar-xxl"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src="/api/v4/users/1234/image"
|
src="/api/v4/users/1234/image"
|
||||||
tabindex="-1"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,114 +2,68 @@
|
|||||||
|
|
||||||
exports[`components/AtMention should match snapshot when mentioning a group followed by punctuation 1`] = `
|
exports[`components/AtMention should match snapshot when mentioning a group followed by punctuation 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<span>
|
<UserGroupPopoverController
|
||||||
<Overlay
|
group={
|
||||||
animation={[Function]}
|
Object {
|
||||||
onHide={[Function]}
|
"allow_reference": true,
|
||||||
placement="right"
|
"create_at": 1,
|
||||||
rootClose={true}
|
"delete_at": 0,
|
||||||
show={false}
|
"description": "",
|
||||||
>
|
"display_name": "group_display_name",
|
||||||
<Connect(Component)
|
"has_syncables": false,
|
||||||
group={
|
"id": "qwerty1",
|
||||||
Object {
|
"member_count": 0,
|
||||||
"allow_reference": true,
|
"name": "developers",
|
||||||
"create_at": 1,
|
"remote_id": "",
|
||||||
"delete_at": 0,
|
"scheme_admin": false,
|
||||||
"description": "",
|
"source": "",
|
||||||
"display_name": "group_display_name",
|
"update_at": 1,
|
||||||
"has_syncables": false,
|
}
|
||||||
"id": "qwerty1",
|
}
|
||||||
"member_count": 0,
|
returnFocus={[Function]}
|
||||||
"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>
|
|
||||||
<a
|
<a
|
||||||
aria-haspopup="dialog"
|
|
||||||
className="group-mention-link"
|
className="group-mention-link"
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@developers
|
@developers
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</UserGroupPopoverController>
|
||||||
.
|
.
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/AtMention should match snapshot when mentioning a group that is allowed reference 1`] = `
|
exports[`components/AtMention should match snapshot when mentioning a group that is allowed reference 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<span>
|
<UserGroupPopoverController
|
||||||
<Overlay
|
group={
|
||||||
animation={[Function]}
|
Object {
|
||||||
onHide={[Function]}
|
"allow_reference": true,
|
||||||
placement="right"
|
"create_at": 1,
|
||||||
rootClose={true}
|
"delete_at": 0,
|
||||||
show={false}
|
"description": "",
|
||||||
>
|
"display_name": "group_display_name",
|
||||||
<Connect(Component)
|
"has_syncables": false,
|
||||||
group={
|
"id": "qwerty1",
|
||||||
Object {
|
"member_count": 0,
|
||||||
"allow_reference": true,
|
"name": "developers",
|
||||||
"create_at": 1,
|
"remote_id": "",
|
||||||
"delete_at": 0,
|
"scheme_admin": false,
|
||||||
"description": "",
|
"source": "",
|
||||||
"display_name": "group_display_name",
|
"update_at": 1,
|
||||||
"has_syncables": false,
|
}
|
||||||
"id": "qwerty1",
|
}
|
||||||
"member_count": 0,
|
returnFocus={[Function]}
|
||||||
"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>
|
|
||||||
<a
|
<a
|
||||||
aria-haspopup="dialog"
|
|
||||||
className="group-mention-link"
|
className="group-mention-link"
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@developers
|
@developers
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</UserGroupPopoverController>
|
||||||
</Fragment>
|
</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`] = `
|
exports[`components/AtMention should match snapshot when mentioning current user 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<span
|
<ProfilePopoverController
|
||||||
className="mention--highlight"
|
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
|
<a
|
||||||
aria-haspopup="dialog"
|
|
||||||
className="mention-link"
|
className="mention-link"
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@First Last
|
@First Last
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</ProfilePopoverController>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/AtMention should match snapshot when mentioning user 1`] = `
|
exports[`components/AtMention should match snapshot when mentioning user 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<span>
|
<ProfilePopoverController
|
||||||
<Overlay
|
returnFocus={[Function]}
|
||||||
animation={[Function]}
|
src="/api/v4/users/abc2/image"
|
||||||
onHide={[Function]}
|
triggerComponentClass=""
|
||||||
placement="right"
|
userId="abc2"
|
||||||
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>
|
|
||||||
<a
|
<a
|
||||||
aria-haspopup="dialog"
|
|
||||||
className="mention-link"
|
className="mention-link"
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@Nick
|
@Nick
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</ProfilePopoverController>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/AtMention should match snapshot when mentioning user containing and followed by punctuation 1`] = `
|
exports[`components/AtMention should match snapshot when mentioning user containing and followed by punctuation 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<span>
|
<ProfilePopoverController
|
||||||
<Overlay
|
returnFocus={[Function]}
|
||||||
animation={[Function]}
|
src="/api/v4/users/abc3/image"
|
||||||
onHide={[Function]}
|
triggerComponentClass=""
|
||||||
placement="right"
|
userId="abc3"
|
||||||
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>
|
|
||||||
<a
|
<a
|
||||||
aria-haspopup="dialog"
|
|
||||||
className="mention-link"
|
className="mention-link"
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@Dot Matrix
|
@Dot Matrix
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</ProfilePopoverController>
|
||||||
.
|
.
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/AtMention should match snapshot when mentioning user containing punctuation 1`] = `
|
exports[`components/AtMention should match snapshot when mentioning user containing punctuation 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<span>
|
<ProfilePopoverController
|
||||||
<Overlay
|
returnFocus={[Function]}
|
||||||
animation={[Function]}
|
src="/api/v4/users/abc3/image"
|
||||||
onHide={[Function]}
|
triggerComponentClass=""
|
||||||
placement="right"
|
userId="abc3"
|
||||||
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>
|
|
||||||
<a
|
<a
|
||||||
aria-haspopup="dialog"
|
|
||||||
className="mention-link"
|
className="mention-link"
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@Dot Matrix
|
@Dot Matrix
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</ProfilePopoverController>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/AtMention should match snapshot when mentioning user followed by punctuation 1`] = `
|
exports[`components/AtMention should match snapshot when mentioning user followed by punctuation 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<span>
|
<ProfilePopoverController
|
||||||
<Overlay
|
returnFocus={[Function]}
|
||||||
animation={[Function]}
|
src="/api/v4/users/abc2/image"
|
||||||
onHide={[Function]}
|
triggerComponentClass=""
|
||||||
placement="right"
|
userId="abc2"
|
||||||
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>
|
|
||||||
<a
|
<a
|
||||||
aria-haspopup="dialog"
|
|
||||||
className="mention-link"
|
className="mention-link"
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@Nick
|
@Nick
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</ProfilePopoverController>
|
||||||
...
|
...
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/AtMention should match snapshot when mentioning user with different teammate name display setting 1`] = `
|
exports[`components/AtMention should match snapshot when mentioning user with different teammate name display setting 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<span>
|
<ProfilePopoverController
|
||||||
<Overlay
|
returnFocus={[Function]}
|
||||||
animation={[Function]}
|
src="/api/v4/users/abc2/image"
|
||||||
onHide={[Function]}
|
triggerComponentClass=""
|
||||||
placement="right"
|
userId="abc2"
|
||||||
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>
|
|
||||||
<a
|
<a
|
||||||
aria-haspopup="dialog"
|
|
||||||
className="mention-link"
|
className="mention-link"
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@user1
|
@user1
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</ProfilePopoverController>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/AtMention should match snapshot when mentioning user with mixed case 1`] = `
|
exports[`components/AtMention should match snapshot when mentioning user with mixed case 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<span>
|
<ProfilePopoverController
|
||||||
<Overlay
|
returnFocus={[Function]}
|
||||||
animation={[Function]}
|
src="/api/v4/users/abc2/image"
|
||||||
onHide={[Function]}
|
triggerComponentClass=""
|
||||||
placement="right"
|
userId="abc2"
|
||||||
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>
|
|
||||||
<a
|
<a
|
||||||
aria-haspopup="dialog"
|
|
||||||
className="mention-link"
|
className="mention-link"
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@Nick
|
@Nick
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</ProfilePopoverController>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ describe('components/AtMention', () => {
|
|||||||
marketing: TestHelper.getGroupMock({id: 'qwerty2', name: 'marketing', allow_reference: false}),
|
marketing: TestHelper.getGroupMock({id: 'qwerty2', name: 'marketing', allow_reference: false}),
|
||||||
accounting: TestHelper.getGroupMock({id: 'qwerty3', name: 'accounting', allow_reference: true}),
|
accounting: TestHelper.getGroupMock({id: 'qwerty3', name: 'accounting', allow_reference: true}),
|
||||||
},
|
},
|
||||||
|
dispatch: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot when mentioning user', () => {
|
test('should match snapshot when mentioning user', () => {
|
||||||
|
|||||||
@@ -1,108 +1,39 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useRef, useState, useMemo, type ComponentProps} from 'react';
|
import classNames from 'classnames';
|
||||||
import {Overlay} from 'react-bootstrap';
|
import React, {useRef, useMemo, memo} from 'react';
|
||||||
|
|
||||||
import type {Group} from '@mattermost/types/groups';
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
|
||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||||
|
|
||||||
import ProfilePopover from 'components/profile_popover';
|
import ProfilePopover from 'components/profile_popover';
|
||||||
import UserGroupPopover from 'components/user_group_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 type {A11yFocusEventDetail} from 'utils/constants';
|
||||||
import Constants, {A11yCustomEventTypes} from 'utils/constants';
|
import {A11yCustomEventTypes} from 'utils/constants';
|
||||||
import {isKeyPressed} from 'utils/keyboard';
|
|
||||||
import {popOverOverlayPosition, approxGroupPopOverHeight} from 'utils/position_utils';
|
|
||||||
import {getUserOrGroupFromMentionName} from 'utils/post_utils';
|
import {getUserOrGroupFromMentionName} from 'utils/post_utils';
|
||||||
import {getViewportSize} from 'utils/utils';
|
|
||||||
|
|
||||||
const HEADER_HEIGHT_ESTIMATE = 130;
|
import type {PropsFromRedux} from './index';
|
||||||
|
|
||||||
type Props = {
|
type OwnProps = {
|
||||||
currentUserId: string;
|
|
||||||
mentionName: string;
|
mentionName: string;
|
||||||
teammateNameDisplay: string;
|
|
||||||
usersByUsername: Record<string, UserProfile>;
|
|
||||||
groupsByName: Record<string, Group>;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
disableHighlight?: boolean;
|
disableHighlight?: boolean;
|
||||||
disableGroupHighlight?: boolean;
|
disableGroupHighlight?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AtMention = (props: Props) => {
|
type Props = OwnProps & PropsFromRedux;
|
||||||
const ref = useRef<HTMLAnchorElement>(null);
|
|
||||||
|
|
||||||
const [show, setShow] = useState(false);
|
const AtMention = (props: Props) => {
|
||||||
const [groupUser, setGroupUser] = useState<UserProfile | undefined>();
|
const ref = useRef<HTMLAnchorElement>(null);
|
||||||
const [target, setTarget] = useState<HTMLAnchorElement | undefined>();
|
|
||||||
const [placement, setPlacement] = useState<ComponentProps<typeof Overlay>['placement']>('right');
|
|
||||||
|
|
||||||
const [user, group] = useMemo(
|
const [user, group] = useMemo(
|
||||||
() => getUserOrGroupFromMentionName(props.mentionName, props.usersByUsername, props.groupsByName, props.disableGroupHighlight),
|
() => getUserOrGroupFromMentionName(props.mentionName, props.usersByUsername, props.groupsByName, props.disableGroupHighlight),
|
||||||
[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 = () => {
|
const returnFocus = () => {
|
||||||
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
||||||
A11yCustomEventTypes.FOCUS, {
|
A11yCustomEventTypes.FOCUS, {
|
||||||
@@ -114,98 +45,57 @@ export const AtMention = (props: Props) => {
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPopOver = (user?: UserProfile, group?: Group) => {
|
if (user) {
|
||||||
if (user) {
|
const userMentionNameSuffix = props.mentionName.substring(user.username.length);
|
||||||
return (
|
const userDisplayName = displayUsername(user, props.teammateNameDisplay);
|
||||||
|
const highlightMention = !props.disableHighlight && user.id === props.currentUserId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<ProfilePopover
|
<ProfilePopover
|
||||||
className='user-profile-popover'
|
triggerComponentClass={classNames({'mention--highlight': highlightMention})}
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
|
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
|
||||||
hide={hideOverlay}
|
|
||||||
channelId={props.channelId}
|
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
|
<UserGroupPopover
|
||||||
group={group}
|
group={group}
|
||||||
hide={hideOverlay}
|
|
||||||
showUserOverlay={showGroupUserOverlay}
|
|
||||||
returnFocus={returnFocus}
|
returnFocus={returnFocus}
|
||||||
/>
|
>
|
||||||
);
|
<a
|
||||||
}
|
ref={ref}
|
||||||
|
className='group-mention-link'
|
||||||
return null;
|
role='button'
|
||||||
};
|
tabIndex={0}
|
||||||
|
>
|
||||||
if (!user && !group) {
|
{'@' + groupDisplayName}
|
||||||
return <>{props.children}</>;
|
</a>
|
||||||
|
</UserGroupPopover>
|
||||||
|
{groupMentionNameSuffix}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let suffix = '';
|
return <>{props.children}</>;
|
||||||
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}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(AtMention);
|
export default memo(AtMention);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {ConnectedProps} from 'react-redux';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
import {getAllGroupsForReferenceByName} from 'mattermost-redux/selectors/entities/groups';
|
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}
|
userId={dmUser.user.id}
|
||||||
channelId={channel.id}
|
channelId={channel.id}
|
||||||
size='xl'
|
size='xl'
|
||||||
popoverPlacement='left'
|
|
||||||
/>
|
/>
|
||||||
</UserAvatar>
|
</UserAvatar>
|
||||||
<UserInfo>
|
<UserInfo>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ describe('channel_info_rhs/about_area_gm', () => {
|
|||||||
initialState,
|
initialState,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByLabelText('my username')).toBeInTheDocument();
|
expect(screen.getByText('my username')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should display channel header', () => {
|
test('should display channel header', () => {
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ const AboutAreaGM = ({channel, gmUsers, actions}: Props) => {
|
|||||||
userId={user.id}
|
userId={user.id}
|
||||||
username={user.username}
|
username={user.username}
|
||||||
channelId={channel.id}
|
channelId={channel.id}
|
||||||
popoverPlacement='left'
|
|
||||||
/>
|
/>
|
||||||
</ProfilePictureContainer>
|
</ProfilePictureContainer>
|
||||||
))}
|
))}
|
||||||
@@ -99,7 +98,6 @@ const AboutAreaGM = ({channel, gmUsers, actions}: Props) => {
|
|||||||
<React.Fragment key={user.id}>
|
<React.Fragment key={user.id}>
|
||||||
<UserProfileElement
|
<UserProfileElement
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
isRHS={true}
|
|
||||||
channelId={channel.id}
|
channelId={channel.id}
|
||||||
/>
|
/>
|
||||||
{(i + 1 !== length) && (<span>{', '}</span>)}
|
{(i + 1 !== length) && (<span>{', '}</span>)}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
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 ChannelMembersDropdown from 'components/channel_members_dropdown';
|
||||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
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 ProfilePicture from 'components/profile_picture';
|
||||||
import ProfilePopover from 'components/profile_popover';
|
import ProfilePopover from 'components/profile_popover';
|
||||||
import Tooltip from 'components/tooltip';
|
|
||||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||||
|
import WithTooltip from 'components/with_tooltip';
|
||||||
import Constants from 'utils/constants';
|
|
||||||
|
|
||||||
import type {ChannelMember} from './channel_members_rhs';
|
import type {ChannelMember} from './channel_members_rhs';
|
||||||
|
|
||||||
@@ -30,15 +26,6 @@ const Avatar = styled.div`
|
|||||||
flex-shrink: 0;
|
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`
|
const DisplayName = styled.span`
|
||||||
display: inline;
|
display: inline;
|
||||||
overflow: hidden;
|
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 Member = ({className, channel, member, index, totalUsers, editing, actions}: Props) => {
|
||||||
const overlay = React.createRef<MMOverlayTrigger>();
|
const {formatMessage} = useIntl();
|
||||||
const profileSrc = Client4.getProfilePictureUrl(member.user.id, member.user.last_picture_update);
|
|
||||||
|
|
||||||
const hideProfilePopover = () => {
|
const userProfileSrc = Client4.getProfilePictureUrl(member.user.id, member.user.last_picture_update);
|
||||||
if (overlay.current) {
|
|
||||||
overlay.current.hide();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -130,59 +108,46 @@ const Member = ({className, channel, member, index, totalUsers, editing, actions
|
|||||||
style={{height: '48px'}}
|
style={{height: '48px'}}
|
||||||
data-testid={`memberline-${member.user.id}`}
|
data-testid={`memberline-${member.user.id}`}
|
||||||
>
|
>
|
||||||
|
<span className='ProfileSpan'>
|
||||||
<OverlayTrigger
|
<Avatar>
|
||||||
ref={overlay}
|
<ProfilePicture
|
||||||
trigger={['click']}
|
size='sm'
|
||||||
placement={'left'}
|
status={member.status}
|
||||||
rootClose={true}
|
isBot={member.user.is_bot}
|
||||||
overlay={
|
|
||||||
<ProfilePopover
|
|
||||||
className='user-profile-popover'
|
|
||||||
userId={member.user.id}
|
userId={member.user.id}
|
||||||
src={profileSrc}
|
username={member.displayName}
|
||||||
hide={hideProfilePopover}
|
src={userProfileSrc}
|
||||||
hideStatus={member.user.is_bot}
|
|
||||||
/>
|
/>
|
||||||
}
|
</Avatar>
|
||||||
>
|
<ProfilePopover
|
||||||
<span className='ProfileSpan'>
|
triggerComponentClass='profileSpan_userInfo'
|
||||||
<Avatar>
|
userId={member.user.id}
|
||||||
<ProfilePicture
|
src={userProfileSrc}
|
||||||
popoverPlacement='left'
|
hideStatus={member.user.is_bot}
|
||||||
size='sm'
|
>
|
||||||
status={member.status}
|
<DisplayName>
|
||||||
isBot={member.user.is_bot}
|
{member.displayName}
|
||||||
userId={member.user.id}
|
{isGuest(member.user.roles) && <GuestTag/>}
|
||||||
username={member.displayName}
|
</DisplayName>
|
||||||
src={Client4.getProfilePictureUrl(member.user.id, member.user.last_picture_update)}
|
{
|
||||||
/>
|
member.displayName === member.user.username ? null : <Username>{'@'}{member.user.username}</Username>
|
||||||
</Avatar>
|
}
|
||||||
<UserInfo>
|
<CustomStatusEmoji
|
||||||
<DisplayName>
|
userID={member.user.id}
|
||||||
{member.displayName}
|
showTooltip={true}
|
||||||
{isGuest(member.user.roles) && <GuestTag/>}
|
emojiSize={16}
|
||||||
</DisplayName>
|
spanStyle={{
|
||||||
{
|
display: 'flex',
|
||||||
member.displayName === member.user.username ? null : <Username>{'@'}{member.user.username}</Username>
|
flex: '0 0 auto',
|
||||||
}
|
alignItems: 'center',
|
||||||
<CustomStatusEmoji
|
}}
|
||||||
userID={member.user.id}
|
emojiStyle={{
|
||||||
showTooltip={true}
|
marginLeft: '8px',
|
||||||
emojiSize={16}
|
alignItems: 'center',
|
||||||
spanStyle={{
|
}}
|
||||||
display: 'flex',
|
/>
|
||||||
flex: '0 0 auto',
|
</ProfilePopover>
|
||||||
alignItems: 'center',
|
</span>
|
||||||
}}
|
|
||||||
emojiStyle={{
|
|
||||||
marginLeft: '8px',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</UserInfo>
|
|
||||||
</span>
|
|
||||||
</OverlayTrigger>
|
|
||||||
|
|
||||||
<RoleChooser
|
<RoleChooser
|
||||||
className={classNames({editing}, 'member-role-chooser')}
|
className={classNames({editing}, 'member-role-chooser')}
|
||||||
@@ -217,22 +182,18 @@ const Member = ({className, channel, member, index, totalUsers, editing, actions
|
|||||||
)}
|
)}
|
||||||
</RoleChooser>
|
</RoleChooser>
|
||||||
{!editing && (
|
{!editing && (
|
||||||
<SendMessage onClick={() => actions.openDirectMessage(member.user)}>
|
<WithTooltip
|
||||||
<OverlayTrigger
|
id={`member-tooltip-${member.user.id}`}
|
||||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
title={formatMessage({
|
||||||
placement='left'
|
id: 'channel_members_rhs.member.send_message',
|
||||||
overlay={
|
defaultMessage: 'Send message',
|
||||||
<Tooltip>
|
})}
|
||||||
<FormattedMessage
|
placement='left'
|
||||||
id='channel_members_rhs.member.send_message'
|
>
|
||||||
defaultMessage='Send message'
|
<SendMessage onClick={() => actions.openDirectMessage(member.user)}>
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className='icon icon-send'/>
|
<i className='icon icon-send'/>
|
||||||
</OverlayTrigger>
|
</SendMessage>
|
||||||
</SendMessage>
|
</WithTooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -256,14 +217,20 @@ export default styled(Member)`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ProfileSpan {
|
.ProfileSpan {
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// This padding is to make sure the status icon doesnt get clipped off because of the overflow
|
padding: 4px 0; // This padding is to make sure the status icon doesn't get clipped off because of the overflow
|
||||||
padding: 4px 0;
|
|
||||||
margin-right: auto;
|
.profileSpan_userInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.MenuWrapper {
|
.MenuWrapper {
|
||||||
|
|||||||
@@ -31,161 +31,89 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
|
|||||||
>
|
>
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
hasMention={false}
|
|
||||||
isEmoji={false}
|
|
||||||
popoverPlacement="right"
|
|
||||||
size="md"
|
size="md"
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
status="status"
|
status="status"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
username="username"
|
username="username"
|
||||||
wrapperClass=""
|
|
||||||
>
|
>
|
||||||
<OverlayTrigger
|
<ProfilePopoverController
|
||||||
defaultOverlayShown={false}
|
channelId="channel_id"
|
||||||
overlay={
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
<Memo(ProfilePopover)
|
triggerComponentClass="status-wrapper"
|
||||||
channelId="channel_id"
|
userId="user_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",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<OverlayTrigger
|
<span
|
||||||
defaultOverlayShown={false}
|
aria-expanded="false"
|
||||||
overlay={
|
aria-haspopup="dialog"
|
||||||
<OverlayWrapper
|
className="status-wrapper"
|
||||||
channelId="channel_id"
|
onClick={[Function]}
|
||||||
className="user-profile-popover"
|
onKeyDown={[Function]}
|
||||||
hide={[Function]}
|
onKeyUp={[Function]}
|
||||||
intl={
|
onMouseDown={[Function]}
|
||||||
Object {
|
onPointerDown={[Function]}
|
||||||
"$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
|
<RoundButton
|
||||||
className="status-wrapper "
|
className="style--none"
|
||||||
onClick={[Function]}
|
size="md"
|
||||||
>
|
>
|
||||||
<RoundButton
|
<button
|
||||||
className="style--none"
|
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<button
|
<span
|
||||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
className="profile-icon "
|
||||||
size="md"
|
|
||||||
>
|
>
|
||||||
<span
|
<Memo(Avatar)
|
||||||
className="profile-icon "
|
size="md"
|
||||||
|
url="/api/v4/users/user_id/image?_=0"
|
||||||
|
username="username"
|
||||||
>
|
>
|
||||||
<Memo(Avatar)
|
<img
|
||||||
size="md"
|
alt="username profile image"
|
||||||
tabIndex={-1}
|
className="Avatar Avatar-md"
|
||||||
url="/api/v4/users/user_id/image?_=0"
|
loading="lazy"
|
||||||
username="username"
|
onError={[Function]}
|
||||||
>
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
<img
|
/>
|
||||||
alt="username profile image"
|
</Memo(Avatar)>
|
||||||
className="Avatar Avatar-md"
|
</span>
|
||||||
loading="lazy"
|
</button>
|
||||||
onError={[Function]}
|
</RoundButton>
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
<Memo(StatusIcon)
|
||||||
tabIndex={-1}
|
status="status"
|
||||||
/>
|
>
|
||||||
</Memo(Avatar)>
|
<StatusOfflineIcon
|
||||||
</span>
|
className="status "
|
||||||
</button>
|
|
||||||
</RoundButton>
|
|
||||||
<Memo(StatusIcon)
|
|
||||||
status="status"
|
|
||||||
>
|
>
|
||||||
<StatusOfflineIcon
|
<span
|
||||||
className="status "
|
className="status "
|
||||||
>
|
>
|
||||||
<span
|
<svg
|
||||||
className="status "
|
aria-label="Offline Icon"
|
||||||
>
|
className="offline--icon"
|
||||||
<svg
|
height="100%"
|
||||||
aria-label="Offline Icon"
|
role="img"
|
||||||
className="offline--icon"
|
style={
|
||||||
height="100%"
|
Object {
|
||||||
role="img"
|
"clipRule": "evenodd",
|
||||||
style={
|
"fillRule": "evenodd",
|
||||||
Object {
|
"strokeLinejoin": "round",
|
||||||
"clipRule": "evenodd",
|
"strokeMiterlimit": 1.41421,
|
||||||
"fillRule": "evenodd",
|
|
||||||
"strokeLinejoin": "round",
|
|
||||||
"strokeMiterlimit": 1.41421,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
viewBox="0 0 20 20"
|
}
|
||||||
width="100%"
|
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"
|
<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>
|
</svg>
|
||||||
</StatusOfflineIcon>
|
</span>
|
||||||
</Memo(StatusIcon)>
|
</StatusOfflineIcon>
|
||||||
</span>
|
</Memo(StatusIcon)>
|
||||||
</OverlayTrigger>
|
</span>
|
||||||
</OverlayTrigger>
|
</ProfilePopoverController>
|
||||||
</ProfilePicture>
|
</ProfilePicture>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -300,161 +228,89 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
hasMention={false}
|
|
||||||
isEmoji={false}
|
|
||||||
popoverPlacement="right"
|
|
||||||
size="md"
|
size="md"
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
status="status"
|
status="status"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
username="username"
|
username="username"
|
||||||
wrapperClass=""
|
|
||||||
>
|
>
|
||||||
<OverlayTrigger
|
<ProfilePopoverController
|
||||||
defaultOverlayShown={false}
|
channelId="channel_id"
|
||||||
overlay={
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
<Memo(ProfilePopover)
|
triggerComponentClass="status-wrapper"
|
||||||
channelId="channel_id"
|
userId="user_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",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<OverlayTrigger
|
<span
|
||||||
defaultOverlayShown={false}
|
aria-expanded="false"
|
||||||
overlay={
|
aria-haspopup="dialog"
|
||||||
<OverlayWrapper
|
className="status-wrapper"
|
||||||
channelId="channel_id"
|
onClick={[Function]}
|
||||||
className="user-profile-popover"
|
onKeyDown={[Function]}
|
||||||
hide={[Function]}
|
onKeyUp={[Function]}
|
||||||
intl={
|
onMouseDown={[Function]}
|
||||||
Object {
|
onPointerDown={[Function]}
|
||||||
"$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
|
<RoundButton
|
||||||
className="status-wrapper "
|
className="style--none"
|
||||||
onClick={[Function]}
|
size="md"
|
||||||
>
|
>
|
||||||
<RoundButton
|
<button
|
||||||
className="style--none"
|
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<button
|
<span
|
||||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
className="profile-icon "
|
||||||
size="md"
|
|
||||||
>
|
>
|
||||||
<span
|
<Memo(Avatar)
|
||||||
className="profile-icon "
|
size="md"
|
||||||
|
url="/api/v4/users/user_id/image?_=0"
|
||||||
|
username="username"
|
||||||
>
|
>
|
||||||
<Memo(Avatar)
|
<img
|
||||||
size="md"
|
alt="username profile image"
|
||||||
tabIndex={-1}
|
className="Avatar Avatar-md"
|
||||||
url="/api/v4/users/user_id/image?_=0"
|
loading="lazy"
|
||||||
username="username"
|
onError={[Function]}
|
||||||
>
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
<img
|
/>
|
||||||
alt="username profile image"
|
</Memo(Avatar)>
|
||||||
className="Avatar Avatar-md"
|
</span>
|
||||||
loading="lazy"
|
</button>
|
||||||
onError={[Function]}
|
</RoundButton>
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
<Memo(StatusIcon)
|
||||||
tabIndex={-1}
|
status="status"
|
||||||
/>
|
>
|
||||||
</Memo(Avatar)>
|
<StatusOfflineIcon
|
||||||
</span>
|
className="status "
|
||||||
</button>
|
|
||||||
</RoundButton>
|
|
||||||
<Memo(StatusIcon)
|
|
||||||
status="status"
|
|
||||||
>
|
>
|
||||||
<StatusOfflineIcon
|
<span
|
||||||
className="status "
|
className="status "
|
||||||
>
|
>
|
||||||
<span
|
<svg
|
||||||
className="status "
|
aria-label="Offline Icon"
|
||||||
>
|
className="offline--icon"
|
||||||
<svg
|
height="100%"
|
||||||
aria-label="Offline Icon"
|
role="img"
|
||||||
className="offline--icon"
|
style={
|
||||||
height="100%"
|
Object {
|
||||||
role="img"
|
"clipRule": "evenodd",
|
||||||
style={
|
"fillRule": "evenodd",
|
||||||
Object {
|
"strokeLinejoin": "round",
|
||||||
"clipRule": "evenodd",
|
"strokeMiterlimit": 1.41421,
|
||||||
"fillRule": "evenodd",
|
|
||||||
"strokeLinejoin": "round",
|
|
||||||
"strokeMiterlimit": 1.41421,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
viewBox="0 0 20 20"
|
}
|
||||||
width="100%"
|
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"
|
<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>
|
</svg>
|
||||||
</StatusOfflineIcon>
|
</span>
|
||||||
</Memo(StatusIcon)>
|
</StatusOfflineIcon>
|
||||||
</span>
|
</Memo(StatusIcon)>
|
||||||
</OverlayTrigger>
|
</span>
|
||||||
</OverlayTrigger>
|
</ProfilePopoverController>
|
||||||
</ProfilePicture>
|
</ProfilePicture>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -575,161 +431,89 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
|
|||||||
>
|
>
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
hasMention={false}
|
|
||||||
isEmoji={false}
|
|
||||||
popoverPlacement="right"
|
|
||||||
size="md"
|
size="md"
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
status="status"
|
status="status"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
username="username"
|
username="username"
|
||||||
wrapperClass=""
|
|
||||||
>
|
>
|
||||||
<OverlayTrigger
|
<ProfilePopoverController
|
||||||
defaultOverlayShown={false}
|
channelId="channel_id"
|
||||||
overlay={
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
<Memo(ProfilePopover)
|
triggerComponentClass="status-wrapper"
|
||||||
channelId="channel_id"
|
userId="user_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",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<OverlayTrigger
|
<span
|
||||||
defaultOverlayShown={false}
|
aria-expanded="false"
|
||||||
overlay={
|
aria-haspopup="dialog"
|
||||||
<OverlayWrapper
|
className="status-wrapper"
|
||||||
channelId="channel_id"
|
onClick={[Function]}
|
||||||
className="user-profile-popover"
|
onKeyDown={[Function]}
|
||||||
hide={[Function]}
|
onKeyUp={[Function]}
|
||||||
intl={
|
onMouseDown={[Function]}
|
||||||
Object {
|
onPointerDown={[Function]}
|
||||||
"$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
|
<RoundButton
|
||||||
className="status-wrapper "
|
className="style--none"
|
||||||
onClick={[Function]}
|
size="md"
|
||||||
>
|
>
|
||||||
<RoundButton
|
<button
|
||||||
className="style--none"
|
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<button
|
<span
|
||||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
className="profile-icon "
|
||||||
size="md"
|
|
||||||
>
|
>
|
||||||
<span
|
<Memo(Avatar)
|
||||||
className="profile-icon "
|
size="md"
|
||||||
|
url="/api/v4/users/user_id/image?_=0"
|
||||||
|
username="username"
|
||||||
>
|
>
|
||||||
<Memo(Avatar)
|
<img
|
||||||
size="md"
|
alt="username profile image"
|
||||||
tabIndex={-1}
|
className="Avatar Avatar-md"
|
||||||
url="/api/v4/users/user_id/image?_=0"
|
loading="lazy"
|
||||||
username="username"
|
onError={[Function]}
|
||||||
>
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
<img
|
/>
|
||||||
alt="username profile image"
|
</Memo(Avatar)>
|
||||||
className="Avatar Avatar-md"
|
</span>
|
||||||
loading="lazy"
|
</button>
|
||||||
onError={[Function]}
|
</RoundButton>
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
<Memo(StatusIcon)
|
||||||
tabIndex={-1}
|
status="status"
|
||||||
/>
|
>
|
||||||
</Memo(Avatar)>
|
<StatusOfflineIcon
|
||||||
</span>
|
className="status "
|
||||||
</button>
|
|
||||||
</RoundButton>
|
|
||||||
<Memo(StatusIcon)
|
|
||||||
status="status"
|
|
||||||
>
|
>
|
||||||
<StatusOfflineIcon
|
<span
|
||||||
className="status "
|
className="status "
|
||||||
>
|
>
|
||||||
<span
|
<svg
|
||||||
className="status "
|
aria-label="Offline Icon"
|
||||||
>
|
className="offline--icon"
|
||||||
<svg
|
height="100%"
|
||||||
aria-label="Offline Icon"
|
role="img"
|
||||||
className="offline--icon"
|
style={
|
||||||
height="100%"
|
Object {
|
||||||
role="img"
|
"clipRule": "evenodd",
|
||||||
style={
|
"fillRule": "evenodd",
|
||||||
Object {
|
"strokeLinejoin": "round",
|
||||||
"clipRule": "evenodd",
|
"strokeMiterlimit": 1.41421,
|
||||||
"fillRule": "evenodd",
|
|
||||||
"strokeLinejoin": "round",
|
|
||||||
"strokeMiterlimit": 1.41421,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
viewBox="0 0 20 20"
|
}
|
||||||
width="100%"
|
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"
|
<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>
|
</svg>
|
||||||
</StatusOfflineIcon>
|
</span>
|
||||||
</Memo(StatusIcon)>
|
</StatusOfflineIcon>
|
||||||
</span>
|
</Memo(StatusIcon)>
|
||||||
</OverlayTrigger>
|
</span>
|
||||||
</OverlayTrigger>
|
</ProfilePopoverController>
|
||||||
</ProfilePicture>
|
</ProfilePicture>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -912,161 +696,89 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
|
|||||||
>
|
>
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
channelId="channel_id"
|
channelId="channel_id"
|
||||||
hasMention={false}
|
|
||||||
isEmoji={false}
|
|
||||||
popoverPlacement="right"
|
|
||||||
size="md"
|
size="md"
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
status="status"
|
status="status"
|
||||||
userId="user_id"
|
userId="user_id"
|
||||||
username="username"
|
username="username"
|
||||||
wrapperClass=""
|
|
||||||
>
|
>
|
||||||
<OverlayTrigger
|
<ProfilePopoverController
|
||||||
defaultOverlayShown={false}
|
channelId="channel_id"
|
||||||
overlay={
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
<Memo(ProfilePopover)
|
triggerComponentClass="status-wrapper"
|
||||||
channelId="channel_id"
|
userId="user_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",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<OverlayTrigger
|
<span
|
||||||
defaultOverlayShown={false}
|
aria-expanded="false"
|
||||||
overlay={
|
aria-haspopup="dialog"
|
||||||
<OverlayWrapper
|
className="status-wrapper"
|
||||||
channelId="channel_id"
|
onClick={[Function]}
|
||||||
className="user-profile-popover"
|
onKeyDown={[Function]}
|
||||||
hide={[Function]}
|
onKeyUp={[Function]}
|
||||||
intl={
|
onMouseDown={[Function]}
|
||||||
Object {
|
onPointerDown={[Function]}
|
||||||
"$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
|
<RoundButton
|
||||||
className="status-wrapper "
|
className="style--none"
|
||||||
onClick={[Function]}
|
size="md"
|
||||||
>
|
>
|
||||||
<RoundButton
|
<button
|
||||||
className="style--none"
|
className="RoundButton-dvlhqG gfRzmz style--none"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<button
|
<span
|
||||||
className="RoundButton-dvlhqG gfRzmz style--none"
|
className="profile-icon "
|
||||||
size="md"
|
|
||||||
>
|
>
|
||||||
<span
|
<Memo(Avatar)
|
||||||
className="profile-icon "
|
size="md"
|
||||||
|
url="/api/v4/users/user_id/image?_=0"
|
||||||
|
username="username"
|
||||||
>
|
>
|
||||||
<Memo(Avatar)
|
<img
|
||||||
size="md"
|
alt="username profile image"
|
||||||
tabIndex={-1}
|
className="Avatar Avatar-md"
|
||||||
url="/api/v4/users/user_id/image?_=0"
|
loading="lazy"
|
||||||
username="username"
|
onError={[Function]}
|
||||||
>
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
<img
|
/>
|
||||||
alt="username profile image"
|
</Memo(Avatar)>
|
||||||
className="Avatar Avatar-md"
|
</span>
|
||||||
loading="lazy"
|
</button>
|
||||||
onError={[Function]}
|
</RoundButton>
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
<Memo(StatusIcon)
|
||||||
tabIndex={-1}
|
status="status"
|
||||||
/>
|
>
|
||||||
</Memo(Avatar)>
|
<StatusOfflineIcon
|
||||||
</span>
|
className="status "
|
||||||
</button>
|
|
||||||
</RoundButton>
|
|
||||||
<Memo(StatusIcon)
|
|
||||||
status="status"
|
|
||||||
>
|
>
|
||||||
<StatusOfflineIcon
|
<span
|
||||||
className="status "
|
className="status "
|
||||||
>
|
>
|
||||||
<span
|
<svg
|
||||||
className="status "
|
aria-label="Offline Icon"
|
||||||
>
|
className="offline--icon"
|
||||||
<svg
|
height="100%"
|
||||||
aria-label="Offline Icon"
|
role="img"
|
||||||
className="offline--icon"
|
style={
|
||||||
height="100%"
|
Object {
|
||||||
role="img"
|
"clipRule": "evenodd",
|
||||||
style={
|
"fillRule": "evenodd",
|
||||||
Object {
|
"strokeLinejoin": "round",
|
||||||
"clipRule": "evenodd",
|
"strokeMiterlimit": 1.41421,
|
||||||
"fillRule": "evenodd",
|
|
||||||
"strokeLinejoin": "round",
|
|
||||||
"strokeMiterlimit": 1.41421,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
viewBox="0 0 20 20"
|
}
|
||||||
width="100%"
|
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"
|
<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>
|
</svg>
|
||||||
</StatusOfflineIcon>
|
</span>
|
||||||
</Memo(StatusIcon)>
|
</StatusOfflineIcon>
|
||||||
</span>
|
</Memo(StatusIcon)>
|
||||||
</OverlayTrigger>
|
</span>
|
||||||
</OverlayTrigger>
|
</ProfilePopoverController>
|
||||||
</ProfilePicture>
|
</ProfilePicture>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import UserProfile from 'components/user_profile';
|
|||||||
import BotTag from 'components/widgets/tag/bot_tag';
|
import BotTag from 'components/widgets/tag/bot_tag';
|
||||||
import Tag from 'components/widgets/tag/tag';
|
import Tag from 'components/widgets/tag/tag';
|
||||||
|
|
||||||
import {Locations} from 'utils/constants';
|
|
||||||
import {fromAutoResponder, isFromWebhook} from 'utils/post_utils';
|
import {fromAutoResponder, isFromWebhook} from 'utils/post_utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -24,7 +23,6 @@ type Props = {
|
|||||||
isBot: boolean;
|
isBot: boolean;
|
||||||
isSystemMessage: boolean;
|
isSystemMessage: boolean;
|
||||||
isMobileView: boolean;
|
isMobileView: boolean;
|
||||||
location: keyof typeof Locations;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PostUserProfile = (props: Props): JSX.Element | null => {
|
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 {post, compactDisplay, isMobileView, isConsecutivePost, enablePostUsernameOverride, isBot, isSystemMessage, colorizeUsernames} = props;
|
||||||
const isFromAutoResponder = fromAutoResponder(post);
|
const isFromAutoResponder = fromAutoResponder(post);
|
||||||
const colorize = compactDisplay && colorizeUsernames;
|
const colorize = compactDisplay && colorizeUsernames;
|
||||||
const isRHS = props.location === Locations.RHS_COMMENT || props.location === Locations.RHS_ROOT || props.location === Locations.SEARCH;
|
|
||||||
|
|
||||||
let userProfile: ReactNode = null;
|
let userProfile: ReactNode = null;
|
||||||
let botIndicator = null;
|
let botIndicator = null;
|
||||||
@@ -55,7 +52,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
|||||||
<UserProfile
|
<UserProfile
|
||||||
userId={post.user_id}
|
userId={post.user_id}
|
||||||
channelId={post.channel_id}
|
channelId={post.channel_id}
|
||||||
isRHS={isRHS}
|
|
||||||
colorize={colorize}
|
colorize={colorize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -66,7 +62,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
|||||||
<UserProfile
|
<UserProfile
|
||||||
userId={post.user_id}
|
userId={post.user_id}
|
||||||
channelId={post.channel_id}
|
channelId={post.channel_id}
|
||||||
isRHS={isRHS}
|
|
||||||
colorize={colorize}
|
colorize={colorize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -75,7 +70,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
|||||||
<UserProfile
|
<UserProfile
|
||||||
userId={post.user_id}
|
userId={post.user_id}
|
||||||
channelId={post.channel_id}
|
channelId={post.channel_id}
|
||||||
isRHS={isRHS}
|
|
||||||
colorize={colorize}
|
colorize={colorize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -105,7 +99,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
|||||||
userId={post.user_id}
|
userId={post.user_id}
|
||||||
channelId={post.channel_id}
|
channelId={post.channel_id}
|
||||||
hideStatus={true}
|
hideStatus={true}
|
||||||
isRHS={isRHS}
|
|
||||||
colorize={colorize}
|
colorize={colorize}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -226,7 +226,6 @@ exports[`components/post_edit_history should match snapshot 1`] = `
|
|||||||
class="Avatar Avatar-sm avatar-post-preview"
|
class="Avatar Avatar-sm avatar-post-preview"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src="/api/v4/users/user_id/image?_=0"
|
src="/api/v4/users/user_id/image?_=0"
|
||||||
tabindex="0"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -41,11 +41,10 @@ describe('components/PostProfilePicture', () => {
|
|||||||
expect(screen.getByLabelText('Offline Icon')).toBeInTheDocument();
|
expect(screen.getByLabelText('Offline Icon')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('status and post icon override specified, default props', () => {
|
test('status is specified, default props', () => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
status: 'away',
|
status: 'away',
|
||||||
postIconOverrideURL: 'http://example.com/image.png',
|
|
||||||
};
|
};
|
||||||
renderWithContext(
|
renderWithContext(
|
||||||
<PostProfilePicture {...props}/>,
|
<PostProfilePicture {...props}/>,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ type Props = {
|
|||||||
status?: string;
|
status?: string;
|
||||||
user: UserProfile;
|
user: UserProfile;
|
||||||
isBot?: boolean;
|
isBot?: boolean;
|
||||||
postIconOverrideURL?: string;
|
|
||||||
overwriteIcon?: string;
|
overwriteIcon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +92,6 @@ export default class PostProfilePicture extends React.PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
const fromAutoResponder = PostUtils.fromAutoResponder(post);
|
const fromAutoResponder = PostUtils.fromAutoResponder(post);
|
||||||
|
|
||||||
const hasMention = !fromAutoResponder && !fromWebhook;
|
|
||||||
const profileSrc = this.getProfilePictureURL();
|
const profileSrc = this.getProfilePictureURL();
|
||||||
const src = this.getPostIconURL(profileSrc, fromAutoResponder, fromWebhook);
|
const src = this.getPostIconURL(profileSrc, fromAutoResponder, fromWebhook);
|
||||||
|
|
||||||
@@ -104,7 +102,6 @@ export default class PostProfilePicture extends React.PureComponent<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
hasMention={hasMention}
|
|
||||||
size='md'
|
size='md'
|
||||||
src={src}
|
src={src}
|
||||||
profileSrc={profileSrc}
|
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();
|
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).toBeInTheDocument();
|
||||||
expect(teammate).toHaveTextContent('my teammate');
|
expect(teammate).toHaveTextContent('my teammate');
|
||||||
expect(teammate).toHaveClass('user-popover style--none');
|
expect(teammate).toHaveClass('style--none');
|
||||||
|
|
||||||
const image = screen.getByRole('img');
|
const image = screen.getByRole('img');
|
||||||
|
|
||||||
|
|||||||
@@ -277,13 +277,11 @@ function createDMIntroMessage(
|
|||||||
status={teammate.is_bot ? '' : channel.status}
|
status={teammate.is_bot ? '' : channel.status}
|
||||||
userId={teammate?.id}
|
userId={teammate?.id}
|
||||||
username={teammate?.username}
|
username={teammate?.username}
|
||||||
hasMention={true}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2 className='channel-intro__title'>
|
<h2 className='channel-intro__title'>
|
||||||
<UserProfile
|
<UserProfile
|
||||||
userId={teammate?.id}
|
userId={teammate?.id}
|
||||||
disablePopover={false}
|
|
||||||
/>
|
/>
|
||||||
</h2>
|
</h2>
|
||||||
<p className='channel-intro__text'>
|
<p className='channel-intro__text'>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ exports[`components/post_view/CommentedOn should match snapshot 1`] = `
|
|||||||
className="theme user_name"
|
className="theme user_name"
|
||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -47,7 +46,6 @@ exports[`components/post_view/CommentedOn should match snapshot 2`] = `
|
|||||||
className="theme user_name"
|
className="theme user_name"
|
||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -79,7 +77,6 @@ exports[`components/post_view/CommentedOn should match snapshot 3`] = `
|
|||||||
className="theme user_name"
|
className="theme user_name"
|
||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -113,7 +110,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
|||||||
className="theme user_name"
|
className="theme user_name"
|
||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -145,7 +141,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
|||||||
className="theme user_name"
|
className="theme user_name"
|
||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -177,7 +172,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
|||||||
className="theme user_name"
|
className="theme user_name"
|
||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
@@ -209,7 +203,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p
|
|||||||
className="theme user_name"
|
className="theme user_name"
|
||||||
>
|
>
|
||||||
<Memo(Connect(UserProfile))
|
<Memo(Connect(UserProfile))
|
||||||
disablePopover={false}
|
|
||||||
userId=""
|
userId=""
|
||||||
/>
|
/>
|
||||||
</a>,
|
</a>,
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ function CommentedOn({post, parentPostUser, onCommentClick}: Props) {
|
|||||||
const parentUserProfile = (
|
const parentUserProfile = (
|
||||||
<UserProfile
|
<UserProfile
|
||||||
userId={parentPostUserId}
|
userId={parentPostUserId}
|
||||||
disablePopover={false}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports[`components/ProfilePicture should match snapshot, no user specified, default props 1`] = `
|
exports[`components/ProfilePicture should match snapshot, no user specified, default props 1`] = `
|
||||||
<span
|
<span
|
||||||
className="status-wrapper style--none "
|
className="status-wrapper style--none"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="profile-icon "
|
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`] = `
|
exports[`components/ProfilePicture should match snapshot, no user specified, overridden props 1`] = `
|
||||||
<span
|
<span
|
||||||
className="status-wrapper style--none "
|
className="status-wrapper style--none"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="profile-icon "
|
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`] = `
|
exports[`components/ProfilePicture should match snapshot, profile and src, default props 1`] = `
|
||||||
<OverlayTrigger
|
<ProfilePopoverController
|
||||||
defaultOverlayShown={false}
|
src="http://example.com/image.png"
|
||||||
overlay={
|
triggerComponentClass="status-wrapper"
|
||||||
<Memo(ProfilePopover)
|
userId="uid"
|
||||||
className="user-profile-popover"
|
|
||||||
hide={[Function]}
|
|
||||||
src="http://example.com/image.png"
|
|
||||||
userId="uid"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
placement="right"
|
|
||||||
rootClose={true}
|
|
||||||
trigger={
|
|
||||||
Array [
|
|
||||||
"click",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span
|
<RoundButton
|
||||||
className="status-wrapper "
|
className="style--none"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<RoundButton
|
<span
|
||||||
className="style--none"
|
className="profile-icon "
|
||||||
size="md"
|
|
||||||
>
|
>
|
||||||
<span
|
<Memo(Avatar)
|
||||||
className="profile-icon "
|
url="http://example.com/emoji.png"
|
||||||
>
|
/>
|
||||||
<Memo(Avatar)
|
</span>
|
||||||
size="md"
|
</RoundButton>
|
||||||
tabIndex={-1}
|
<Memo(StatusIcon)
|
||||||
url="http://example.com/emoji.png"
|
status="away"
|
||||||
/>
|
/>
|
||||||
</span>
|
</ProfilePopoverController>
|
||||||
</RoundButton>
|
|
||||||
<Memo(StatusIcon)
|
|
||||||
status="away"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</OverlayTrigger>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/ProfilePicture should match snapshot, user specified 1`] = `
|
exports[`components/ProfilePicture should match snapshot, user specified 1`] = `
|
||||||
<span
|
<span
|
||||||
className="status-wrapper style--none "
|
className="status-wrapper style--none"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="profile-icon "
|
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`] = `
|
exports[`components/ProfilePicture should match snapshot, user specified, overridden props 1`] = `
|
||||||
<span
|
<span
|
||||||
className="status-wrapper style--none "
|
className="status-wrapper style--none"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="profile-icon "
|
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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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';
|
@import 'utils/mixins';
|
||||||
|
|
||||||
.user-popover__set-custom-status-btn {
|
.user-profile-popover-floating-overlay {
|
||||||
display: flex;
|
// 99 being the z-index of the global header
|
||||||
width: max-content;
|
// 1060 being the z-index of the user group popover
|
||||||
align-items: center;
|
z-index: 1070;
|
||||||
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 {
|
.user-profile-popover {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
max-height: calc(100vh - 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;
|
overflow-y: auto;
|
||||||
|
|
||||||
.popover-title {
|
.user-profile-popover-title {
|
||||||
overflow: unset;
|
display: flex;
|
||||||
max-width: 110%;
|
flex-wrap: nowrap;
|
||||||
padding: 0;
|
align-items: center;
|
||||||
border-bottom: none;
|
justify-content: end;
|
||||||
margin-top: 8px;
|
padding: 12px 8px 12px 8px;
|
||||||
margin-right: -8px;
|
|
||||||
|
|
||||||
&.popover-title_height {
|
|
||||||
overflow: visible;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-popover__role {
|
.user-popover__role {
|
||||||
padding: 0 4px;
|
flex-grow: 1;
|
||||||
margin-left: 0;
|
background: unset;
|
||||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
padding-inline-start: 8px;
|
||||||
color: var(--center-channel-color-rgb);
|
|
||||||
font-family: Open Sans, sans-serif;
|
> span {
|
||||||
font-size: 10px;
|
height: 16px;
|
||||||
letter-spacing: 0.02em;
|
padding: 0 4px;
|
||||||
line-height: 16px;
|
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 {
|
.user-profile-popover-content {
|
||||||
padding: 0 16px;
|
.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;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-popover__subtitle {
|
.user-profile-popover__heading {
|
||||||
margin-bottom: 4px;
|
display: flex;
|
||||||
font-size: 11px;
|
flex-wrap: nowrap;
|
||||||
font-weight: 600;
|
align-items: center;
|
||||||
line-height: 16px;
|
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 {
|
.user-popover__time-status-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px;
|
padding: 0 16px 16px 16px;
|
||||||
gap: 12px;
|
margin: 0;
|
||||||
|
gap: 4px;
|
||||||
|
row-gap: 8px;
|
||||||
|
|
||||||
&.first {
|
.user-popover__bottom-row-end {
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
.callButtonContainer {
|
justify-items: flex-end;
|
||||||
height: unset;
|
row-gap: 4px;
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {getDirectChannelName} from 'utils/utils';
|
|||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
import ProfilePopover from '.';
|
import ProfilePopover from './profile_popover';
|
||||||
|
|
||||||
jest.mock('@mattermost/client', () => ({
|
jest.mock('@mattermost/client', () => ({
|
||||||
...jest.requireActual('@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 {useIntl} from 'react-intl';
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
import {AccountPlusOutlineIcon} from '@mattermost/compass-icons/components';
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
import {canManageAnyChannelMembersInCurrentTeam as getCanManageAnyChannelMembersInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
|
import {canManageAnyChannelMembersInCurrentTeam as getCanManageAnyChannelMembersInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getCurrentTeam, getTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeam, getTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
||||||
|
|
||||||
import AddUserToChannelModal from 'components/add_user_to_channel_modal';
|
import AddUserToChannelModal from 'components/add_user_to_channel_modal';
|
||||||
import OverlayTrigger from 'components/overlay_trigger';
|
|
||||||
import ToggleModalButton from 'components/toggle_modal_button';
|
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';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
@@ -33,7 +31,7 @@ function getIsInCurrentTeam(state: GlobalState, userId: string) {
|
|||||||
return Boolean(teamMember) && teamMember?.delete_at === 0;
|
return Boolean(teamMember) && teamMember?.delete_at === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddToChannel = ({
|
const ProfilePopoverAddToChannel = ({
|
||||||
handleCloseModals,
|
handleCloseModals,
|
||||||
returnFocus,
|
returnFocus,
|
||||||
user,
|
user,
|
||||||
@@ -52,42 +50,38 @@ const AddToChannel = ({
|
|||||||
if (!canManageAnyChannelMembersInCurrentTeam || !isInCurrentTeam) {
|
if (!canManageAnyChannelMembersInCurrentTeam || !isInCurrentTeam) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const addToChannelMessage = formatMessage({
|
|
||||||
id: 'user_profile.add_user_to_channel',
|
|
||||||
defaultMessage: 'Add to a Channel',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OverlayTrigger
|
<WithTooltip
|
||||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
id='user_profile.add_user_to_channel.icon'
|
||||||
|
title={formatMessage({
|
||||||
|
id: 'user_profile.add_user_to_channel',
|
||||||
|
defaultMessage: 'Add to a Channel',
|
||||||
|
})}
|
||||||
placement='top'
|
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
|
<ToggleModalButton
|
||||||
id='addToChannelButton'
|
id='addToChannelButton'
|
||||||
className='btn icon-btn'
|
className='btn btn-icon btn-sm'
|
||||||
ariaLabel={addToChannelMessage}
|
ariaLabel={formatMessage({
|
||||||
|
id: 'user_profile.add_user_to_channel',
|
||||||
|
defaultMessage: 'Add to a Channel',
|
||||||
|
})}
|
||||||
modalId={ModalIdentifiers.ADD_USER_TO_CHANNEL}
|
modalId={ModalIdentifiers.ADD_USER_TO_CHANNEL}
|
||||||
dialogType={AddUserToChannelModal}
|
dialogType={AddUserToChannelModal}
|
||||||
dialogProps={{user, onExited: returnFocus}}
|
dialogProps={{user, onExited: returnFocus}}
|
||||||
onClick={handleAddToChannel}
|
onClick={handleAddToChannel}
|
||||||
>
|
>
|
||||||
<AccountPlusOutlineIcon
|
<i
|
||||||
size={18}
|
className='icon icon-account-plus-outline'
|
||||||
aria-label={formatMessage({
|
aria-hidden='true'
|
||||||
id: 'user_profile.add_user_to_channel.icon',
|
|
||||||
defaultMessage: 'Add User to Channel Icon',
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
</ToggleModalButton>
|
</ToggleModalButton>
|
||||||
</div>
|
</span>
|
||||||
</OverlayTrigger>
|
</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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {isUserInCall} from './call_button';
|
import {isUserInCall} from './index';
|
||||||
|
|
||||||
describe('isUserInCall', () => {
|
describe('isUserInCall', () => {
|
||||||
test('missing state', () => {
|
test('missing state', () => {
|
||||||
@@ -14,7 +14,7 @@ import {getChannelByName} from 'mattermost-redux/selectors/entities/channels';
|
|||||||
import {isCallsEnabled as getIsCallsEnabled, getSessionsInCalls} from 'selectors/calls';
|
import {isCallsEnabled as getIsCallsEnabled, getSessionsInCalls} from 'selectors/calls';
|
||||||
|
|
||||||
import OverlayTrigger from 'components/overlay_trigger';
|
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 Tooltip from 'components/tooltip';
|
||||||
|
|
||||||
import Constants from 'utils/constants';
|
import Constants from 'utils/constants';
|
||||||
@@ -98,7 +98,7 @@ const CallButton = ({
|
|||||||
id: 'webapp.mattermost.feature.start_call',
|
id: 'webapp.mattermost.feature.start_call',
|
||||||
defaultMessage: '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 = (
|
const callButton = (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className='callButtonContainer flex-child'
|
|
||||||
onClick={clickHandler}
|
onClick={clickHandler}
|
||||||
onTouchEnd={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 {FormattedMessage} from 'react-intl';
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
import {EmoticonHappyOutlineIcon} from '@mattermost/compass-icons/components';
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
import {CustomStatusDuration} from '@mattermost/types/users';
|
import {CustomStatusDuration} from '@mattermost/types/users';
|
||||||
|
|
||||||
@@ -32,8 +31,9 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const emojiStyles: React.CSSProperties = {
|
const emojiStyles: React.CSSProperties = {
|
||||||
marginRight: 4,
|
marginRight: 8,
|
||||||
marginTop: 1,
|
width: 16,
|
||||||
|
height: 16,
|
||||||
};
|
};
|
||||||
const ProfilePopoverCustomStatus = ({
|
const ProfilePopoverCustomStatus = ({
|
||||||
currentUserId,
|
currentUserId,
|
||||||
@@ -72,7 +72,10 @@ const ProfilePopoverCustomStatus = ({
|
|||||||
let customStatusContent;
|
let customStatusContent;
|
||||||
if (customStatusSet) {
|
if (customStatusSet) {
|
||||||
customStatusContent = (
|
customStatusContent = (
|
||||||
<div className='d-flex align-items-center'>
|
<div
|
||||||
|
className='user-popover__custom-status'
|
||||||
|
aria-labelledby='user-popover__custom-status-title'
|
||||||
|
>
|
||||||
<CustomStatusEmoji
|
<CustomStatusEmoji
|
||||||
userID={user.id}
|
userID={user.id}
|
||||||
showTooltip={false}
|
showTooltip={false}
|
||||||
@@ -81,17 +84,16 @@ const ProfilePopoverCustomStatus = ({
|
|||||||
<CustomStatusText
|
<CustomStatusText
|
||||||
tooltipDirection='top'
|
tooltipDirection='top'
|
||||||
text={customStatus.text || ''}
|
text={customStatus.text || ''}
|
||||||
className='user-popover__email'
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (canSetCustomStatus) {
|
} else if (canSetCustomStatus) {
|
||||||
customStatusContent = (
|
customStatusContent = (
|
||||||
<button
|
<button
|
||||||
className='user-popover__set-custom-status-btn'
|
className='btn btn-sm btn-quaternary user-popover__set-status'
|
||||||
onClick={showCustomStatusModal}
|
onClick={showCustomStatusModal}
|
||||||
>
|
>
|
||||||
<EmoticonHappyOutlineIcon size={14}/>
|
<i className='icon icon-emoticon-plus-outline'/>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='user_profile.custom_status.set_status'
|
id='user_profile.custom_status.set_status'
|
||||||
defaultMessage='Set a status'
|
defaultMessage='Set a status'
|
||||||
@@ -105,7 +107,10 @@ const ProfilePopoverCustomStatus = ({
|
|||||||
id='user-popover-status'
|
id='user-popover-status'
|
||||||
className='user-popover__time-status-container'
|
className='user-popover__time-status-container'
|
||||||
>
|
>
|
||||||
<span className='user-popover__subtitle'>
|
<strong
|
||||||
|
id='user-popover__custom-status-title'
|
||||||
|
className='user-popover__subtitle'
|
||||||
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='user_profile.custom_status'
|
id='user_profile.custom_status'
|
||||||
defaultMessage='Status'
|
defaultMessage='Status'
|
||||||
@@ -114,11 +119,10 @@ const ProfilePopoverCustomStatus = ({
|
|||||||
<ExpiryTime
|
<ExpiryTime
|
||||||
time={customStatus.expires_at!} // has to be defined since showExpiryTime is true
|
time={customStatus.expires_at!} // has to be defined since showExpiryTime is true
|
||||||
timezone={currentUserTimezone}
|
timezone={currentUserTimezone}
|
||||||
className='ml-1'
|
|
||||||
withinBrackets={true}
|
withinBrackets={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</span>
|
</strong>
|
||||||
{customStatusContent}
|
{customStatusContent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string;
|
email?: string;
|
||||||
haveOverrideProp: boolean;
|
haveOverrideProp?: boolean;
|
||||||
isBot: boolean;
|
isBot?: boolean;
|
||||||
}
|
}
|
||||||
const ProfilePopoverEmail = ({
|
const ProfilePopoverEmail = ({
|
||||||
email,
|
email,
|
||||||
@@ -16,14 +16,19 @@ const ProfilePopoverEmail = ({
|
|||||||
if (!email || isBot || haveOverrideProp) {
|
if (!email || isBot || haveOverrideProp) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-toggle='tooltip'
|
|
||||||
title={email}
|
title={email}
|
||||||
|
className='user-profile-popover__email'
|
||||||
|
|
||||||
>
|
>
|
||||||
|
<i
|
||||||
|
className='icon icon-email-outline'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
<a
|
<a
|
||||||
href={'mailto:' + email}
|
href={'mailto:' + email}
|
||||||
className='text-nowrap text-lowercase user-popover__email pb-1'
|
|
||||||
>
|
>
|
||||||
{email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type Props = {
|
|||||||
username: string;
|
username: string;
|
||||||
remoteId?: string;
|
remoteId?: string;
|
||||||
}
|
}
|
||||||
const FullName = ({
|
const ProfilePopoverFullName = ({
|
||||||
fullname,
|
fullname,
|
||||||
username,
|
username,
|
||||||
remoteId,
|
remoteId,
|
||||||
@@ -19,7 +19,7 @@ const FullName = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sharedIcon;
|
let sharedIcon = null;
|
||||||
if (remoteId) {
|
if (remoteId) {
|
||||||
sharedIcon = (
|
sharedIcon = (
|
||||||
<SharedUserIndicator
|
<SharedUserIndicator
|
||||||
@@ -29,14 +29,16 @@ const FullName = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid={`popover-fullname-${username}`}
|
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}
|
{sharedIcon}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default FullName;
|
|
||||||
|
export default ProfilePopoverFullName;
|
||||||
@@ -5,10 +5,10 @@ import React from 'react';
|
|||||||
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
import BotDescription from './bot_description';
|
import BotDescription from 'components/profile_popover/profile_popover_bot_description';
|
||||||
import FullName from './full_name';
|
import FullName from 'components/profile_popover/profile_popover_full_name';
|
||||||
import Position from './position';
|
import Position from 'components/profile_popover/profile_popover_position';
|
||||||
import UserName from './user_name';
|
import UserName from 'components/profile_popover/profile_popover_user_name';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
haveOverrideProp: boolean;
|
haveOverrideProp: boolean;
|
||||||
@@ -27,19 +27,20 @@ const ProfilePopoverName = ({
|
|||||||
remoteId={user.remote_id}
|
remoteId={user.remote_id}
|
||||||
username={user.username}
|
username={user.username}
|
||||||
/>
|
/>
|
||||||
<BotDescription
|
{(user.is_bot && !haveOverrideProp) && (
|
||||||
botDescription={user.bot_description}
|
<BotDescription
|
||||||
haveOverrideProp={haveOverrideProp}
|
botDescription={user.bot_description}
|
||||||
isBot={user.is_bot}
|
/>
|
||||||
/>
|
)}
|
||||||
<UserName
|
<UserName
|
||||||
hasFullName={Boolean(fullname)}
|
hasFullName={Boolean(fullname)}
|
||||||
username={user.username}
|
username={user.username}
|
||||||
/>
|
/>
|
||||||
<Position
|
{(user.position && !haveOverrideProp) && (
|
||||||
haveOverrideProp={haveOverrideProp}
|
<Position
|
||||||
position={user.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.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
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 type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
import AddToChannel from './add_to_channel';
|
import ProfilePopoverAddToChannel from 'components/profile_popover/profile_popover_add_to_channel';
|
||||||
import CallButton from './call_button';
|
import ProfilePopoverCallButtonWrapper from 'components/profile_popover/profile_popover_call_button_wrapper';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: UserProfile;
|
user: UserProfile;
|
||||||
@@ -21,7 +20,7 @@ type Props = {
|
|||||||
hide?: () => void;
|
hide?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProfilePopoverActions = ({
|
const ProfilePopoverOtherUserRow = ({
|
||||||
currentUserId,
|
currentUserId,
|
||||||
haveOverrideProp,
|
haveOverrideProp,
|
||||||
user,
|
user,
|
||||||
@@ -31,42 +30,34 @@ const ProfilePopoverActions = ({
|
|||||||
hide,
|
hide,
|
||||||
fullname,
|
fullname,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const {formatMessage} = useIntl();
|
|
||||||
|
|
||||||
if (user.id === currentUserId || haveOverrideProp) {
|
if (user.id === currentUserId || haveOverrideProp) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='user-popover__bottom-row-container'>
|
||||||
data-toggle='tooltip'
|
|
||||||
className='popover__row first'
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
id='messageButton'
|
|
||||||
type='button'
|
type='button'
|
||||||
className='btn'
|
className='btn btn-primary btn-sm'
|
||||||
onClick={handleShowDirectChannel}
|
onClick={handleShowDirectChannel}
|
||||||
>
|
>
|
||||||
<SendIcon
|
<i
|
||||||
size={16}
|
className='icon icon-send'
|
||||||
aria-label={formatMessage({
|
aria-hidden='true'
|
||||||
id: 'user_profile.send.dm.icon',
|
|
||||||
defaultMessage: 'Send Message Icon',
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='user_profile.send.dm'
|
id='user_profile.send.dm'
|
||||||
defaultMessage='Message'
|
defaultMessage='Message'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div className='popover_row-controlContainer'>
|
<div className='user-popover__bottom-row-end'>
|
||||||
<AddToChannel
|
<ProfilePopoverAddToChannel
|
||||||
handleCloseModals={handleCloseModals}
|
handleCloseModals={handleCloseModals}
|
||||||
returnFocus={returnFocus}
|
returnFocus={returnFocus}
|
||||||
user={user}
|
user={user}
|
||||||
hide={hide}
|
hide={hide}
|
||||||
/>
|
/>
|
||||||
<CallButton
|
<ProfilePopoverCallButtonWrapper
|
||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
fullname={fullname}
|
fullname={fullname}
|
||||||
userId={user.id}
|
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.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -12,23 +12,24 @@ const ProfilePopoverOverrideDisclaimer = ({
|
|||||||
username,
|
username,
|
||||||
haveOverrideProp,
|
haveOverrideProp,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
if (!haveOverrideProp) {
|
if (!haveOverrideProp) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<p
|
||||||
data-toggle='tooltip'
|
className='user-popover__bottom-row-container'
|
||||||
className='popover__row first'
|
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
{formatMessage({
|
||||||
id='user_profile.account.post_was_created'
|
id: 'user_profile.account.post_was_created',
|
||||||
defaultMessage='This post was created by an integration from @{username}'
|
defaultMessage: 'This post was created by an integration from @{username}',
|
||||||
values={{
|
},
|
||||||
username,
|
{
|
||||||
}}
|
username,
|
||||||
/>
|
})}
|
||||||
</div>
|
</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 {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import {useDispatch} from 'react-redux';
|
import {useDispatch} from 'react-redux';
|
||||||
|
|
||||||
import {AccountOutlineIcon, SendIcon} from '@mattermost/compass-icons/components';
|
|
||||||
|
|
||||||
import {openModal} from 'actions/views/modals';
|
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 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 = {
|
type Props = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -25,7 +22,7 @@ type Props = {
|
|||||||
handleShowDirectChannel: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
handleShowDirectChannel: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfilePopoverEdit = ({
|
const ProfilePopoverSelfUserRow = ({
|
||||||
userId,
|
userId,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
haveOverrideProp,
|
haveOverrideProp,
|
||||||
@@ -51,59 +48,43 @@ const ProfilePopoverEdit = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendMessageTooltip = (
|
|
||||||
<Tooltip id='sendMessageTooltip'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='user_profile.send.dm.yourself'
|
|
||||||
defaultMessage='Send yourself a message'
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-toggle='tooltip'
|
className='user-popover__bottom-row-container'
|
||||||
className='popover__row first'
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
id='editProfileButton'
|
|
||||||
type='button'
|
type='button'
|
||||||
className='btn'
|
className='btn btn-primary btn-sm'
|
||||||
onClick={handleEditAccountSettings}
|
onClick={handleEditAccountSettings}
|
||||||
>
|
>
|
||||||
<AccountOutlineIcon
|
<i
|
||||||
size={16}
|
className='icon icon-account-outline'
|
||||||
aria-label={formatMessage({
|
aria-hidden='true'
|
||||||
id: 'generic_icons.edit',
|
|
||||||
defaultMessage: 'Edit Icon',
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='user_profile.account.editProfile'
|
id='user_profile.account.editProfile'
|
||||||
defaultMessage='Edit Profile'
|
defaultMessage='Edit Profile'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<OverlayTrigger
|
<WithTooltip
|
||||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
id='user_profile.send.dm.yourself'
|
||||||
|
title={formatMessage({id: 'user_profile.send.dm.yourself', defaultMessage: 'Send yourself a message'})}
|
||||||
placement='top'
|
placement='top'
|
||||||
overlay={sendMessageTooltip}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn icon-btn'
|
className='btn btn-icon btn-sm'
|
||||||
onClick={handleShowDirectChannel}
|
onClick={handleShowDirectChannel}
|
||||||
|
aria-label={formatMessage({id: 'user_profile.send.dm.yourself', defaultMessage: 'Send yourself a message'})}
|
||||||
>
|
>
|
||||||
<SendIcon
|
<i
|
||||||
size={18}
|
className='icon icon-send'
|
||||||
aria-label={formatMessage({
|
aria-hidden='true'
|
||||||
id: 'user_profile.send.dm.icon',
|
|
||||||
defaultMessage: 'Send Message Icon',
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</OverlayTrigger>
|
</WithTooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfilePopoverEdit;
|
export default ProfilePopoverSelfUserRow;
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import {DateTime, Duration} from 'luxon';
|
import {DateTime, Duration} from 'luxon';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
import type {UserTimezone} from '@mattermost/types/users';
|
import type {UserTimezone} from '@mattermost/types/users';
|
||||||
|
|
||||||
@@ -17,10 +17,16 @@ type ProfileTimezoneProps = {
|
|||||||
haveOverrideProp: boolean;
|
haveOverrideProp: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const returnTimeDiff = (
|
const TimeZoneDifference = ({
|
||||||
currentUserTimezone: string | undefined | null,
|
currentUserTimezone,
|
||||||
profileUserTimezone: string,
|
profileUserTimezone,
|
||||||
|
}: {
|
||||||
|
currentUserTimezone: string | undefined | null;
|
||||||
|
profileUserTimezone: string;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
if (!currentUserTimezone) {
|
if (!currentUserTimezone) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -32,23 +38,33 @@ const returnTimeDiff = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!offset.valueOf()) {
|
if (!offset.valueOf()) {
|
||||||
return undefined;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeOffset = offset.toHuman({unitDisplay: 'short', signDisplay: 'never'});
|
const timeOffset = offset.toHuman({unitDisplay: 'short', signDisplay: 'never'});
|
||||||
|
|
||||||
return offset.valueOf() > 0 ? (
|
return offset.valueOf() > 0 ? (
|
||||||
<FormattedMessage
|
<>
|
||||||
id='user_profile.account.hoursAhead'
|
{formatMessage(
|
||||||
defaultMessage='({timeOffset} ahead)'
|
{
|
||||||
values={{timeOffset}}
|
id: 'user_profile.account.hoursAhead',
|
||||||
/>
|
defaultMessage: '({timeOffset} ahead)',
|
||||||
|
},
|
||||||
|
{timeOffset},
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage
|
<>
|
||||||
id='user_profile.account.hoursBehind'
|
{
|
||||||
defaultMessage='({timeOffset} behind)'
|
formatMessage(
|
||||||
values={{timeOffset}}
|
{
|
||||||
/>
|
id: 'user_profile.account.hoursBehind',
|
||||||
|
defaultMessage: '({timeOffset} behind)',
|
||||||
|
},
|
||||||
|
{timeOffset},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,6 +73,8 @@ const ProfileTimezone = ({
|
|||||||
profileUserTimezone,
|
profileUserTimezone,
|
||||||
haveOverrideProp,
|
haveOverrideProp,
|
||||||
}: ProfileTimezoneProps) => {
|
}: ProfileTimezoneProps) => {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
if (haveOverrideProp || !profileUserTimezone) {
|
if (haveOverrideProp || !profileUserTimezone) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -68,24 +86,27 @@ const ProfileTimezone = ({
|
|||||||
<div
|
<div
|
||||||
className='user-popover__time-status-container'
|
className='user-popover__time-status-container'
|
||||||
>
|
>
|
||||||
<span className='user-popover__subtitle'>
|
<strong
|
||||||
{profileTimezoneShort ? (
|
id='user-popover__timezone-title'
|
||||||
<FormattedMessage
|
className='user-popover__subtitle'
|
||||||
id='user_profile.account.localTimeWithTimezone'
|
>
|
||||||
defaultMessage='Local Time ({timezone})'
|
{profileTimezoneShort ? formatMessage(
|
||||||
values={{
|
{
|
||||||
timezone: profileTimezoneShort,
|
id: 'user_profile.account.localTimeWithTimezone',
|
||||||
}}
|
defaultMessage: 'Local Time ({timezone})',
|
||||||
/>
|
},
|
||||||
) : (
|
{
|
||||||
<FormattedMessage
|
timezone: profileTimezoneShort,
|
||||||
id='user_profile.account.localTime'
|
},
|
||||||
defaultMessage='Local Time'
|
) : formatMessage({
|
||||||
/>
|
id: 'user_profile.account.localTime',
|
||||||
)}
|
defaultMessage: 'Local Time',
|
||||||
|
})}
|
||||||
</span>
|
</strong>
|
||||||
<span>
|
<p
|
||||||
|
aria-labelledby='user-popover__timezone-title'
|
||||||
|
className='user-popover__subtitle-text'
|
||||||
|
>
|
||||||
<Timestamp
|
<Timestamp
|
||||||
useRelative={false}
|
useRelative={false}
|
||||||
useDate={false}
|
useDate={false}
|
||||||
@@ -96,9 +117,11 @@ const ProfileTimezone = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{' '}
|
{' '}
|
||||||
{returnTimeDiff(currentUserTimezone, profileTimezone)}
|
<TimeZoneDifference
|
||||||
</span>
|
currentUserTimezone={currentUserTimezone}
|
||||||
|
profileUserTimezone={profileTimezone}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import React, {useEffect, useRef} from 'react';
|
||||||
import React, {useCallback} from 'react';
|
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
import {CloseIcon} from '@mattermost/compass-icons/components';
|
|
||||||
|
|
||||||
import {getChannelMember} from 'mattermost-redux/selectors/entities/channels';
|
import {getChannelMember} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getCurrentTeam, getTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeam, getTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {isGuest, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
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 GuestTag from 'components/widgets/tag/guest_tag';
|
||||||
import Tag from 'components/widgets/tag/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';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isBot: boolean;
|
isBot?: boolean;
|
||||||
roles: string;
|
roles: string;
|
||||||
username: string;
|
|
||||||
returnFocus: () => void;
|
returnFocus: () => void;
|
||||||
hide?: () => void;
|
hide?: () => void;
|
||||||
userId: string;
|
userId: string;
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
closeButtonRef: React.RefObject<HTMLButtonElement>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIsTeamAdmin(state: GlobalState, userId: string) {
|
function getIsTeamAdmin(state: GlobalState, userId: string) {
|
||||||
@@ -55,22 +53,34 @@ function getIsChannelAdmin(state: GlobalState, userId: string, channelId?: strin
|
|||||||
const ProfilePopoverTitle = ({
|
const ProfilePopoverTitle = ({
|
||||||
isBot,
|
isBot,
|
||||||
roles,
|
roles,
|
||||||
username,
|
|
||||||
returnFocus,
|
returnFocus,
|
||||||
hide,
|
hide,
|
||||||
userId,
|
userId,
|
||||||
channelId,
|
channelId,
|
||||||
closeButtonRef,
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
const closeRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const isTeamAdmin = useSelector((state: GlobalState) => getIsTeamAdmin(state, userId));
|
const isTeamAdmin = useSelector((state: GlobalState) => getIsTeamAdmin(state, userId));
|
||||||
const isChannelAdmin = useSelector((state: GlobalState) => getIsChannelAdmin(state, userId, channelId));
|
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?.();
|
hide?.();
|
||||||
returnFocus();
|
returnFocus();
|
||||||
}, [hide, returnFocus]);
|
}
|
||||||
|
|
||||||
let roleTitle;
|
let roleTitle;
|
||||||
if (isBot) {
|
if (isBot) {
|
||||||
@@ -93,7 +103,7 @@ const ProfilePopoverTitle = ({
|
|||||||
className='user-popover__role'
|
className='user-popover__role'
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
text={formatMessage({
|
text={formatMessage({
|
||||||
id: 'admin.permissions.roles.system_admin.name',
|
id: 'user_profile.roleTitle.system_admin',
|
||||||
defaultMessage: 'System Admin',
|
defaultMessage: 'System Admin',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -104,7 +114,7 @@ const ProfilePopoverTitle = ({
|
|||||||
className='user-popover__role'
|
className='user-popover__role'
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
text={formatMessage({
|
text={formatMessage({
|
||||||
id: 'admin.permissions.roles.team_admin.name',
|
id: 'user_profile.roleTitle.team_admin',
|
||||||
defaultMessage: 'Team Admin',
|
defaultMessage: 'Team Admin',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -115,28 +125,24 @@ const ProfilePopoverTitle = ({
|
|||||||
className='user-popover__role'
|
className='user-popover__role'
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
text={formatMessage({
|
text={formatMessage({
|
||||||
id: 'admin.permissions.roles.channel_admin.name',
|
id: 'user_profile.roleTitle.channel_admin',
|
||||||
defaultMessage: 'Channel Admin',
|
defaultMessage: 'Channel Admin',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const titleClassName = classNames('popover-title', {'popover-title_height': !roleTitle});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={titleClassName}>
|
<div className='user-profile-popover-title'>
|
||||||
<span data-testid={`profilePopoverTitle_${username}`}>
|
{roleTitle}
|
||||||
{roleTitle}
|
<button
|
||||||
<button
|
ref={closeRef}
|
||||||
ref={closeButtonRef}
|
className='btn btn-icon btn-sm closeButtonRelativePosition'
|
||||||
className='user-popover__close'
|
onClick={handleClose}
|
||||||
onClick={handleClose}
|
aria-label={formatMessage({id: 'user_profile.close', defaultMessage: 'Close user profile popover'})}
|
||||||
>
|
>
|
||||||
<CloseIcon
|
<i className='icon icon-close'/>
|
||||||
size={18}
|
</button>
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -9,19 +8,21 @@ type Props = {
|
|||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserName = ({
|
const ProfilePopoverUserName = ({
|
||||||
hasFullName,
|
hasFullName,
|
||||||
username,
|
username,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const userNameClass = classNames('overflow--ellipsis pb-1', {'user-profile-popover__heading': !hasFullName});
|
|
||||||
return (
|
return (
|
||||||
<div
|
<p
|
||||||
id='userPopoverUsername'
|
id='userPopoverUsername'
|
||||||
className={userNameClass}
|
className={
|
||||||
|
hasFullName ? 'user-profile-popover__non-heading' : 'user-profile-popover__heading'
|
||||||
|
}
|
||||||
|
title={username}
|
||||||
>
|
>
|
||||||
{`@${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]}
|
channelLeaveHandler={[Function]}
|
||||||
icon={
|
icon={
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
hasMention={false}
|
|
||||||
isEmoji={false}
|
|
||||||
newStatusIcon={true}
|
newStatusIcon={true}
|
||||||
popoverPlacement="right"
|
|
||||||
size="xs"
|
size="xs"
|
||||||
src="/api/v4/users/user_id/image"
|
src="/api/v4/users/user_id/image"
|
||||||
statusClass="DirectChannel__status-icon "
|
statusClass="DirectChannel__status-icon "
|
||||||
@@ -64,10 +61,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match
|
|||||||
channelLeaveHandler={[Function]}
|
channelLeaveHandler={[Function]}
|
||||||
icon={
|
icon={
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
hasMention={false}
|
|
||||||
isEmoji={false}
|
|
||||||
newStatusIcon={true}
|
newStatusIcon={true}
|
||||||
popoverPlacement="right"
|
|
||||||
size="xs"
|
size="xs"
|
||||||
src="/api/v4/users/user_id/image"
|
src="/api/v4/users/user_id/image"
|
||||||
status=""
|
status=""
|
||||||
@@ -105,10 +99,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match
|
|||||||
channelLeaveHandler={[Function]}
|
channelLeaveHandler={[Function]}
|
||||||
icon={
|
icon={
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
hasMention={false}
|
|
||||||
isEmoji={false}
|
|
||||||
newStatusIcon={true}
|
newStatusIcon={true}
|
||||||
popoverPlacement="right"
|
|
||||||
size="xs"
|
size="xs"
|
||||||
src="/api/v4/users/user_id/image"
|
src="/api/v4/users/user_id/image"
|
||||||
statusClass="DirectChannel__status-icon "
|
statusClass="DirectChannel__status-icon "
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ exports[`at mention suggestion Should display nick name of non signed in user 1`
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={[Function]}
|
onError={[Function]}
|
||||||
src="/api/v4/users/userid2/image?_=0"
|
src="/api/v4/users/userid2/image?_=0"
|
||||||
tabIndex={0}
|
|
||||||
/>
|
/>
|
||||||
</Memo(Avatar)>
|
</Memo(Avatar)>
|
||||||
</span>
|
</span>
|
||||||
@@ -155,7 +154,6 @@ exports[`at mention suggestion Should not display nick name of the signed in use
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={[Function]}
|
onError={[Function]}
|
||||||
src="/api/v4/users/userid1/image?_=0"
|
src="/api/v4/users/userid1/image?_=0"
|
||||||
tabIndex={0}
|
|
||||||
/>
|
/>
|
||||||
</Memo(Avatar)>
|
</Memo(Avatar)>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ const AtMentionSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<Ite
|
|||||||
<SharedUserIndicator
|
<SharedUserIndicator
|
||||||
id={`sharedUserIndicator-${item.id}`}
|
id={`sharedUserIndicator-${item.id}`}
|
||||||
className='shared-user-icon'
|
className='shared-user-icon'
|
||||||
withTooltip={true}
|
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ const SearchUserSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<Us
|
|||||||
<SharedUserIndicator
|
<SharedUserIndicator
|
||||||
id={`sharedUserIndicator-${item.id}`}
|
id={`sharedUserIndicator-${item.id}`}
|
||||||
className='mention__shared-user-icon'
|
className='mention__shared-user-icon'
|
||||||
withTooltip={true}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -6,3 +6,6 @@ export enum Load {
|
|||||||
LOADING,
|
LOADING,
|
||||||
FAILED,
|
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
|
<ProfilePopoverController
|
||||||
aria-haspopup="dialog"
|
hideStatus={false}
|
||||||
onClick={[Function]}
|
src="/api/v4/users/id0/image?_=0"
|
||||||
|
userId="id0"
|
||||||
>
|
>
|
||||||
<button
|
<span
|
||||||
|
aria-expanded="false"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
className="UserButton-bZNeGd lLsMM"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onPointerDown={[Function]}
|
||||||
>
|
>
|
||||||
<Memo(Avatar)
|
<UserButton>
|
||||||
className="avatar-post-preview"
|
<button
|
||||||
size="sm"
|
className="UserButton-bZNeGd lLsMM"
|
||||||
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
|
<Memo(Avatar)
|
||||||
</span>
|
className="avatar-post-preview"
|
||||||
</Username>
|
size="sm"
|
||||||
<Gap
|
tabIndex={-1}
|
||||||
className="group-member-list_gap"
|
url="/api/v4/users/id0/image?_=0"
|
||||||
>
|
username="username0"
|
||||||
<span
|
>
|
||||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
<img
|
||||||
/>
|
alt="username0 profile image"
|
||||||
</Gap>
|
className="Avatar Avatar-sm avatar-post-preview"
|
||||||
</button>
|
loading="lazy"
|
||||||
</UserButton>
|
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
|
<DMContainer
|
||||||
className="group-member-list_dm-button"
|
className="group-member-list_dm-button"
|
||||||
>
|
>
|
||||||
@@ -593,49 +604,60 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UserButton
|
<ProfilePopoverController
|
||||||
aria-haspopup="dialog"
|
hideStatus={false}
|
||||||
onClick={[Function]}
|
src="/api/v4/users/id1/image?_=0"
|
||||||
|
userId="id1"
|
||||||
>
|
>
|
||||||
<button
|
<span
|
||||||
|
aria-expanded="false"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
className="UserButton-bZNeGd lLsMM"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onPointerDown={[Function]}
|
||||||
>
|
>
|
||||||
<Memo(Avatar)
|
<UserButton>
|
||||||
className="avatar-post-preview"
|
<button
|
||||||
size="sm"
|
className="UserButton-bZNeGd lLsMM"
|
||||||
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
|
<Memo(Avatar)
|
||||||
</span>
|
className="avatar-post-preview"
|
||||||
</Username>
|
size="sm"
|
||||||
<Gap
|
tabIndex={-1}
|
||||||
className="group-member-list_gap"
|
url="/api/v4/users/id1/image?_=0"
|
||||||
>
|
username="username1"
|
||||||
<span
|
>
|
||||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
<img
|
||||||
/>
|
alt="username1 profile image"
|
||||||
</Gap>
|
className="Avatar Avatar-sm avatar-post-preview"
|
||||||
</button>
|
loading="lazy"
|
||||||
</UserButton>
|
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
|
<DMContainer
|
||||||
className="group-member-list_dm-button"
|
className="group-member-list_dm-button"
|
||||||
>
|
>
|
||||||
@@ -801,49 +823,60 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UserButton
|
<ProfilePopoverController
|
||||||
aria-haspopup="dialog"
|
hideStatus={false}
|
||||||
onClick={[Function]}
|
src="/api/v4/users/id2/image?_=0"
|
||||||
|
userId="id2"
|
||||||
>
|
>
|
||||||
<button
|
<span
|
||||||
|
aria-expanded="false"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
className="UserButton-bZNeGd lLsMM"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onPointerDown={[Function]}
|
||||||
>
|
>
|
||||||
<Memo(Avatar)
|
<UserButton>
|
||||||
className="avatar-post-preview"
|
<button
|
||||||
size="sm"
|
className="UserButton-bZNeGd lLsMM"
|
||||||
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
|
<Memo(Avatar)
|
||||||
</span>
|
className="avatar-post-preview"
|
||||||
</Username>
|
size="sm"
|
||||||
<Gap
|
tabIndex={-1}
|
||||||
className="group-member-list_gap"
|
url="/api/v4/users/id2/image?_=0"
|
||||||
>
|
username="username2"
|
||||||
<span
|
>
|
||||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
<img
|
||||||
/>
|
alt="username2 profile image"
|
||||||
</Gap>
|
className="Avatar Avatar-sm avatar-post-preview"
|
||||||
</button>
|
loading="lazy"
|
||||||
</UserButton>
|
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
|
<DMContainer
|
||||||
className="group-member-list_dm-button"
|
className="group-member-list_dm-button"
|
||||||
>
|
>
|
||||||
@@ -1009,49 +1042,60 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UserButton
|
<ProfilePopoverController
|
||||||
aria-haspopup="dialog"
|
hideStatus={false}
|
||||||
onClick={[Function]}
|
src="/api/v4/users/id3/image?_=0"
|
||||||
|
userId="id3"
|
||||||
>
|
>
|
||||||
<button
|
<span
|
||||||
|
aria-expanded="false"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
className="UserButton-bZNeGd lLsMM"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onPointerDown={[Function]}
|
||||||
>
|
>
|
||||||
<Memo(Avatar)
|
<UserButton>
|
||||||
className="avatar-post-preview"
|
<button
|
||||||
size="sm"
|
className="UserButton-bZNeGd lLsMM"
|
||||||
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
|
<Memo(Avatar)
|
||||||
</span>
|
className="avatar-post-preview"
|
||||||
</Username>
|
size="sm"
|
||||||
<Gap
|
tabIndex={-1}
|
||||||
className="group-member-list_gap"
|
url="/api/v4/users/id3/image?_=0"
|
||||||
>
|
username="username3"
|
||||||
<span
|
>
|
||||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
<img
|
||||||
/>
|
alt="username3 profile image"
|
||||||
</Gap>
|
className="Avatar Avatar-sm avatar-post-preview"
|
||||||
</button>
|
loading="lazy"
|
||||||
</UserButton>
|
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
|
<DMContainer
|
||||||
className="group-member-list_dm-button"
|
className="group-member-list_dm-button"
|
||||||
>
|
>
|
||||||
@@ -1217,49 +1261,60 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UserButton
|
<ProfilePopoverController
|
||||||
aria-haspopup="dialog"
|
hideStatus={false}
|
||||||
onClick={[Function]}
|
src="/api/v4/users/id4/image?_=0"
|
||||||
|
userId="id4"
|
||||||
>
|
>
|
||||||
<button
|
<span
|
||||||
|
aria-expanded="false"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
className="UserButton-bZNeGd lLsMM"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onPointerDown={[Function]}
|
||||||
>
|
>
|
||||||
<Memo(Avatar)
|
<UserButton>
|
||||||
className="avatar-post-preview"
|
<button
|
||||||
size="sm"
|
className="UserButton-bZNeGd lLsMM"
|
||||||
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
|
<Memo(Avatar)
|
||||||
</span>
|
className="avatar-post-preview"
|
||||||
</Username>
|
size="sm"
|
||||||
<Gap
|
tabIndex={-1}
|
||||||
className="group-member-list_gap"
|
url="/api/v4/users/id4/image?_=0"
|
||||||
>
|
username="username4"
|
||||||
<span
|
>
|
||||||
className="Gap-pVXiI hWeVKL group-member-list_gap"
|
<img
|
||||||
/>
|
alt="username4 profile image"
|
||||||
</Gap>
|
className="Avatar Avatar-sm avatar-post-preview"
|
||||||
</button>
|
loading="lazy"
|
||||||
</UserButton>
|
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
|
<DMContainer
|
||||||
className="group-member-list_dm-button"
|
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 NoResultsIndicator from 'components/no_results_indicator';
|
||||||
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
||||||
|
import ProfilePopover from 'components/profile_popover';
|
||||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||||
import SimpleTooltip from 'components/widgets/simple_tooltip';
|
import SimpleTooltip from 'components/widgets/simple_tooltip';
|
||||||
import Avatar from 'components/widgets/users/avatar';
|
import Avatar from 'components/widgets/users/avatar';
|
||||||
@@ -55,11 +56,6 @@ export type Props = {
|
|||||||
*/
|
*/
|
||||||
hide: () => void;
|
hide: () => void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to call to show a profile popover and hide parent popover
|
|
||||||
*/
|
|
||||||
showUserOverlay: (user: UserProfile) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State of current search
|
* State of current search
|
||||||
*/
|
*/
|
||||||
@@ -88,7 +84,6 @@ const GroupMemberList = (props: Props) => {
|
|||||||
teamUrl,
|
teamUrl,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
searchState,
|
searchState,
|
||||||
showUserOverlay,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -170,20 +165,23 @@ const GroupMemberList = (props: Props) => {
|
|||||||
key={user.id}
|
key={user.id}
|
||||||
role='listitem'
|
role='listitem'
|
||||||
>
|
>
|
||||||
<UserButton
|
<ProfilePopover
|
||||||
onClick={() => showUserOverlay(user)}
|
userId={user.id}
|
||||||
aria-haspopup='dialog'
|
src={Utils.imageURLForUser(user?.id ?? '')}
|
||||||
|
hideStatus={user.is_bot}
|
||||||
>
|
>
|
||||||
<Avatar
|
<UserButton>
|
||||||
username={user.username}
|
<Avatar
|
||||||
size={'sm'}
|
username={user.username}
|
||||||
url={Utils.imageURLForUser(user?.id ?? '')}
|
size={'sm'}
|
||||||
className={'avatar-post-preview'}
|
url={Utils.imageURLForUser(user?.id ?? '')}
|
||||||
tabIndex={-1}
|
className={'avatar-post-preview'}
|
||||||
/>
|
tabIndex={-1}
|
||||||
<Username className='overflow--ellipsis text-nowrap'>{name}</Username>
|
/>
|
||||||
<Gap className='group-member-list_gap'/>
|
<Username className='overflow--ellipsis text-nowrap'>{name}</Username>
|
||||||
</UserButton>
|
<Gap className='group-member-list_gap'/>
|
||||||
|
</UserButton>
|
||||||
|
</ProfilePopover>
|
||||||
<DMContainer className='group-member-list_dm-button'>
|
<DMContainer className='group-member-list_dm-button'>
|
||||||
<SimpleTooltip
|
<SimpleTooltip
|
||||||
id={`name-${user.id}`}
|
id={`name-${user.id}`}
|
||||||
|
|||||||
@@ -1,35 +1,6 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {connect} from 'react-redux';
|
import {UserGroupPopoverController} from './user_group_popover_controller';
|
||||||
import {bindActionCreators} from 'redux';
|
|
||||||
import type {Dispatch} from 'redux';
|
|
||||||
|
|
||||||
import {searchProfiles} from 'mattermost-redux/actions/users';
|
export default UserGroupPopoverController;
|
||||||
|
|
||||||
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);
|
|
||||||
|
|||||||
@@ -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 {
|
#user-group-popover .popover-content {
|
||||||
padding: 0;
|
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> = {
|
const baseProps: ComponentProps<typeof UserGroupPopover> = {
|
||||||
searchTerm: '',
|
|
||||||
group: group1,
|
group: group1,
|
||||||
showUserOverlay: jest.fn(),
|
|
||||||
hide: jest.fn(),
|
hide: jest.fn(),
|
||||||
returnFocus: jest.fn(),
|
returnFocus: jest.fn(),
|
||||||
actions: {
|
|
||||||
setPopoverSearchTerm: jest.fn(),
|
|
||||||
openModal: jest.fn(),
|
|
||||||
searchProfiles: jest.fn().mockImplementation(() => Promise.resolve()),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot', async () => {
|
test('should match snapshot', async () => {
|
||||||
@@ -129,25 +122,6 @@ describe('component/user_group_popover', () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
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 () => {
|
test('should not show search bar', async () => {
|
||||||
const store = await mockStore(initialState);
|
const store = await mockStore(initialState);
|
||||||
const wrapper = mountWithIntl(
|
const wrapper = mountWithIntl(
|
||||||
@@ -165,24 +139,6 @@ describe('component/user_group_popover', () => {
|
|||||||
expect(wrapper.find('.user-group-popover_search-bar').exists()).toBe(false);
|
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 () => {
|
test('should show users', async () => {
|
||||||
const store = await mockStore(initialState);
|
const store = await mockStore(initialState);
|
||||||
const wrapper = mountWithIntl(
|
const wrapper = mountWithIntl(
|
||||||
|
|||||||
@@ -2,97 +2,62 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import type {ChangeEvent} from 'react';
|
||||||
import React, {useEffect, useCallback, useState, useRef} from 'react';
|
import React, {useEffect, useCallback, useState, useRef} from 'react';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import {MagnifyIcon} from '@mattermost/compass-icons/components';
|
import {MagnifyIcon} from '@mattermost/compass-icons/components';
|
||||||
import type {Group} from '@mattermost/types/groups';
|
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 {QuickInput} from 'components/quick_input/quick_input';
|
||||||
import GroupMemberList from 'components/user_group_popover/group_member_list';
|
import GroupMemberList from 'components/user_group_popover/group_member_list';
|
||||||
import UserGroupsModal from 'components/user_groups_modal';
|
import UserGroupsModal from 'components/user_groups_modal';
|
||||||
import ViewUserGroupModal from 'components/view_user_group_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 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 {Load} from './constants';
|
||||||
import useShouldClose from './useShouldClose';
|
|
||||||
|
|
||||||
import './user_group_popover.scss';
|
import './user_group_popover.scss';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
|
||||||
/**
|
|
||||||
* The group corresponding to the parent popover
|
|
||||||
*/
|
|
||||||
group: Group;
|
group: Group;
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to call if parent popover should be hidden
|
|
||||||
*/
|
|
||||||
hide: () => void;
|
hide: () => void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to call if focus should be returned to triggering element
|
|
||||||
*/
|
|
||||||
returnFocus: () => void;
|
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 = ({
|
const UserGroupPopover = ({
|
||||||
actions,
|
|
||||||
group,
|
group,
|
||||||
hide,
|
hide,
|
||||||
returnFocus,
|
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) => {
|
}: Props) => {
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
const closeRef = useRef<HTMLButtonElement>(null);
|
const closeRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const searchTerm = useSelector((state: GlobalState) => state.views.search.popoverSearch);
|
||||||
|
|
||||||
const [searchState, setSearchState] = useState(Load.DONE);
|
const [searchState, setSearchState] = useState(Load.DONE);
|
||||||
|
|
||||||
const shouldClose = useShouldClose();
|
|
||||||
|
|
||||||
const doSearch = useCallback(debounce(async (term) => {
|
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) {
|
if (res.data) {
|
||||||
setSearchState(Load.DONE);
|
setSearchState(Load.DONE);
|
||||||
} else {
|
} else {
|
||||||
setSearchState(Load.FAILED);
|
setSearchState(Load.FAILED);
|
||||||
}
|
}
|
||||||
}, Constants.SEARCH_TIMEOUT_MILLISECONDS), [actions.searchProfiles]);
|
}, Constants.SEARCH_TIMEOUT_MILLISECONDS), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Focus the close button when the popover first opens
|
// Focus the close button when the popover first opens
|
||||||
@@ -107,9 +72,9 @@ const UserGroupPopover = ({
|
|||||||
|
|
||||||
// Unset the popover search term on mount and unmount
|
// Unset the popover search term on mount and unmount
|
||||||
// This is to prevent some odd rendering issues when quickly opening and closing popovers
|
// This is to prevent some odd rendering issues when quickly opening and closing popovers
|
||||||
actions.setPopoverSearchTerm('');
|
dispatch(setPopoverSearchTerm(''));
|
||||||
return () => {
|
return () => {
|
||||||
actions.setPopoverSearchTerm('');
|
dispatch(setPopoverSearchTerm(''));
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -123,26 +88,20 @@ const UserGroupPopover = ({
|
|||||||
}
|
}
|
||||||
}, [searchTerm, doSearch]);
|
}, [searchTerm, doSearch]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (shouldClose) {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
}, [hide, shouldClose]);
|
|
||||||
|
|
||||||
const openGroupsModal = () => {
|
const openGroupsModal = () => {
|
||||||
actions.openModal({
|
dispatch(openModal({
|
||||||
modalId: ModalIdentifiers.USER_GROUPS,
|
modalId: ModalIdentifiers.USER_GROUPS,
|
||||||
dialogType: UserGroupsModal,
|
dialogType: UserGroupsModal,
|
||||||
dialogProps: {
|
dialogProps: {
|
||||||
backButtonAction: openGroupsModal,
|
backButtonAction: openGroupsModal,
|
||||||
onExited: returnFocus,
|
onExited: returnFocus,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const openViewGroupModal = () => {
|
const openViewGroupModal = () => {
|
||||||
hide();
|
hide();
|
||||||
actions.openModal({
|
dispatch(openModal({
|
||||||
modalId: ModalIdentifiers.VIEW_USER_GROUP,
|
modalId: ModalIdentifiers.VIEW_USER_GROUP,
|
||||||
dialogType: ViewUserGroupModal,
|
dialogType: ViewUserGroupModal,
|
||||||
dialogProps: {
|
dialogProps: {
|
||||||
@@ -151,7 +110,7 @@ const UserGroupPopover = ({
|
|||||||
backButtonAction: openViewGroupModal,
|
backButtonAction: openViewGroupModal,
|
||||||
onExited: returnFocus,
|
onExited: returnFocus,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -159,107 +118,81 @@ const UserGroupPopover = ({
|
|||||||
returnFocus();
|
returnFocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearch = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
actions.setPopoverSearchTerm(event.target.value);
|
dispatch(setPopoverSearchTerm(event.target.value));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
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 (
|
return (
|
||||||
<Popover
|
<Body>
|
||||||
id='user-group-popover'
|
<Header>
|
||||||
{...popoverProps}
|
<Heading>
|
||||||
>
|
<Title
|
||||||
{tabCatcher}
|
className='overflow--ellipsis text-nowrap'
|
||||||
<Body
|
>
|
||||||
role='dialog'
|
{group.display_name}
|
||||||
aria-modal={true}
|
</Title>
|
||||||
onKeyDown={handleKeyDown}
|
<CloseButton
|
||||||
className={A11yClassNames.POPUP}
|
className='btn btn-sm btn-compact btn-icon'
|
||||||
aria-label={group.display_name}
|
aria-label={formatMessage({id: 'user_group_popover.close', defaultMessage: 'Close user group popover'})}
|
||||||
>
|
onClick={handleClose}
|
||||||
<Header>
|
ref={closeRef}
|
||||||
<Heading>
|
>
|
||||||
<Title
|
<i
|
||||||
className='overflow--ellipsis text-nowrap'
|
className='icon icon-close'
|
||||||
>
|
|
||||||
{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}
|
|
||||||
/>
|
/>
|
||||||
</Subtitle>
|
</CloseButton>
|
||||||
<HeaderButton
|
</Heading>
|
||||||
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'})}`}
|
<Subtitle>
|
||||||
onClick={openViewGroupModal}
|
<span className='overflow--ellipsis text-nowrap'>{'@'}{group.name}</span>
|
||||||
className='user-group-popover_header-button'
|
<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>
|
</Subtitle>
|
||||||
{group.member_count > 10 ? (
|
<HeaderButton
|
||||||
<SearchBar>
|
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'})}`}
|
||||||
<MagnifyIcon/>
|
onClick={openViewGroupModal}
|
||||||
<QuickInput
|
className='user-group-popover_header-button'
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
</Body>
|
</Header>
|
||||||
{tabCatcher}
|
{group.member_count > 10 ? (
|
||||||
</Popover>);
|
<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`
|
const Body = styled.div`
|
||||||
width: 264px;
|
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`
|
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}
|
status={statusProp}
|
||||||
size='md'
|
size='md'
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
hasMention={true}
|
|
||||||
username={user.username}
|
username={user.username}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -2,65 +2,31 @@
|
|||||||
|
|
||||||
exports[`components/UserProfile should match snapshot 1`] = `
|
exports[`components/UserProfile should match snapshot 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<OverlayTrigger
|
<ProfilePopoverController
|
||||||
defaultOverlayShown={false}
|
hideStatus={false}
|
||||||
overlay={
|
overwriteName=""
|
||||||
<Memo(ProfilePopover)
|
src="/api/v4/users/undefined/image?_=0"
|
||||||
className="user-profile-popover"
|
triggerComponentAs="button"
|
||||||
hide={[Function]}
|
triggerComponentClass="user-popover style--none"
|
||||||
hideStatus={false}
|
userId="user_id"
|
||||||
overwriteName=""
|
|
||||||
src="/api/v4/users/undefined/image?_=0"
|
|
||||||
userId="user_id"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
placement="right"
|
|
||||||
rootClose={true}
|
|
||||||
trigger={
|
|
||||||
Array [
|
|
||||||
"click",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<button
|
nickname
|
||||||
aria-label="nickname"
|
</ProfilePopoverController>
|
||||||
className="user-popover style--none"
|
|
||||||
>
|
|
||||||
nickname
|
|
||||||
</button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/UserProfile should match snapshot, when displayUsername is enabled 1`] = `
|
exports[`components/UserProfile should match snapshot, when displayUsername is enabled 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<OverlayTrigger
|
<ProfilePopoverController
|
||||||
defaultOverlayShown={false}
|
hideStatus={false}
|
||||||
overlay={
|
overwriteName=""
|
||||||
<Memo(ProfilePopover)
|
src="/api/v4/users/undefined/image?_=0"
|
||||||
className="user-profile-popover"
|
triggerComponentAs="button"
|
||||||
hide={[Function]}
|
triggerComponentClass="user-popover style--none"
|
||||||
hideStatus={false}
|
userId="user_id"
|
||||||
overwriteName=""
|
|
||||||
src="/api/v4/users/undefined/image?_=0"
|
|
||||||
userId="user_id"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
placement="right"
|
|
||||||
rootClose={true}
|
|
||||||
trigger={
|
|
||||||
Array [
|
|
||||||
"click",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<button
|
@username
|
||||||
aria-label="@username"
|
</ProfilePopoverController>
|
||||||
className="user-popover style--none"
|
|
||||||
>
|
|
||||||
@username
|
|
||||||
</button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
</Fragment>
|
</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`] = `
|
exports[`components/UserProfile should match snapshot, when user is shared 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<OverlayTrigger
|
<ProfilePopoverController
|
||||||
defaultOverlayShown={false}
|
hideStatus={false}
|
||||||
overlay={
|
overwriteName=""
|
||||||
<Memo(ProfilePopover)
|
src="/api/v4/users/undefined/image?_=0"
|
||||||
className="user-profile-popover"
|
triggerComponentAs="button"
|
||||||
hide={[Function]}
|
triggerComponentClass="user-popover style--none"
|
||||||
hideStatus={false}
|
userId="user_id"
|
||||||
overwriteName=""
|
|
||||||
src="/api/v4/users/undefined/image?_=0"
|
|
||||||
userId="user_id"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
placement="right"
|
|
||||||
rootClose={true}
|
|
||||||
trigger={
|
|
||||||
Array [
|
|
||||||
"click",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<button
|
nickname
|
||||||
aria-label="nickname"
|
</ProfilePopoverController>
|
||||||
className="user-popover style--none"
|
|
||||||
>
|
|
||||||
nickname
|
|
||||||
</button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
<SharedUserIndicator
|
<SharedUserIndicator
|
||||||
className="shared-user-icon"
|
className="shared-user-icon"
|
||||||
id="sharedUserIndicator-user_id"
|
id="sharedUserIndicator-user_id"
|
||||||
withTooltip={true}
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/UserProfile should match snapshot, with colorization 1`] = `
|
exports[`components/UserProfile should match snapshot, with colorization 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<OverlayTrigger
|
<ProfilePopoverController
|
||||||
defaultOverlayShown={false}
|
hideStatus={false}
|
||||||
overlay={
|
overwriteName=""
|
||||||
<Memo(ProfilePopover)
|
src="/api/v4/users/undefined/image?_=0"
|
||||||
className="user-profile-popover"
|
triggerComponentAs="button"
|
||||||
hide={[Function]}
|
triggerComponentClass="user-popover style--none"
|
||||||
hideStatus={false}
|
triggerComponentStyle={
|
||||||
overwriteName=""
|
Object {
|
||||||
src="/api/v4/users/undefined/image?_=0"
|
"color": "#bbd279",
|
||||||
userId="user_id"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
placement="right"
|
|
||||||
rootClose={true}
|
|
||||||
trigger={
|
|
||||||
Array [
|
|
||||||
"click",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="nickname"
|
|
||||||
className="user-popover style--none"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"color": "#bbd279",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
>
|
}
|
||||||
nickname
|
userId="user_id"
|
||||||
</button>
|
>
|
||||||
</OverlayTrigger>
|
nickname
|
||||||
|
</ProfilePopoverController>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {ConnectedProps} from 'react-redux';
|
||||||
import {connect} 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 {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getUser, makeGetDisplayName} from 'mattermost-redux/selectors/entities/users';
|
import {getUser, makeGetDisplayName} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {getIsMobileView} from 'selectors/views/browser';
|
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
import UserProfile from './user_profile';
|
import UserProfile from './user_profile';
|
||||||
|
|
||||||
type OwnProps = {
|
export type OwnProps = {
|
||||||
userId: string;
|
userId: UserProfileType['id'];
|
||||||
|
overwriteName?: string;
|
||||||
|
overwriteIcon?: string;
|
||||||
|
disablePopover?: boolean;
|
||||||
|
displayUsername?: boolean;
|
||||||
|
colorize?: boolean;
|
||||||
|
hideStatus?: boolean;
|
||||||
|
channelId?: Channel['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeMapStateToProps() {
|
function makeMapStateToProps() {
|
||||||
@@ -27,10 +36,13 @@ function makeMapStateToProps() {
|
|||||||
displayName: getDisplayName(state, ownProps.userId, true),
|
displayName: getDisplayName(state, ownProps.userId, true),
|
||||||
user,
|
user,
|
||||||
theme,
|
theme,
|
||||||
isMobileView: getIsMobileView(state),
|
|
||||||
isShared: Boolean(user && user.remote_id),
|
isShared: Boolean(user && user.remote_id),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connector = connect(makeMapStateToProps);
|
||||||
|
|
||||||
|
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
export default connect(makeMapStateToProps)(UserProfile);
|
export default connect(makeMapStateToProps)(UserProfile);
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ describe('components/UserProfile', () => {
|
|||||||
user: {username: 'username'} as UserProfileType,
|
user: {username: 'username'} as UserProfileType,
|
||||||
userId: 'user_id',
|
userId: 'user_id',
|
||||||
theme: Preferences.THEMES.onyx,
|
theme: Preferences.THEMES.onyx,
|
||||||
|
isShared: false,
|
||||||
|
dispatch: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot', () => {
|
test('should match snapshot', () => {
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 {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 ProfilePopover from 'components/profile_popover';
|
||||||
import SharedUserIndicator from 'components/shared_user_indicator';
|
import SharedUserIndicator from 'components/shared_user_indicator';
|
||||||
import BotTag from 'components/widgets/tag/bot_tag';
|
import BotTag from 'components/widgets/tag/bot_tag';
|
||||||
@@ -19,144 +15,80 @@ import {imageURLForUser} from 'utils/utils';
|
|||||||
|
|
||||||
import {generateColor} from './utils';
|
import {generateColor} from './utils';
|
||||||
|
|
||||||
export type Props = {
|
import type {OwnProps, PropsFromRedux} from './index';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class UserProfile extends PureComponent<Props> {
|
export type Props = PropsFromRedux & OwnProps;
|
||||||
private overlay?: BaseOverlayTrigger;
|
|
||||||
|
|
||||||
static defaultProps: Partial<Props> = {
|
export default function UserProfile({
|
||||||
disablePopover: false,
|
disablePopover = false,
|
||||||
displayUsername: false,
|
displayUsername = false,
|
||||||
hideStatus: false,
|
hideStatus = false,
|
||||||
isRHS: false,
|
overwriteName = '',
|
||||||
overwriteName: '',
|
colorize = false,
|
||||||
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 => {
|
let userColor = theme?.centerChannelColor;
|
||||||
if (this.overlay) {
|
if (user && theme) {
|
||||||
this.overlay.hide();
|
userColor = generateColor(user.username, theme.centerChannelBg);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
setOverlaynRef = (ref: BaseOverlayTrigger): void => {
|
let userStyle;
|
||||||
this.overlay = ref;
|
if (colorize) {
|
||||||
};
|
userStyle = {color: userColor};
|
||||||
|
}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (disablePopover) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<OverlayTrigger
|
className='user-popover'
|
||||||
ref={this.setOverlaynRef}
|
style={userStyle}
|
||||||
trigger={['click']}
|
>
|
||||||
placement={placement}
|
{name}
|
||||||
rootClose={true}
|
</div>
|
||||||
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/>}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
.group-member-username {
|
||||||
|
padding-left: 6px;
|
||||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ exports[`components/widgets/users/Avatar should match the snapshot 1`] = `
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={[Function]}
|
onError={[Function]}
|
||||||
src="test-url"
|
src="test-url"
|
||||||
tabIndex={0}
|
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -25,6 +24,5 @@ exports[`components/widgets/users/Avatar should match the snapshot only with url
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={[Function]}
|
onError={[Function]}
|
||||||
src="test-url"
|
src="test-url"
|
||||||
tabIndex={0}
|
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, {memo} from 'react';
|
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';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
|
|
||||||
@@ -56,33 +57,38 @@ const Avatar = ({
|
|||||||
text,
|
text,
|
||||||
...attrs
|
...attrs
|
||||||
}: Props & Attrs) => {
|
}: Props & Attrs) => {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
const classes = classNames(`Avatar Avatar-${size}`, attrs.className);
|
const classes = classNames(`Avatar Avatar-${size}`, attrs.className);
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...attrs}
|
{...attrs}
|
||||||
className={classes + ' Avatar-plain'}
|
className={classNames(classes, 'Avatar-plain')}
|
||||||
data-content={text}
|
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 (
|
return (
|
||||||
<img
|
<img
|
||||||
tabIndex={0}
|
|
||||||
{...attrs}
|
{...attrs}
|
||||||
className={classes}
|
className={classes}
|
||||||
alt={`${username || 'user'} profile image`}
|
alt={formatMessage({id: 'avatar.alt', defaultMessage: '{username} profile image'}, {
|
||||||
|
username: username || 'user',
|
||||||
|
})}
|
||||||
src={url}
|
src={url}
|
||||||
loading='lazy'
|
loading='lazy'
|
||||||
onError={(e) => {
|
onError={handleOnError}
|
||||||
const fallbackSrc = (url && isURLForUser(url)) ? replaceURLWithDefaultImageURL(url) : BotDefaultIcon;
|
|
||||||
|
|
||||||
if (e.currentTarget.src !== fallbackSrc) {
|
|
||||||
e.currentTarget.src = fallbackSrc;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -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';
|
import {mockStore} from 'tests/test_store';
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ describe('components/widgets/users/Avatars', () => {
|
|||||||
mountOptions,
|
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', () => {
|
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/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/6/image?_=0'}).exists()).toBe(true);
|
||||||
expect(wrapper.find(Avatar).find({url: '/api/v4/users/7/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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 type {ComponentProps, CSSProperties} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {useSelector, useDispatch} from 'react-redux';
|
import {useSelector, useDispatch} from 'react-redux';
|
||||||
import styled from 'styled-components';
|
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
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 {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getUser as selectUser, makeDisplayNameGetter} from 'mattermost-redux/selectors/entities/users';
|
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 ProfilePopover from 'components/profile_popover';
|
||||||
import SimpleTooltip, {useSynchronizedImmediate} from 'components/widgets/simple_tooltip';
|
|
||||||
import Avatar from 'components/widgets/users/avatar';
|
import Avatar from 'components/widgets/users/avatar';
|
||||||
|
import WithTooltip from 'components/with_tooltip';
|
||||||
|
|
||||||
import {t} from 'utils/i18n';
|
|
||||||
import {imageURLForUser} from 'utils/utils';
|
import {imageURLForUser} from 'utils/utils';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
@@ -30,16 +26,10 @@ import './avatars.scss';
|
|||||||
type Props = {
|
type Props = {
|
||||||
userIds: Array<UserProfile['id']>;
|
userIds: Array<UserProfile['id']>;
|
||||||
totalUsers?: number;
|
totalUsers?: number;
|
||||||
breakAt?: number;
|
|
||||||
size?: ComponentProps<typeof Avatar>['size'];
|
size?: ComponentProps<typeof Avatar>['size'];
|
||||||
fetchMissingUsers?: boolean;
|
fetchMissingUsers?: boolean;
|
||||||
disableProfileOverlay?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MMOverlayTrigger extends BaseOverlayTrigger {
|
|
||||||
hide: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OTHERS_DISPLAY_LIMIT = 99;
|
const OTHERS_DISPLAY_LIMIT = 99;
|
||||||
|
|
||||||
function countMeta<T>(
|
function countMeta<T>(
|
||||||
@@ -61,58 +51,34 @@ const displayNameGetter = makeDisplayNameGetter();
|
|||||||
|
|
||||||
function UserAvatar({
|
function UserAvatar({
|
||||||
userId,
|
userId,
|
||||||
overlayProps,
|
|
||||||
disableProfileOverlay,
|
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
userId: UserProfile['id'];
|
userId: UserProfile['id'];
|
||||||
overlayProps: Partial<ComponentProps<typeof SimpleTooltip>>;
|
|
||||||
disableProfileOverlay: boolean;
|
|
||||||
} & ComponentProps<typeof Avatar>) {
|
} & ComponentProps<typeof Avatar>) {
|
||||||
const user = useSelector((state: GlobalState) => selectUser(state, userId)) as UserProfile | undefined;
|
const user = useSelector((state: GlobalState) => selectUser(state, userId)) as UserProfile | undefined;
|
||||||
const name = useSelector((state: GlobalState) => displayNameGetter(state, true)(user));
|
const name = useSelector((state: GlobalState) => displayNameGetter(state, true)(user));
|
||||||
|
|
||||||
const profilePictureURL = userId ? imageURLForUser(userId) : '';
|
const profilePictureURL = userId ? imageURLForUser(userId) : '';
|
||||||
|
|
||||||
const overlay = useRef<MMOverlayTrigger>(null);
|
|
||||||
|
|
||||||
const hideProfilePopover = () => {
|
|
||||||
overlay.current?.hide();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OverlayTrigger
|
<ProfilePopover<HTMLButtonElement>
|
||||||
trigger='click'
|
triggerComponentAs='button'
|
||||||
disabled={disableProfileOverlay}
|
triggerComponentClass='style--none rounded-button'
|
||||||
placement='right'
|
userId={userId}
|
||||||
rootClose={true}
|
src={profilePictureURL}
|
||||||
ref={overlay}
|
|
||||||
overlay={
|
|
||||||
<ProfilePopover
|
|
||||||
className='user-profile-popover'
|
|
||||||
userId={userId}
|
|
||||||
src={profilePictureURL}
|
|
||||||
hide={hideProfilePopover}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SimpleTooltip
|
<WithTooltip
|
||||||
id={`name-${userId}`}
|
id={`tooltip-name-${userId}`}
|
||||||
content={name}
|
title={name}
|
||||||
{...overlayProps}
|
placement='top'
|
||||||
>
|
>
|
||||||
<RoundButton
|
<Avatar
|
||||||
className={'style--none'}
|
url={imageURLForUser(userId, user?.last_picture_update)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
tabIndex={-1}
|
||||||
>
|
{...props}
|
||||||
<Avatar
|
/>
|
||||||
url={imageURLForUser(userId, user?.last_picture_update)}
|
</WithTooltip>
|
||||||
tabIndex={-1}
|
</ProfilePopover>
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</RoundButton>
|
|
||||||
</SimpleTooltip>
|
|
||||||
</OverlayTrigger>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,11 +87,9 @@ function Avatars({
|
|||||||
userIds,
|
userIds,
|
||||||
totalUsers,
|
totalUsers,
|
||||||
fetchMissingUsers = true,
|
fetchMissingUsers = true,
|
||||||
disableProfileOverlay = false,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [overlayProps, setImmediate] = useSynchronizedImmediate();
|
|
||||||
const [displayUserIds, overflowUserIds, {overflowUnnamedCount, nonDisplayCount}] = countMeta(userIds, totalUsers);
|
const [displayUserIds, overflowUserIds, {overflowUnnamedCount, nonDisplayCount}] = countMeta(userIds, totalUsers);
|
||||||
const overflowNames = useSelector((state: GlobalState) => {
|
const overflowNames = useSelector((state: GlobalState) => {
|
||||||
return overflowUserIds.map((userId) => displayNameGetter(state, true)(selectUser(state, userId))).join(', ');
|
return overflowUserIds.map((userId) => displayNameGetter(state, true)(selectUser(state, userId))).join(', ');
|
||||||
@@ -142,10 +106,33 @@ function Avatars({
|
|||||||
}
|
}
|
||||||
}, [fetchMissingUsers, userIds]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`Avatars Avatars___${size}`}
|
className={`Avatars Avatars___${size}`}
|
||||||
onMouseLeave={() => setImmediate(false)}
|
|
||||||
>
|
>
|
||||||
{displayUserIds.map((id) => (
|
{displayUserIds.map((id) => (
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
@@ -153,30 +140,13 @@ function Avatars({
|
|||||||
key={id}
|
key={id}
|
||||||
userId={id}
|
userId={id}
|
||||||
size={size}
|
size={size}
|
||||||
overlayProps={overlayProps}
|
|
||||||
disableProfileOverlay={disableProfileOverlay}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{Boolean(nonDisplayCount) && (
|
{Boolean(nonDisplayCount) && (
|
||||||
<SimpleTooltip
|
<WithTooltip
|
||||||
id={'names-overflow'}
|
id={'names-overflow'}
|
||||||
{...overlayProps}
|
placement='top'
|
||||||
content={overflowUserIds.length ? formatMessage(
|
title={overflowUsersTooltip}
|
||||||
{
|
|
||||||
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},
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
style={avatarStyle}
|
style={avatarStyle}
|
||||||
@@ -184,14 +154,10 @@ function Avatars({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
text={nonDisplayCount > OTHERS_DISPLAY_LIMIT ? `${OTHERS_DISPLAY_LIMIT}+` : `+${nonDisplayCount}`}
|
text={nonDisplayCount > OTHERS_DISPLAY_LIMIT ? `${OTHERS_DISPLAY_LIMIT}+` : `+${nonDisplayCount}`}
|
||||||
/>
|
/>
|
||||||
</SimpleTooltip>
|
</WithTooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoundButton = styled.button`
|
|
||||||
border-radius: 50%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default memo(Avatars);
|
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.app": "The app **{appName}** would like the ability to access and modify your basic information.",
|
||||||
"authorize.deny": "Deny",
|
"authorize.deny": "Deny",
|
||||||
"authorize.title": "Authorize **{appName}** to Connect to Your **Mattermost** User Account",
|
"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.overflowUnnamedOnly": "{overflowUnnamedCount, plural, =1 {one other} other {# others}}",
|
||||||
"avatars.overflowUsers": "{overflowUnnamedCount, plural, =0 {{names}} =1 {{names} and one other} other {{names} and # others}}",
|
"avatars.overflowUsers": "{overflowUnnamedCount, plural, =0 {{names}} =1 {{names} and one other} other {{names} and # others}}",
|
||||||
"backstage_list.search": "Search",
|
"backstage_list.search": "Search",
|
||||||
@@ -4590,7 +4591,6 @@
|
|||||||
"pricing_modal.title": "Select a plan",
|
"pricing_modal.title": "Select a plan",
|
||||||
"pricing_modal.wantToTry": "Want to try? ",
|
"pricing_modal.wantToTry": "Want to try? ",
|
||||||
"pricing_modal.wantToUpgrade": "Want to upgrade? ",
|
"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.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.promote": "Promote",
|
||||||
"promote_to_user_modal.title": "Promote guest {username} to member",
|
"promote_to_user_modal.title": "Promote guest {username} to member",
|
||||||
@@ -5216,7 +5216,7 @@
|
|||||||
"url_input.buttonLabel.done": "Done",
|
"url_input.buttonLabel.done": "Done",
|
||||||
"url_input.buttonLabel.edit": "Edit",
|
"url_input.buttonLabel.edit": "Edit",
|
||||||
"url_input.label.url": "URL: ",
|
"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.memberCount": "{member_count} {member_count, plural, one {Member} other {Members}}",
|
||||||
"user_group_popover.openGroupModal": "View full group info",
|
"user_group_popover.openGroupModal": "View full group info",
|
||||||
"user_group_popover.searchGroupMembers": "Search members",
|
"user_group_popover.searchGroupMembers": "Search members",
|
||||||
@@ -5260,12 +5260,14 @@
|
|||||||
"user_profile.account.localTimeWithTimezone": "Local Time ({timezone})",
|
"user_profile.account.localTimeWithTimezone": "Local Time ({timezone})",
|
||||||
"user_profile.account.post_was_created": "This post was created by an integration from @{username}",
|
"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": "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.call.ongoing": "Call with {user} is ongoing",
|
||||||
|
"user_profile.close": "Close user profile popover",
|
||||||
"user_profile.custom_status": "Status",
|
"user_profile.custom_status": "Status",
|
||||||
"user_profile.custom_status.set_status": "Set a 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": "Message",
|
||||||
"user_profile.send.dm.icon": "Send Message Icon",
|
|
||||||
"user_profile.send.dm.yourself": "Send yourself a message",
|
"user_profile.send.dm.yourself": "Send yourself a message",
|
||||||
"user.settings.advance.confirmDeactivateAccountTitle": "Confirm Deactivation",
|
"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.",
|
"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 {
|
button {
|
||||||
.unread-badge {
|
.unread-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -19,52 +19,6 @@
|
|||||||
.app__body & {
|
.app__body & {
|
||||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
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 {
|
&.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 {
|
.hidden-label + .search-autocomplete__divider {
|
||||||
&::before {
|
&::before {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user