Better invites management (#2336)

* move block users to account settings

* filter invites and add more options

* add better rate limit recovery in rateLimitedActions util function
This commit is contained in:
Ajay Bura 2025-05-24 20:07:56 +05:30 committed by GitHub
parent 0d27bde33e
commit 206ed33516
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1088 additions and 524 deletions

View file

@ -5,7 +5,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';
import { SearchOrderBy } from 'matrix-js-sdk';
import { PageHero, PageHeroSection } from '../../components/page';
import { PageHero, PageHeroEmpty, PageHeroSection } from '../../components/page';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { _SearchPathSearchParams } from '../../pages/paths';
import { useSetting } from '../../state/hooks/settings';
@ -222,18 +222,7 @@ export function MessageSearch({
</Box>
{!msgSearchParams.term && status === 'pending' && (
<Box
className={ContainerColor({ variant: 'SurfaceVariant' })}
style={{
padding: config.space.S400,
borderRadius: config.radii.R400,
minHeight: toRem(450),
}}
direction="Column"
alignItems="Center"
justifyContent="Center"
gap="200"
>
<PageHeroEmpty>
<PageHeroSection>
<PageHero
icon={<Icon size="600" src={Icons.Message} />}
@ -241,7 +230,7 @@ export function MessageSearch({
subTitle="Find helpful messages in your community by searching with related keywords."
/>
</PageHeroSection>
</Box>
</PageHeroEmpty>
)}
{msgSearchParams.term && groups.length === 0 && status === 'success' && (

View file

@ -1,396 +1,10 @@
import React, {
ChangeEventHandler,
FormEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import {
Box,
Text,
IconButton,
Icon,
Icons,
Scroll,
Input,
Avatar,
Button,
Chip,
Overlay,
OverlayBackdrop,
OverlayCenter,
Modal,
Dialog,
Header,
config,
Spinner,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import React from 'react';
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { UserAvatar } from '../../../components/user-avatar';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { nameInitials } from '../../../utils/common';
import { copyToClipboard } from '../../../utils/dom';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useFilePicker } from '../../../hooks/useFilePicker';
import { useObjectURL } from '../../../hooks/useObjectURL';
import { stopPropagation } from '../../../utils/keyboard';
import { ImageEditor } from '../../../components/image-editor';
import { ModalWide } from '../../../styles/Modal.css';
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
import { CompactUploadCardRenderer } from '../../../components/upload-card';
import { useCapabilities } from '../../../hooks/useCapabilities';
function MatrixId() {
const mx = useMatrixClient();
const userId = mx.getUserId()!;
return (
<Box direction="Column" gap="100">
<Text size="L400">Matrix ID</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title={userId}
after={
<Chip variant="Secondary" radii="Pill" onClick={() => copyToClipboard(userId)}>
<Text size="T200">Copy</Text>
</Chip>
}
/>
</SequenceCard>
</Box>
);
}
type ProfileProps = {
profile: UserProfile;
userId: string;
};
function ProfileAvatar({ profile, userId }: ProfileProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const capabilities = useCapabilities();
const [alertRemove, setAlertRemove] = useState(false);
const disableSetAvatar = capabilities['m.set_avatar_url']?.enabled === false;
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
const avatarUrl = profile.avatarUrl
? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined;
const [imageFile, setImageFile] = useState<File>();
const imageFileURL = useObjectURL(imageFile);
const uploadAtom = useMemo(() => {
if (imageFile) return createUploadAtom(imageFile);
return undefined;
}, [imageFile]);
const pickFile = useFilePicker(setImageFile, false);
const handleRemoveUpload = useCallback(() => {
setImageFile(undefined);
}, []);
const handleUploaded = useCallback(
(upload: UploadSuccess) => {
const { mxc } = upload;
mx.setAvatarUrl(mxc);
handleRemoveUpload();
},
[mx, handleRemoveUpload]
);
const handleRemoveAvatar = () => {
mx.setAvatarUrl('');
setAlertRemove(false);
};
return (
<SettingTile
title={
<Text as="span" size="L400">
Avatar
</Text>
}
after={
<Avatar size="500" radii="300">
<UserAvatar
userId={userId}
src={avatarUrl}
renderFallback={() => <Text size="H4">{nameInitials(defaultDisplayName)}</Text>}
/>
</Avatar>
}
>
{uploadAtom ? (
<Box gap="200" direction="Column">
<CompactUploadCardRenderer
uploadAtom={uploadAtom}
onRemove={handleRemoveUpload}
onComplete={handleUploaded}
/>
</Box>
) : (
<Box gap="200">
<Button
onClick={() => pickFile('image/*')}
size="300"
variant="Secondary"
fill="Soft"
outlined
radii="300"
disabled={disableSetAvatar}
>
<Text size="B300">Upload</Text>
</Button>
{avatarUrl && (
<Button
size="300"
variant="Critical"
fill="None"
radii="300"
disabled={disableSetAvatar}
onClick={() => setAlertRemove(true)}
>
<Text size="B300">Remove</Text>
</Button>
)}
</Box>
)}
{imageFileURL && (
<Overlay open={false} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: handleRemoveUpload,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal className={ModalWide} variant="Surface" size="500">
<ImageEditor
name={imageFile?.name ?? 'Unnamed'}
url={imageFileURL}
requestClose={handleRemoveUpload}
/>
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
)}
<Overlay open={alertRemove} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setAlertRemove(false),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface">
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">Remove Avatar</Text>
</Box>
<IconButton size="300" onClick={() => setAlertRemove(false)} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="200">
<Text priority="400">Are you sure you want to remove profile avatar?</Text>
</Box>
<Button variant="Critical" onClick={handleRemoveAvatar}>
<Text size="B400">Remove</Text>
</Button>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
</SettingTile>
);
}
function ProfileDisplayName({ profile, userId }: ProfileProps) {
const mx = useMatrixClient();
const capabilities = useCapabilities();
const disableSetDisplayname = capabilities['m.set_displayname']?.enabled === false;
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
const [displayName, setDisplayName] = useState<string>(defaultDisplayName);
const [changeState, changeDisplayName] = useAsyncCallback(
useCallback((name: string) => mx.setDisplayName(name), [mx])
);
const changingDisplayName = changeState.status === AsyncStatus.Loading;
useEffect(() => {
setDisplayName(defaultDisplayName);
}, [defaultDisplayName]);
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
const name = evt.currentTarget.value;
setDisplayName(name);
};
const handleReset = () => {
setDisplayName(defaultDisplayName);
};
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
if (changingDisplayName) return;
const target = evt.target as HTMLFormElement | undefined;
const displayNameInput = target?.displayNameInput as HTMLInputElement | undefined;
const name = displayNameInput?.value;
if (!name) return;
changeDisplayName(name);
};
const hasChanges = displayName !== defaultDisplayName;
return (
<SettingTile
title={
<Text as="span" size="L400">
Display Name
</Text>
}
>
<Box direction="Column" grow="Yes" gap="100">
<Box
as="form"
onSubmit={handleSubmit}
gap="200"
aria-disabled={changingDisplayName || disableSetDisplayname}
>
<Box grow="Yes" direction="Column">
<Input
required
name="displayNameInput"
value={displayName}
onChange={handleChange}
variant="Secondary"
radii="300"
style={{ paddingRight: config.space.S200 }}
readOnly={changingDisplayName || disableSetDisplayname}
after={
hasChanges &&
!changingDisplayName && (
<IconButton
type="reset"
onClick={handleReset}
size="300"
radii="300"
variant="Secondary"
>
<Icon src={Icons.Cross} size="100" />
</IconButton>
)
}
/>
</Box>
<Button
size="400"
variant={hasChanges ? 'Success' : 'Secondary'}
fill={hasChanges ? 'Solid' : 'Soft'}
outlined
radii="300"
disabled={!hasChanges || changingDisplayName}
type="submit"
>
{changingDisplayName && <Spinner variant="Success" fill="Solid" size="300" />}
<Text size="B400">Save</Text>
</Button>
</Box>
</Box>
</SettingTile>
);
}
function Profile() {
const mx = useMatrixClient();
const userId = mx.getUserId()!;
const profile = useUserProfile(userId);
return (
<Box direction="Column" gap="100">
<Text size="L400">Profile</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<ProfileAvatar userId={userId} profile={profile} />
<ProfileDisplayName userId={userId} profile={profile} />
</SequenceCard>
</Box>
);
}
function ContactInformation() {
const mx = useMatrixClient();
const [threePIdsState, loadThreePIds] = useAsyncCallback(
useCallback(() => mx.getThreePids(), [mx])
);
const threePIds =
threePIdsState.status === AsyncStatus.Success ? threePIdsState.data.threepids : undefined;
const emailIds = threePIds?.filter((id) => id.medium === 'email');
useEffect(() => {
loadThreePIds();
}, [loadThreePIds]);
return (
<Box direction="Column" gap="100">
<Text size="L400">Contact Information</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile title="Email Address" description="Email address attached to your account.">
<Box>
{emailIds?.map((email) => (
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">
<Text size="T200">{email.address}</Text>
</Chip>
))}
</Box>
{/* <Input defaultValue="" variant="Secondary" radii="300" /> */}
</SettingTile>
</SequenceCard>
</Box>
);
}
import { MatrixId } from './MatrixId';
import { Profile } from './Profile';
import { ContactInformation } from './ContactInfo';
import { IgnoredUserList } from './IgnoredUserList';
type AccountProps = {
requestClose: () => void;
@ -419,6 +33,7 @@ export function Account({ requestClose }: AccountProps) {
<Profile />
<MatrixId />
<ContactInformation />
<IgnoredUserList />
</Box>
</PageContent>
</Scroll>

View file

@ -0,0 +1,45 @@
import React, { useCallback, useEffect } from 'react';
import { Box, Text, Chip } from 'folds';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
export function ContactInformation() {
const mx = useMatrixClient();
const [threePIdsState, loadThreePIds] = useAsyncCallback(
useCallback(() => mx.getThreePids(), [mx])
);
const threePIds =
threePIdsState.status === AsyncStatus.Success ? threePIdsState.data.threepids : undefined;
const emailIds = threePIds?.filter((id) => id.medium === 'email');
useEffect(() => {
loadThreePIds();
}, [loadThreePIds]);
return (
<Box direction="Column" gap="100">
<Text size="L400">Contact Information</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile title="Email Address" description="Email address attached to your account.">
<Box>
{emailIds?.map((email) => (
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">
<Text size="T200">{email.address}</Text>
</Chip>
))}
</Box>
{/* <Input defaultValue="" variant="Secondary" radii="300" /> */}
</SettingTile>
</SequenceCard>
</Box>
);
}

View file

@ -7,16 +7,17 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { isUserId } from '../../../utils/matrix';
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
import { useAlive } from '../../../hooks/useAlive';
function IgnoreUserInput({ userList }: { userList: string[] }) {
const mx = useMatrixClient();
const [userId, setUserId] = useState<string>('');
const alive = useAlive();
const [ignoreState, ignore] = useAsyncCallback(
useCallback(
async (uId: string) => {
mx.setIgnoredUsers([...userList, uId]);
setUserId('');
await mx.setIgnoredUsers([...userList, uId]);
},
[mx, userList]
)
@ -43,7 +44,11 @@ function IgnoreUserInput({ userList }: { userList: string[] }) {
if (!isUserId(uId)) return;
ignore(uId);
ignore(uId).then(() => {
if (alive()) {
setUserId('');
}
});
};
return (
@ -129,7 +134,7 @@ export function IgnoredUserList() {
return (
<Box direction="Column" gap="100">
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<Text size="L400">Block Messages</Text>
<Text size="L400">Blocked Users</Text>
</Box>
<SequenceCard
className={SequenceCardStyle}
@ -139,13 +144,13 @@ export function IgnoredUserList() {
>
<SettingTile
title="Select User"
description="Prevent receiving message by adding userId into blocklist."
description="Prevent receiving messages or invites from user by adding their userId."
>
<Box direction="Column" gap="300">
<IgnoreUserInput userList={ignoredUsers} />
{ignoredUsers.length > 0 && (
<Box direction="Inherit" gap="100">
<Text size="L400">Blocklist</Text>
<Text size="L400">Users</Text>
<Box wrap="Wrap" gap="200">
{ignoredUsers.map((userId) => (
<IgnoredUserChip key={userId} userId={userId} userList={ignoredUsers} />

View file

@ -0,0 +1,33 @@
import React from 'react';
import { Box, Text, Chip } from 'folds';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { copyToClipboard } from '../../../../util/common';
export function MatrixId() {
const mx = useMatrixClient();
const userId = mx.getUserId()!;
return (
<Box direction="Column" gap="100">
<Text size="L400">Matrix ID</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title={userId}
after={
<Chip variant="Secondary" radii="Pill" onClick={() => copyToClipboard(userId)}>
<Text size="T200">Copy</Text>
</Chip>
}
/>
</SequenceCard>
</Box>
);
}

View file

@ -0,0 +1,325 @@
import React, {
ChangeEventHandler,
FormEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import {
Box,
Text,
IconButton,
Icon,
Icons,
Input,
Avatar,
Button,
Overlay,
OverlayBackdrop,
OverlayCenter,
Modal,
Dialog,
Header,
config,
Spinner,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { UserAvatar } from '../../../components/user-avatar';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { nameInitials } from '../../../utils/common';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useFilePicker } from '../../../hooks/useFilePicker';
import { useObjectURL } from '../../../hooks/useObjectURL';
import { stopPropagation } from '../../../utils/keyboard';
import { ImageEditor } from '../../../components/image-editor';
import { ModalWide } from '../../../styles/Modal.css';
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
import { CompactUploadCardRenderer } from '../../../components/upload-card';
import { useCapabilities } from '../../../hooks/useCapabilities';
type ProfileProps = {
profile: UserProfile;
userId: string;
};
function ProfileAvatar({ profile, userId }: ProfileProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const capabilities = useCapabilities();
const [alertRemove, setAlertRemove] = useState(false);
const disableSetAvatar = capabilities['m.set_avatar_url']?.enabled === false;
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
const avatarUrl = profile.avatarUrl
? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined;
const [imageFile, setImageFile] = useState<File>();
const imageFileURL = useObjectURL(imageFile);
const uploadAtom = useMemo(() => {
if (imageFile) return createUploadAtom(imageFile);
return undefined;
}, [imageFile]);
const pickFile = useFilePicker(setImageFile, false);
const handleRemoveUpload = useCallback(() => {
setImageFile(undefined);
}, []);
const handleUploaded = useCallback(
(upload: UploadSuccess) => {
const { mxc } = upload;
mx.setAvatarUrl(mxc);
handleRemoveUpload();
},
[mx, handleRemoveUpload]
);
const handleRemoveAvatar = () => {
mx.setAvatarUrl('');
setAlertRemove(false);
};
return (
<SettingTile
title={
<Text as="span" size="L400">
Avatar
</Text>
}
after={
<Avatar size="500" radii="300">
<UserAvatar
userId={userId}
src={avatarUrl}
renderFallback={() => <Text size="H4">{nameInitials(defaultDisplayName)}</Text>}
/>
</Avatar>
}
>
{uploadAtom ? (
<Box gap="200" direction="Column">
<CompactUploadCardRenderer
uploadAtom={uploadAtom}
onRemove={handleRemoveUpload}
onComplete={handleUploaded}
/>
</Box>
) : (
<Box gap="200">
<Button
onClick={() => pickFile('image/*')}
size="300"
variant="Secondary"
fill="Soft"
outlined
radii="300"
disabled={disableSetAvatar}
>
<Text size="B300">Upload</Text>
</Button>
{avatarUrl && (
<Button
size="300"
variant="Critical"
fill="None"
radii="300"
disabled={disableSetAvatar}
onClick={() => setAlertRemove(true)}
>
<Text size="B300">Remove</Text>
</Button>
)}
</Box>
)}
{imageFileURL && (
<Overlay open={false} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: handleRemoveUpload,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal className={ModalWide} variant="Surface" size="500">
<ImageEditor
name={imageFile?.name ?? 'Unnamed'}
url={imageFileURL}
requestClose={handleRemoveUpload}
/>
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
)}
<Overlay open={alertRemove} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setAlertRemove(false),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface">
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">Remove Avatar</Text>
</Box>
<IconButton size="300" onClick={() => setAlertRemove(false)} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="200">
<Text priority="400">Are you sure you want to remove profile avatar?</Text>
</Box>
<Button variant="Critical" onClick={handleRemoveAvatar}>
<Text size="B400">Remove</Text>
</Button>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
</SettingTile>
);
}
function ProfileDisplayName({ profile, userId }: ProfileProps) {
const mx = useMatrixClient();
const capabilities = useCapabilities();
const disableSetDisplayname = capabilities['m.set_displayname']?.enabled === false;
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
const [displayName, setDisplayName] = useState<string>(defaultDisplayName);
const [changeState, changeDisplayName] = useAsyncCallback(
useCallback((name: string) => mx.setDisplayName(name), [mx])
);
const changingDisplayName = changeState.status === AsyncStatus.Loading;
useEffect(() => {
setDisplayName(defaultDisplayName);
}, [defaultDisplayName]);
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
const name = evt.currentTarget.value;
setDisplayName(name);
};
const handleReset = () => {
setDisplayName(defaultDisplayName);
};
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
if (changingDisplayName) return;
const target = evt.target as HTMLFormElement | undefined;
const displayNameInput = target?.displayNameInput as HTMLInputElement | undefined;
const name = displayNameInput?.value;
if (!name) return;
changeDisplayName(name);
};
const hasChanges = displayName !== defaultDisplayName;
return (
<SettingTile
title={
<Text as="span" size="L400">
Display Name
</Text>
}
>
<Box direction="Column" grow="Yes" gap="100">
<Box
as="form"
onSubmit={handleSubmit}
gap="200"
aria-disabled={changingDisplayName || disableSetDisplayname}
>
<Box grow="Yes" direction="Column">
<Input
required
name="displayNameInput"
value={displayName}
onChange={handleChange}
variant="Secondary"
radii="300"
style={{ paddingRight: config.space.S200 }}
readOnly={changingDisplayName || disableSetDisplayname}
after={
hasChanges &&
!changingDisplayName && (
<IconButton
type="reset"
onClick={handleReset}
size="300"
radii="300"
variant="Secondary"
>
<Icon src={Icons.Cross} size="100" />
</IconButton>
)
}
/>
</Box>
<Button
size="400"
variant={hasChanges ? 'Success' : 'Secondary'}
fill={hasChanges ? 'Solid' : 'Soft'}
outlined
radii="300"
disabled={!hasChanges || changingDisplayName}
type="submit"
>
{changingDisplayName && <Spinner variant="Success" fill="Solid" size="300" />}
<Text size="B400">Save</Text>
</Button>
</Box>
</Box>
</SettingTile>
);
}
export function Profile() {
const mx = useMatrixClient();
const userId = mx.getUserId()!;
const profile = useUserProfile(userId);
return (
<Box direction="Column" gap="100">
<Text size="L400">Profile</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<ProfileAvatar userId={userId} profile={profile} />
<ProfileDisplayName userId={userId} profile={profile} />
</SequenceCard>
</Box>
);
}

View file

@ -5,7 +5,9 @@ import { SystemNotification } from './SystemNotification';
import { AllMessagesNotifications } from './AllMessages';
import { SpecialMessagesNotifications } from './SpecialMessages';
import { KeywordMessagesNotifications } from './KeywordMessages';
import { IgnoredUserList } from './IgnoredUserList';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
type NotificationsProps = {
requestClose: () => void;
@ -35,7 +37,19 @@ export function Notifications({ requestClose }: NotificationsProps) {
<AllMessagesNotifications />
<SpecialMessagesNotifications />
<KeywordMessagesNotifications />
<IgnoredUserList />
<Box direction="Column" gap="100">
<Text size="L400">Block Messages</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
description={'This option has been moved to "Account > Block Users" section.'}
/>
</SequenceCard>
</Box>
</Box>
</PageContent>
</Scroll>