snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
import { composeStories } from '@storybook/react';
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import * as stories from './UserInfo.stories';
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />);
expect(baseElement).toMatchSnapshot();
});
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

View File

@@ -0,0 +1,72 @@
import { ContextualbarDialog } from '@rocket.chat/ui-client';
import type { Meta, StoryFn } from '@storybook/react';
import UserInfo from './UserInfo';
import { UserCardRole } from '../UserCard';
import * as Status from '../UserStatus';
import { UserStatusText } from '../UserStatusText';
export default {
component: UserInfo,
parameters: {
layout: 'fullscreen',
actions: { argTypesRegex: '^on.*' },
},
decorators: [
(fn) => (
<ContextualbarDialog aria-label='User Info' height='100vh'>
{fn()}
</ContextualbarDialog>
),
],
} satisfies Meta<typeof UserInfo>;
const defaultArgs = {
name: 'Guilherme Gazzo',
username: 'guilherme.gazzo',
nickname: 'gazzo',
bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempus, eros convallis vulputate cursus, nisi neque eleifend libero, eget lacinia justo purus nec est. In at sodales ipsum. Sed lacinia quis purus eget pulvinar. Aenean eu pretium nunc, at aliquam magna. Praesent dignissim, tortor sed volutpat mattis, mauris diam pulvinar leo, porta commodo risus est non purus.',
email: 'rocketchat@rocket.chat',
status: <Status.Offline />,
customStatus: <UserStatusText statusText='🛴 currently working on User Card' />,
roles: [<UserCardRole key='admin'>admin</UserCardRole>, <UserCardRole key='user'>user</UserCardRole>],
};
const Template: StoryFn<typeof UserInfo> = (args) => <UserInfo {...defaultArgs} {...args} />;
export const Default = {
render: Template,
};
export const WithVoiceCallExtension = {
render: Template,
args: {
freeSwitchExtension: '1234567890',
},
};
export const WithABACAttributes = {
render: Template,
args: {
abacAttributes: [
{
key: 'Classified',
values: ['Top Secret', 'Confidential'],
},
{
key: 'Security_Clearance',
values: ['Top Secret', 'Confidential'],
},
],
},
};
export const InvitedUser = {
render: Template,
args: {
invitationDate: '2025-01-01T12:00:00Z',
},
};

View File

@@ -0,0 +1,233 @@
import type { IUser, Serialized } from '@rocket.chat/core-typings';
import { Box, Margins, Tag } from '@rocket.chat/fuselage';
import {
useUserDisplayName,
ContextualbarScrollableContent,
InfoPanel,
InfoPanelActionGroup,
InfoPanelAvatar,
InfoPanelField,
InfoPanelLabel,
InfoPanelSection,
InfoPanelText,
InfoPanelTitle,
} from '@rocket.chat/ui-client';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import type { ReactNode } from 'react';
import { memo, useId } from 'react';
import { useTranslation } from 'react-i18next';
import { useTimeAgo } from '../../hooks/useTimeAgo';
import { useUserCustomFields } from '../../hooks/useUserCustomFields';
import MarkdownText from '../MarkdownText';
import UTCClock from '../UTCClock';
import { UserCardRoles } from '../UserCard';
import UserInfoABACAttributes from './UserInfoABACAttributes';
import UserInfoAvatar from './UserInfoAvatar';
type UserInfoDataProps = Serialized<
Pick<
IUser,
| 'name'
| 'username'
| 'nickname'
| 'bio'
| 'lastLogin'
| 'avatarETag'
| 'utcOffset'
| 'phone'
| 'createdAt'
| 'canViewAllInfo'
| 'customFields'
| 'freeSwitchExtension'
| 'abacAttributes'
>
>;
export type UserInfoProps = UserInfoDataProps & {
status: ReactNode;
customStatus?: ReactNode;
email?: string;
verified?: boolean;
actions: ReactNode;
roles: ReactNode[];
reason?: string;
invitationDate?: string;
};
const UserInfo = ({
username,
name,
lastLogin,
nickname,
bio,
avatarETag,
roles,
utcOffset,
phone,
email,
verified,
createdAt,
status,
customStatus,
customFields,
canViewAllInfo,
actions,
reason,
freeSwitchExtension,
abacAttributes,
invitationDate,
...props
}: UserInfoProps) => {
const { t } = useTranslation();
const timeAgo = useTimeAgo();
const userDisplayName = useUserDisplayName({ name, username });
const userCustomFields = useUserCustomFields(customFields);
const usernameId = useId();
return (
<ContextualbarScrollableContent padding={24} {...props}>
<InfoPanel>
{username && (
<InfoPanelAvatar>
<UserInfoAvatar username={username} etag={avatarETag} />
</InfoPanelAvatar>
)}
{actions && <InfoPanelActionGroup>{actions}</InfoPanelActionGroup>}
<InfoPanelSection>
{userDisplayName && <InfoPanelTitle icon={status} title={userDisplayName} />}
{customStatus && <InfoPanelText>{customStatus}</InfoPanelText>}
</InfoPanelSection>
<InfoPanelSection>
{reason && (
<InfoPanelField>
<InfoPanelLabel>{t('Reason_for_joining')}</InfoPanelLabel>
<InfoPanelText>{reason}</InfoPanelText>
</InfoPanelField>
)}
{nickname && (
<InfoPanelField>
<InfoPanelLabel>{t('Nickname')}</InfoPanelLabel>
<InfoPanelText>{nickname}</InfoPanelText>
</InfoPanelField>
)}
{roles?.length !== 0 && (
<InfoPanelField>
<InfoPanelLabel>{t('Roles')}</InfoPanelLabel>
<UserCardRoles>{roles}</UserCardRoles>
</InfoPanelField>
)}
{username && username !== name && (
<InfoPanelField is='dl'>
<InfoPanelLabel is='dt' id={usernameId}>
{t('Username')}
</InfoPanelLabel>
<InfoPanelText is='dd' aria-labelledby={usernameId}>
{username}
</InfoPanelText>
</InfoPanelField>
)}
{utcOffset && Number.isInteger(utcOffset) && (
<InfoPanelField>
<InfoPanelLabel>{t('Local_Time')}</InfoPanelLabel>
<InfoPanelText>
<UTCClock utcOffset={utcOffset} />
</InfoPanelText>
</InfoPanelField>
)}
{bio && (
<InfoPanelField>
<InfoPanelLabel>{t('Bio')}</InfoPanelLabel>
<InfoPanelText withTruncatedText={false}>
<MarkdownText variant='inline' content={bio} />
</InfoPanelText>
</InfoPanelField>
)}
{Number.isInteger(utcOffset) && canViewAllInfo && (
<InfoPanelField>
<InfoPanelLabel>{t('Last_login')}</InfoPanelLabel>
<InfoPanelText>{lastLogin ? timeAgo(lastLogin) : t('Never')}</InfoPanelText>
</InfoPanelField>
)}
{phone && (
<InfoPanelField>
<InfoPanelLabel>{t('Phone')}</InfoPanelLabel>
<InfoPanelText display='flex' flexDirection='row' alignItems='center'>
<Box is='a' withTruncatedText href={`tel:${phone}`}>
{phone}
</Box>
</InfoPanelText>
</InfoPanelField>
)}
{email && (
<InfoPanelField>
<InfoPanelLabel>{t('Email')}</InfoPanelLabel>
<InfoPanelText display='flex' flexDirection='row' alignItems='center'>
<Box is='a' withTruncatedText href={`mailto:${email}`}>
{email}
</Box>
<Margins inline={4}>
<Tag>{verified ? t('Verified') : t('Not_verified')}</Tag>
</Margins>
</InfoPanelText>
</InfoPanelField>
)}
{freeSwitchExtension && (
<InfoPanelField>
<InfoPanelLabel>{t('Voice_call_extension')}</InfoPanelLabel>
<InfoPanelText>{freeSwitchExtension}</InfoPanelText>
</InfoPanelField>
)}
{abacAttributes && abacAttributes.length > 0 && (
<InfoPanelField>
<InfoPanelLabel title={t('ABAC_Attributes_description')}>{t('ABAC_Attributes')}</InfoPanelLabel>
<UserInfoABACAttributes abacAttributes={abacAttributes} />
</InfoPanelField>
)}
{userCustomFields?.map(
(customField) =>
customField?.value && (
<InfoPanelField key={customField.value}>
<InfoPanelLabel>{t(customField.label as TranslationKey)}</InfoPanelLabel>
<InfoPanelText>
<MarkdownText content={customField.value} variant='inline' />
</InfoPanelText>
</InfoPanelField>
),
)}
{invitationDate && (
<InfoPanelField>
<InfoPanelLabel>{t('Invitation_date')}</InfoPanelLabel>
<InfoPanelText>{timeAgo(invitationDate)}</InfoPanelText>
</InfoPanelField>
)}
{createdAt && (
<InfoPanelField>
<InfoPanelLabel>{t('Created_at')}</InfoPanelLabel>
<InfoPanelText>{timeAgo(createdAt)}</InfoPanelText>
</InfoPanelField>
)}
</InfoPanelSection>
</InfoPanel>
</ContextualbarScrollableContent>
);
};
export default memo(UserInfo);

View File

@@ -0,0 +1,11 @@
import { Tag } from '@rocket.chat/fuselage';
export type UserInfoABACAttributeProps = {
attribute: string;
};
const UserInfoABACAttribute = ({ attribute }: UserInfoABACAttributeProps) => {
return <Tag variant='secondary-warning'>{attribute}</Tag>;
};
export default UserInfoABACAttribute;

View File

@@ -0,0 +1,26 @@
import type { IAbacAttributeDefinition } from '@rocket.chat/core-typings';
import { Box, Margins } from '@rocket.chat/fuselage';
import UserInfoABACAttribute from './UserInfoABACAttribute';
export type UserInfoABACAttributesProps = {
abacAttributes: IAbacAttributeDefinition[];
};
const UserInfoABACAttributes = ({ abacAttributes }: UserInfoABACAttributesProps) => {
return (
<Box margin='neg-x2'>
<Box flexWrap='wrap' display='flex' flexShrink={0} marginBlock={8}>
{abacAttributes.map((attribute, index) =>
attribute.values.map((value) => (
<Margins inline={2} blockEnd={4} key={`${attribute.key}-${value}-${index}`}>
<UserInfoABACAttribute attribute={value} />
</Margins>
)),
)}
</Box>
</Box>
);
};
export default UserInfoABACAttributes;

View File

@@ -0,0 +1,21 @@
import { Button, IconButton } from '@rocket.chat/fuselage';
import type { Keys as IconName } from '@rocket.chat/icons';
import type { ComponentProps } from 'react';
export type UserInfoActionProps = {
icon: IconName;
} & ComponentProps<typeof Button>;
const UserInfoAction = ({ icon, label, title, ...props }: UserInfoActionProps) => {
if (!label && icon && title) {
return <IconButton small secondary icon={icon} title={title} aria-label={title} {...props} marginInline={4} size={40} />;
}
return (
<Button icon={icon} {...props} marginInline={4}>
{label}
</Button>
);
};
export default UserInfoAction;

View File

@@ -0,0 +1,6 @@
import { UserAvatar } from '@rocket.chat/ui-avatar';
import type { ComponentProps } from 'react';
const UserInfoAvatar = (props: ComponentProps<typeof UserAvatar>) => <UserAvatar size='x332' {...props} />;
export default UserInfoAvatar;

View File

@@ -0,0 +1,16 @@
import type { IUser } from '@rocket.chat/core-typings';
import type { Box } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactNode } from 'react';
import { UserCardUsername } from '../UserCard';
export type UserInfoUsernameProps = {
username: IUser['username'];
status: ReactNode;
} & ComponentProps<typeof Box>;
const UserInfoUsername = ({ username, status, ...props }: UserInfoUsernameProps) => (
<UserCardUsername name={username} status={status} {...props} />
);
export default UserInfoUsername;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
export { default as UserInfo } from './UserInfo';
export { default as UserInfoAction } from './UserInfoAction';
export { default as UserInfoAvatar } from './UserInfoAvatar';
export { default as UserInfoUsername } from './UserInfoUsername';