mirror of
https://github.com/RGBCube/cinny
synced 2025-07-30 16:37:46 +00:00
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:
parent
0d27bde33e
commit
206ed33516
17 changed files with 1088 additions and 524 deletions
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -21,6 +21,7 @@
|
||||||
"@vanilla-extract/recipes": "0.3.0",
|
"@vanilla-extract/recipes": "0.3.0",
|
||||||
"@vanilla-extract/vite-plugin": "3.7.1",
|
"@vanilla-extract/vite-plugin": "3.7.1",
|
||||||
"await-to-js": "3.0.0",
|
"await-to-js": "3.0.0",
|
||||||
|
"badwords-list": "2.0.1-4",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"browser-encrypt-attachment": "0.3.0",
|
"browser-encrypt-attachment": "0.3.0",
|
||||||
"chroma-js": "3.1.2",
|
"chroma-js": "3.1.2",
|
||||||
|
@ -5436,6 +5437,12 @@
|
||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/badwords-list": {
|
||||||
|
"version": "2.0.1-4",
|
||||||
|
"resolved": "https://registry.npmjs.org/badwords-list/-/badwords-list-2.0.1-4.tgz",
|
||||||
|
"integrity": "sha512-FxfZUp7B9yCnesNtFQS9v6PvZdxTYa14Q60JR6vhjdQdWI4naTjJIyx22JzoER8ooeT8SAAKoHLjKfCV7XgYUQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"@vanilla-extract/recipes": "0.3.0",
|
"@vanilla-extract/recipes": "0.3.0",
|
||||||
"@vanilla-extract/vite-plugin": "3.7.1",
|
"@vanilla-extract/vite-plugin": "3.7.1",
|
||||||
"await-to-js": "3.0.0",
|
"await-to-js": "3.0.0",
|
||||||
|
"badwords-list": "2.0.1-4",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"browser-encrypt-attachment": "0.3.0",
|
"browser-encrypt-attachment": "0.3.0",
|
||||||
"chroma-js": "3.1.2",
|
"chroma-js": "3.1.2",
|
||||||
|
|
|
@ -105,6 +105,20 @@ export const PageContent = as<'div'>(({ className, ...props }, ref) => (
|
||||||
<div className={classNames(css.PageContent, className)} {...props} ref={ref} />
|
<div className={classNames(css.PageContent, className)} {...props} ref={ref} />
|
||||||
));
|
));
|
||||||
|
|
||||||
|
export function PageHeroEmpty({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={classNames(ContainerColor({ variant: 'SurfaceVariant' }), css.PageHeroEmpty)}
|
||||||
|
direction="Column"
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="Center"
|
||||||
|
gap="200"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const PageHeroSection = as<'div', ComponentProps<typeof Box>>(
|
export const PageHeroSection = as<'div', ComponentProps<typeof Box>>(
|
||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<Box
|
<Box
|
||||||
|
|
|
@ -92,6 +92,15 @@ export const PageContent = style([
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const PageHeroEmpty = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
padding: config.space.S400,
|
||||||
|
borderRadius: config.radii.R400,
|
||||||
|
minHeight: toRem(450),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export const PageHeroSection = style([
|
export const PageHeroSection = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { SearchOrderBy } from 'matrix-js-sdk';
|
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 { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { _SearchPathSearchParams } from '../../pages/paths';
|
import { _SearchPathSearchParams } from '../../pages/paths';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
|
@ -222,18 +222,7 @@ export function MessageSearch({
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{!msgSearchParams.term && status === 'pending' && (
|
{!msgSearchParams.term && status === 'pending' && (
|
||||||
<Box
|
<PageHeroEmpty>
|
||||||
className={ContainerColor({ variant: 'SurfaceVariant' })}
|
|
||||||
style={{
|
|
||||||
padding: config.space.S400,
|
|
||||||
borderRadius: config.radii.R400,
|
|
||||||
minHeight: toRem(450),
|
|
||||||
}}
|
|
||||||
direction="Column"
|
|
||||||
alignItems="Center"
|
|
||||||
justifyContent="Center"
|
|
||||||
gap="200"
|
|
||||||
>
|
|
||||||
<PageHeroSection>
|
<PageHeroSection>
|
||||||
<PageHero
|
<PageHero
|
||||||
icon={<Icon size="600" src={Icons.Message} />}
|
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."
|
subTitle="Find helpful messages in your community by searching with related keywords."
|
||||||
/>
|
/>
|
||||||
</PageHeroSection>
|
</PageHeroSection>
|
||||||
</Box>
|
</PageHeroEmpty>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{msgSearchParams.term && groups.length === 0 && status === 'success' && (
|
{msgSearchParams.term && groups.length === 0 && status === 'success' && (
|
||||||
|
|
|
@ -1,396 +1,10 @@
|
||||||
import React, {
|
import React from 'react';
|
||||||
ChangeEventHandler,
|
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
|
||||||
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 { Page, PageContent, PageHeader } from '../../../components/page';
|
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { MatrixId } from './MatrixId';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { Profile } from './Profile';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { ContactInformation } from './ContactInfo';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { IgnoredUserList } from './IgnoredUserList';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountProps = {
|
type AccountProps = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
|
@ -419,6 +33,7 @@ export function Account({ requestClose }: AccountProps) {
|
||||||
<Profile />
|
<Profile />
|
||||||
<MatrixId />
|
<MatrixId />
|
||||||
<ContactInformation />
|
<ContactInformation />
|
||||||
|
<IgnoredUserList />
|
||||||
</Box>
|
</Box>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
|
|
45
src/app/features/settings/account/ContactInfo.tsx
Normal file
45
src/app/features/settings/account/ContactInfo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,16 +7,17 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
import { isUserId } from '../../../utils/matrix';
|
import { isUserId } from '../../../utils/matrix';
|
||||||
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
||||||
|
import { useAlive } from '../../../hooks/useAlive';
|
||||||
|
|
||||||
function IgnoreUserInput({ userList }: { userList: string[] }) {
|
function IgnoreUserInput({ userList }: { userList: string[] }) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const [userId, setUserId] = useState<string>('');
|
const [userId, setUserId] = useState<string>('');
|
||||||
|
const alive = useAlive();
|
||||||
|
|
||||||
const [ignoreState, ignore] = useAsyncCallback(
|
const [ignoreState, ignore] = useAsyncCallback(
|
||||||
useCallback(
|
useCallback(
|
||||||
async (uId: string) => {
|
async (uId: string) => {
|
||||||
mx.setIgnoredUsers([...userList, uId]);
|
await mx.setIgnoredUsers([...userList, uId]);
|
||||||
setUserId('');
|
|
||||||
},
|
},
|
||||||
[mx, userList]
|
[mx, userList]
|
||||||
)
|
)
|
||||||
|
@ -43,7 +44,11 @@ function IgnoreUserInput({ userList }: { userList: string[] }) {
|
||||||
|
|
||||||
if (!isUserId(uId)) return;
|
if (!isUserId(uId)) return;
|
||||||
|
|
||||||
ignore(uId);
|
ignore(uId).then(() => {
|
||||||
|
if (alive()) {
|
||||||
|
setUserId('');
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -129,7 +134,7 @@ export function IgnoredUserList() {
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
||||||
<Text size="L400">Block Messages</Text>
|
<Text size="L400">Blocked Users</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
className={SequenceCardStyle}
|
className={SequenceCardStyle}
|
||||||
|
@ -139,13 +144,13 @@ export function IgnoredUserList() {
|
||||||
>
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Select User"
|
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">
|
<Box direction="Column" gap="300">
|
||||||
<IgnoreUserInput userList={ignoredUsers} />
|
<IgnoreUserInput userList={ignoredUsers} />
|
||||||
{ignoredUsers.length > 0 && (
|
{ignoredUsers.length > 0 && (
|
||||||
<Box direction="Inherit" gap="100">
|
<Box direction="Inherit" gap="100">
|
||||||
<Text size="L400">Blocklist</Text>
|
<Text size="L400">Users</Text>
|
||||||
<Box wrap="Wrap" gap="200">
|
<Box wrap="Wrap" gap="200">
|
||||||
{ignoredUsers.map((userId) => (
|
{ignoredUsers.map((userId) => (
|
||||||
<IgnoredUserChip key={userId} userId={userId} userList={ignoredUsers} />
|
<IgnoredUserChip key={userId} userId={userId} userList={ignoredUsers} />
|
33
src/app/features/settings/account/MatrixId.tsx
Normal file
33
src/app/features/settings/account/MatrixId.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
325
src/app/features/settings/account/Profile.tsx
Normal file
325
src/app/features/settings/account/Profile.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,7 +5,9 @@ import { SystemNotification } from './SystemNotification';
|
||||||
import { AllMessagesNotifications } from './AllMessages';
|
import { AllMessagesNotifications } from './AllMessages';
|
||||||
import { SpecialMessagesNotifications } from './SpecialMessages';
|
import { SpecialMessagesNotifications } from './SpecialMessages';
|
||||||
import { KeywordMessagesNotifications } from './KeywordMessages';
|
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 = {
|
type NotificationsProps = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
|
@ -35,7 +37,19 @@ export function Notifications({ requestClose }: NotificationsProps) {
|
||||||
<AllMessagesNotifications />
|
<AllMessagesNotifications />
|
||||||
<SpecialMessagesNotifications />
|
<SpecialMessagesNotifications />
|
||||||
<KeywordMessagesNotifications />
|
<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>
|
</Box>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
|
|
10
src/app/hooks/useReportRoomSupported.ts
Normal file
10
src/app/hooks/useReportRoomSupported.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { useSpecVersions } from './useSpecVersions';
|
||||||
|
|
||||||
|
export const useReportRoomSupported = (): boolean => {
|
||||||
|
const { versions, unstable_features: unstableFeatures } = useSpecVersions();
|
||||||
|
|
||||||
|
// report room is introduced in spec version 1.13
|
||||||
|
const supported = unstableFeatures?.['org.matrix.msc4151'] || versions.includes('v1.13');
|
||||||
|
|
||||||
|
return supported;
|
||||||
|
};
|
|
@ -32,7 +32,7 @@ function InvitesNavItem() {
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box as="span" grow="Yes">
|
<Box as="span" grow="Yes">
|
||||||
<Text as="span" size="Inherit" truncate>
|
<Text as="span" size="Inherit" truncate>
|
||||||
Invitations
|
Invites
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{inviteCount > 0 && <UnreadBadge highlight count={inviteCount} />}
|
{inviteCount > 0 && <UnreadBadge highlight count={inviteCount} />}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useCallback, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Chip,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
Icons,
|
Icons,
|
||||||
|
@ -16,56 +18,129 @@ import {
|
||||||
config,
|
config,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { RoomTopicEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { MatrixError, Room } from 'matrix-js-sdk';
|
import { MatrixClient, MatrixError, Room } from 'matrix-js-sdk';
|
||||||
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
|
import {
|
||||||
import { useDirectInvites, useRoomInvites, useSpaceInvites } from '../../../state/hooks/inviteList';
|
Page,
|
||||||
|
PageContent,
|
||||||
|
PageContentCenter,
|
||||||
|
PageHeader,
|
||||||
|
PageHero,
|
||||||
|
PageHeroEmpty,
|
||||||
|
PageHeroSection,
|
||||||
|
} from '../../../components/page';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { allInvitesAtom } from '../../../state/room-list/inviteList';
|
import { allInvitesAtom } from '../../../state/room-list/inviteList';
|
||||||
import { mDirectAtom } from '../../../state/mDirectList';
|
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import {
|
import {
|
||||||
|
bannedInRooms,
|
||||||
|
getCommonRooms,
|
||||||
getDirectRoomAvatarUrl,
|
getDirectRoomAvatarUrl,
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
getRoomAvatarUrl,
|
getRoomAvatarUrl,
|
||||||
|
getStateEvent,
|
||||||
isDirectInvite,
|
isDirectInvite,
|
||||||
|
isSpace,
|
||||||
} from '../../../utils/room';
|
} from '../../../utils/room';
|
||||||
import { nameInitials } from '../../../utils/common';
|
import { nameInitials } from '../../../utils/common';
|
||||||
import { RoomAvatar } from '../../../components/room-avatar';
|
import { RoomAvatar } from '../../../components/room-avatar';
|
||||||
import { addRoomIdToMDirect, getMxIdLocalPart, guessDmRoomUserId } from '../../../utils/matrix';
|
import {
|
||||||
|
addRoomIdToMDirect,
|
||||||
|
getMxIdLocalPart,
|
||||||
|
guessDmRoomUserId,
|
||||||
|
rateLimitedActions,
|
||||||
|
} from '../../../utils/matrix';
|
||||||
import { Time } from '../../../components/message';
|
import { Time } from '../../../components/message';
|
||||||
import { useElementSizeObserver } from '../../../hooks/useElementSizeObserver';
|
import { useElementSizeObserver } from '../../../hooks/useElementSizeObserver';
|
||||||
import { onEnterOrSpace, stopPropagation } from '../../../utils/keyboard';
|
import { onEnterOrSpace, stopPropagation } from '../../../utils/keyboard';
|
||||||
import { RoomTopicViewer } from '../../../components/room-topic-viewer';
|
import { RoomTopicViewer } from '../../../components/room-topic-viewer';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||||
import { useRoomTopic } from '../../../hooks/useRoomMeta';
|
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||||
|
import { StateEvent } from '../../../../types/matrix/room';
|
||||||
|
import { testBadWords } from '../../../plugins/bad-words';
|
||||||
|
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||||
|
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
||||||
|
import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported';
|
||||||
|
|
||||||
const COMPACT_CARD_WIDTH = 548;
|
const COMPACT_CARD_WIDTH = 548;
|
||||||
|
|
||||||
type InviteCardProps = {
|
type InviteData = {
|
||||||
room: Room;
|
room: Room;
|
||||||
userId: string;
|
roomId: string;
|
||||||
direct?: boolean;
|
roomName: string;
|
||||||
compact?: boolean;
|
roomAvatar?: string;
|
||||||
onNavigate: (roomId: string) => void;
|
roomTopic?: string;
|
||||||
|
roomAlias?: string;
|
||||||
|
|
||||||
|
senderId: string;
|
||||||
|
senderName: string;
|
||||||
|
inviteTs?: number;
|
||||||
|
|
||||||
|
isSpace: boolean;
|
||||||
|
isDirect: boolean;
|
||||||
|
isEncrypted: boolean;
|
||||||
};
|
};
|
||||||
function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardProps) {
|
|
||||||
const mx = useMatrixClient();
|
const makeInviteData = (mx: MatrixClient, room: Room, useAuthentication: boolean): InviteData => {
|
||||||
const useAuthentication = useMediaAuthentication();
|
const userId = mx.getSafeUserId();
|
||||||
|
const direct = isDirectInvite(room, userId);
|
||||||
|
|
||||||
|
const roomAvatar = direct
|
||||||
|
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||||
|
: getRoomAvatarUrl(mx, room, 96, useAuthentication);
|
||||||
const roomName = room.name || room.getCanonicalAlias() || room.roomId;
|
const roomName = room.name || room.getCanonicalAlias() || room.roomId;
|
||||||
|
const roomTopic =
|
||||||
|
getStateEvent(room, StateEvent.RoomTopic)?.getContent<RoomTopicEventContent>()?.topic ??
|
||||||
|
undefined;
|
||||||
|
|
||||||
const member = room.getMember(userId);
|
const member = room.getMember(userId);
|
||||||
const memberEvent = member?.events.member;
|
const memberEvent = member?.events.member;
|
||||||
const memberTs = memberEvent?.getTs() ?? 0;
|
|
||||||
const senderId = memberEvent?.getSender();
|
const senderId = memberEvent?.getSender();
|
||||||
const senderName = senderId
|
const senderName = senderId
|
||||||
? getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId
|
? getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const inviteTs = memberEvent?.getTs() ?? 0;
|
||||||
|
|
||||||
const topic = useRoomTopic(room);
|
return {
|
||||||
|
room,
|
||||||
|
roomId: room.roomId,
|
||||||
|
roomAvatar,
|
||||||
|
roomName,
|
||||||
|
roomTopic,
|
||||||
|
roomAlias: room.getCanonicalAlias() ?? undefined,
|
||||||
|
|
||||||
|
senderId: senderId ?? 'Unknown',
|
||||||
|
senderName: senderName ?? 'Unknown',
|
||||||
|
inviteTs,
|
||||||
|
|
||||||
|
isSpace: isSpace(room),
|
||||||
|
isDirect: direct,
|
||||||
|
isEncrypted: !!getStateEvent(room, StateEvent.RoomEncryption),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasBadWords = (invite: InviteData): boolean =>
|
||||||
|
testBadWords(invite.roomName) ||
|
||||||
|
testBadWords(invite.roomTopic ?? '') ||
|
||||||
|
testBadWords(invite.senderName) ||
|
||||||
|
testBadWords(invite.senderId);
|
||||||
|
|
||||||
|
type NavigateHandler = (roomId: string, space: boolean) => void;
|
||||||
|
|
||||||
|
type InviteCardProps = {
|
||||||
|
invite: InviteData;
|
||||||
|
compact?: boolean;
|
||||||
|
onNavigate: NavigateHandler;
|
||||||
|
hideAvatar: boolean;
|
||||||
|
};
|
||||||
|
function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const userId = mx.getSafeUserId();
|
||||||
|
|
||||||
const [viewTopic, setViewTopic] = useState(false);
|
const [viewTopic, setViewTopic] = useState(false);
|
||||||
const closeTopic = () => setViewTopic(false);
|
const closeTopic = () => setViewTopic(false);
|
||||||
|
@ -73,17 +148,19 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
||||||
|
|
||||||
const [joinState, join] = useAsyncCallback<void, MatrixError, []>(
|
const [joinState, join] = useAsyncCallback<void, MatrixError, []>(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const dmUserId = isDirectInvite(room, userId) ? guessDmRoomUserId(room, userId) : undefined;
|
const dmUserId = isDirectInvite(invite.room, userId)
|
||||||
|
? guessDmRoomUserId(invite.room, userId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
await mx.joinRoom(room.roomId);
|
await mx.joinRoom(invite.roomId);
|
||||||
if (dmUserId) {
|
if (dmUserId) {
|
||||||
await addRoomIdToMDirect(mx, room.roomId, dmUserId);
|
await addRoomIdToMDirect(mx, invite.roomId, dmUserId);
|
||||||
}
|
}
|
||||||
onNavigate(room.roomId);
|
onNavigate(invite.roomId, invite.isSpace);
|
||||||
}, [mx, room, userId, onNavigate])
|
}, [mx, invite, userId, onNavigate])
|
||||||
);
|
);
|
||||||
const [leaveState, leave] = useAsyncCallback<Record<string, never>, MatrixError, []>(
|
const [leaveState, leave] = useAsyncCallback<Record<string, never>, MatrixError, []>(
|
||||||
useCallback(() => mx.leave(room.roomId), [mx, room])
|
useCallback(() => mx.leave(invite.roomId), [mx, invite])
|
||||||
);
|
);
|
||||||
|
|
||||||
const joining =
|
const joining =
|
||||||
|
@ -95,28 +172,43 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="200"
|
gap="300"
|
||||||
style={{ padding: config.space.S400, paddingTop: config.space.S200 }}
|
style={{ padding: `${config.space.S400} ${config.space.S400} ${config.space.S200}` }}
|
||||||
>
|
>
|
||||||
<Box gap="200" alignItems="Baseline">
|
{(invite.isEncrypted || invite.isDirect || invite.isSpace) && (
|
||||||
<Box grow="Yes">
|
<Box gap="200" alignItems="Center">
|
||||||
<Text size="T200" priority="300" truncate>
|
{invite.isEncrypted && (
|
||||||
Invited by <b>{senderName}</b>
|
<Box shrink="No" alignItems="Center" justifyContent="Center">
|
||||||
</Text>
|
<Badge variant="Success" fill="Solid" size="400" radii="300">
|
||||||
|
<Text size="L400">Encrypted</Text>
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{invite.isDirect && (
|
||||||
|
<Box shrink="No" alignItems="Center" justifyContent="Center">
|
||||||
|
<Badge variant="Primary" fill="Solid" size="400" radii="300">
|
||||||
|
<Text size="L400">Direct Message</Text>
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{invite.isSpace && (
|
||||||
|
<Box shrink="No" alignItems="Center" justifyContent="Center">
|
||||||
|
<Badge variant="Secondary" fill="Soft" size="400" radii="300">
|
||||||
|
<Text size="L400">Space</Text>
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
)}
|
||||||
<Time size="T200" ts={memberTs} priority="300" />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box gap="300">
|
<Box gap="300">
|
||||||
<Avatar size="300">
|
<Avatar size="300">
|
||||||
<RoomAvatar
|
<RoomAvatar
|
||||||
roomId={room.roomId}
|
roomId={invite.roomId}
|
||||||
src={direct ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)}
|
src={hideAvatar ? undefined : invite.roomAvatar}
|
||||||
alt={roomName}
|
alt={invite.roomName}
|
||||||
renderFallback={() => (
|
renderFallback={() => (
|
||||||
<Text as="span" size="H6">
|
<Text as="span" size="H6">
|
||||||
{nameInitials(roomName)}
|
{nameInitials(hideAvatar && invite.roomAvatar ? undefined : invite.roomName)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -125,9 +217,9 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
||||||
<Box grow="Yes" direction="Column" gap="200">
|
<Box grow="Yes" direction="Column" gap="200">
|
||||||
<Box direction="Column">
|
<Box direction="Column">
|
||||||
<Text size="T300" truncate>
|
<Text size="T300" truncate>
|
||||||
<b>{roomName}</b>
|
<b>{invite.roomName}</b>
|
||||||
</Text>
|
</Text>
|
||||||
{topic && (
|
{invite.roomTopic && (
|
||||||
<Text
|
<Text
|
||||||
size="T200"
|
size="T200"
|
||||||
onClick={openTopic}
|
onClick={openTopic}
|
||||||
|
@ -135,7 +227,7 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
truncate
|
truncate
|
||||||
>
|
>
|
||||||
{topic}
|
{invite.roomTopic}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Overlay open={viewTopic} backdrop={<OverlayBackdrop />}>
|
<Overlay open={viewTopic} backdrop={<OverlayBackdrop />}>
|
||||||
|
@ -149,8 +241,8 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RoomTopicViewer
|
<RoomTopicViewer
|
||||||
name={roomName}
|
name={invite.roomName}
|
||||||
topic={topic ?? ''}
|
topic={invite.roomTopic ?? ''}
|
||||||
requestClose={closeTopic}
|
requestClose={closeTopic}
|
||||||
/>
|
/>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
|
@ -173,6 +265,7 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
||||||
onClick={leave}
|
onClick={leave}
|
||||||
size="300"
|
size="300"
|
||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
|
radii="300"
|
||||||
fill="Soft"
|
fill="Soft"
|
||||||
disabled={joining || leaving}
|
disabled={joining || leaving}
|
||||||
before={leaving ? <Spinner variant="Secondary" size="100" /> : undefined}
|
before={leaving ? <Spinner variant="Secondary" size="100" /> : undefined}
|
||||||
|
@ -182,28 +275,392 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
||||||
<Button
|
<Button
|
||||||
onClick={join}
|
onClick={join}
|
||||||
size="300"
|
size="300"
|
||||||
variant="Primary"
|
variant="Success"
|
||||||
fill="Soft"
|
fill="Soft"
|
||||||
|
radii="300"
|
||||||
outlined
|
outlined
|
||||||
disabled={joining || leaving}
|
disabled={joining || leaving}
|
||||||
before={joining ? <Spinner variant="Primary" fill="Soft" size="100" /> : undefined}
|
before={joining ? <Spinner variant="Success" fill="Soft" size="100" /> : undefined}
|
||||||
>
|
>
|
||||||
<Text size="B300">Accept</Text>
|
<Text size="B300">Accept</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box gap="200" alignItems="Baseline">
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Text size="T200" priority="300">
|
||||||
|
From: <b>{invite.senderId}</b>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
{invite.inviteTs && (
|
||||||
|
<Box shrink="No">
|
||||||
|
<Time size="T200" ts={invite.inviteTs} priority="300" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum InviteFilter {
|
||||||
|
Known,
|
||||||
|
Unknown,
|
||||||
|
Spam,
|
||||||
|
}
|
||||||
|
type InviteFiltersProps = {
|
||||||
|
filter: InviteFilter;
|
||||||
|
onFilter: (filter: InviteFilter) => void;
|
||||||
|
knownInvites: InviteData[];
|
||||||
|
unknownInvites: InviteData[];
|
||||||
|
spamInvites: InviteData[];
|
||||||
|
};
|
||||||
|
function InviteFilters({
|
||||||
|
filter,
|
||||||
|
onFilter,
|
||||||
|
knownInvites,
|
||||||
|
unknownInvites,
|
||||||
|
spamInvites,
|
||||||
|
}: InviteFiltersProps) {
|
||||||
|
const isKnown = filter === InviteFilter.Known;
|
||||||
|
const isUnknown = filter === InviteFilter.Unknown;
|
||||||
|
const isSpam = filter === InviteFilter.Spam;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box gap="200">
|
||||||
|
<Chip
|
||||||
|
variant={isKnown ? 'Success' : 'Surface'}
|
||||||
|
aria-selected={isKnown}
|
||||||
|
outlined={!isKnown}
|
||||||
|
onClick={() => onFilter(InviteFilter.Known)}
|
||||||
|
before={isKnown && <Icon size="100" src={Icons.Check} />}
|
||||||
|
after={
|
||||||
|
knownInvites.length > 0 && (
|
||||||
|
<Badge variant={isKnown ? 'Success' : 'Secondary'} fill="Solid" radii="Pill">
|
||||||
|
<Text size="L400">{knownInvites.length}</Text>
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="T200">Primary</Text>
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
variant={isUnknown ? 'Warning' : 'Surface'}
|
||||||
|
aria-selected={isUnknown}
|
||||||
|
outlined={!isUnknown}
|
||||||
|
onClick={() => onFilter(InviteFilter.Unknown)}
|
||||||
|
before={isUnknown && <Icon size="100" src={Icons.Check} />}
|
||||||
|
after={
|
||||||
|
unknownInvites.length > 0 && (
|
||||||
|
<Badge variant={isUnknown ? 'Warning' : 'Secondary'} fill="Solid" radii="Pill">
|
||||||
|
<Text size="L400">{unknownInvites.length}</Text>
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="T200">Public</Text>
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
variant={isSpam ? 'Critical' : 'Surface'}
|
||||||
|
aria-selected={isSpam}
|
||||||
|
outlined={!isSpam}
|
||||||
|
onClick={() => onFilter(InviteFilter.Spam)}
|
||||||
|
before={isSpam && <Icon size="100" src={Icons.Check} />}
|
||||||
|
after={
|
||||||
|
spamInvites.length > 0 && (
|
||||||
|
<Badge variant={isSpam ? 'Critical' : 'Secondary'} fill="Solid" radii="Pill">
|
||||||
|
<Text size="L400">{spamInvites.length}</Text>
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="T200">Spam</Text>
|
||||||
|
</Chip>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type KnownInvitesProps = {
|
||||||
|
invites: InviteData[];
|
||||||
|
handleNavigate: NavigateHandler;
|
||||||
|
compact: boolean;
|
||||||
|
};
|
||||||
|
function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) {
|
||||||
|
return (
|
||||||
|
<Box direction="Column" gap="200">
|
||||||
|
<Text size="H4">Primary</Text>
|
||||||
|
{invites.length > 0 ? (
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
{invites.map((invite) => (
|
||||||
|
<InviteCard
|
||||||
|
key={invite.roomId}
|
||||||
|
invite={invite}
|
||||||
|
compact={compact}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
hideAvatar={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<PageHeroEmpty>
|
||||||
|
<PageHeroSection>
|
||||||
|
<PageHero
|
||||||
|
icon={<Icon size="600" src={Icons.Mail} />}
|
||||||
|
title="No Invites"
|
||||||
|
subTitle="When someone you share a room with sends you an invite, it’ll show up here."
|
||||||
|
/>
|
||||||
|
</PageHeroSection>
|
||||||
|
</PageHeroEmpty>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnknownInvitesProps = {
|
||||||
|
invites: InviteData[];
|
||||||
|
handleNavigate: NavigateHandler;
|
||||||
|
compact: boolean;
|
||||||
|
};
|
||||||
|
function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
|
const [declineAllStatus, declineAll] = useAsyncCallback(
|
||||||
|
useCallback(async () => {
|
||||||
|
const roomIds = invites.map((invite) => invite.roomId);
|
||||||
|
|
||||||
|
await rateLimitedActions(roomIds, (roomId) => mx.leave(roomId));
|
||||||
|
}, [mx, invites])
|
||||||
|
);
|
||||||
|
|
||||||
|
const declining = declineAllStatus.status === AsyncStatus.Loading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box direction="Column" gap="200">
|
||||||
|
<Box gap="200" justifyContent="SpaceBetween" alignItems="Center">
|
||||||
|
<Text size="H4">Public</Text>
|
||||||
|
<Box>
|
||||||
|
<Chip
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
onClick={declineAll}
|
||||||
|
before={declining && <Spinner size="50" variant="Secondary" fill="Soft" />}
|
||||||
|
disabled={declining}
|
||||||
|
radii="Pill"
|
||||||
|
>
|
||||||
|
<Text size="T200">Decline All</Text>
|
||||||
|
</Chip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{invites.length > 0 ? (
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
{invites.map((invite) => (
|
||||||
|
<InviteCard
|
||||||
|
key={invite.roomId}
|
||||||
|
invite={invite}
|
||||||
|
compact={compact}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
hideAvatar
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<PageHeroEmpty>
|
||||||
|
<PageHeroSection>
|
||||||
|
<PageHero
|
||||||
|
icon={<Icon size="600" src={Icons.Info} />}
|
||||||
|
title="No Invites"
|
||||||
|
subTitle="Invites from people outside your rooms will appear here."
|
||||||
|
/>
|
||||||
|
</PageHeroSection>
|
||||||
|
</PageHeroEmpty>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpamInvitesProps = {
|
||||||
|
invites: InviteData[];
|
||||||
|
handleNavigate: NavigateHandler;
|
||||||
|
compact: boolean;
|
||||||
|
};
|
||||||
|
function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const [showInvites, setShowInvites] = useState(false);
|
||||||
|
|
||||||
|
const reportRoomSupported = useReportRoomSupported();
|
||||||
|
|
||||||
|
const [declineAllStatus, declineAll] = useAsyncCallback(
|
||||||
|
useCallback(async () => {
|
||||||
|
const roomIds = invites.map((invite) => invite.roomId);
|
||||||
|
|
||||||
|
await rateLimitedActions(roomIds, (roomId) => mx.leave(roomId));
|
||||||
|
}, [mx, invites])
|
||||||
|
);
|
||||||
|
|
||||||
|
const [reportAllStatus, reportAll] = useAsyncCallback(
|
||||||
|
useCallback(async () => {
|
||||||
|
const roomIds = invites.map((invite) => invite.roomId);
|
||||||
|
|
||||||
|
await rateLimitedActions(roomIds, (roomId) => mx.reportRoom(roomId, 'Spam Invite'));
|
||||||
|
}, [mx, invites])
|
||||||
|
);
|
||||||
|
|
||||||
|
const ignoredUsers = useIgnoredUsers();
|
||||||
|
const unignoredUsers = Array.from(new Set(invites.map((invite) => invite.senderId))).filter(
|
||||||
|
(user) => !ignoredUsers.includes(user)
|
||||||
|
);
|
||||||
|
const [blockAllStatus, blockAll] = useAsyncCallback(
|
||||||
|
useCallback(
|
||||||
|
() => mx.setIgnoredUsers([...ignoredUsers, ...unignoredUsers]),
|
||||||
|
[mx, ignoredUsers, unignoredUsers]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const declining = declineAllStatus.status === AsyncStatus.Loading;
|
||||||
|
const reporting = reportAllStatus.status === AsyncStatus.Loading;
|
||||||
|
const blocking = blockAllStatus.status === AsyncStatus.Loading;
|
||||||
|
const loading = blocking || reporting || declining;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box direction="Column" gap="200">
|
||||||
|
<Text size="H4">Spam</Text>
|
||||||
|
{invites.length > 0 ? (
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
<SequenceCard
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
direction="Column"
|
||||||
|
gap="300"
|
||||||
|
style={{ padding: `${config.space.S400} ${config.space.S400} 0` }}
|
||||||
|
>
|
||||||
|
<PageHeroSection>
|
||||||
|
<PageHero
|
||||||
|
icon={<Icon size="600" src={Icons.Warning} />}
|
||||||
|
title={`${invites.length} Spam Invites`}
|
||||||
|
subTitle="Some of the following invites may contain harmful content or have been sent by banned users."
|
||||||
|
>
|
||||||
|
<Box direction="Row" gap="200" justifyContent="Center" wrap="Wrap">
|
||||||
|
<Button
|
||||||
|
size="300"
|
||||||
|
variant="Critical"
|
||||||
|
fill="Solid"
|
||||||
|
radii="300"
|
||||||
|
onClick={declineAll}
|
||||||
|
before={declining && <Spinner size="100" variant="Critical" fill="Solid" />}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
Decline All
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
{reportRoomSupported && reportAllStatus.status !== AsyncStatus.Success && (
|
||||||
|
<Button
|
||||||
|
size="300"
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Solid"
|
||||||
|
radii="300"
|
||||||
|
onClick={reportAll}
|
||||||
|
before={reporting && <Spinner size="100" variant="Secondary" fill="Solid" />}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
Report All
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{unignoredUsers.length > 0 && (
|
||||||
|
<Button
|
||||||
|
size="300"
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Solid"
|
||||||
|
radii="300"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={blockAll}
|
||||||
|
before={blocking && <Spinner size="100" variant="Secondary" fill="Solid" />}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
Block All
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<span data-spacing-node />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="300"
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Soft"
|
||||||
|
radii="Pill"
|
||||||
|
before={
|
||||||
|
<Icon size="100" src={showInvites ? Icons.ChevronTop : Icons.ChevronBottom} />
|
||||||
|
}
|
||||||
|
onClick={() => setShowInvites(!showInvites)}
|
||||||
|
>
|
||||||
|
<Text size="B300">{showInvites ? 'Hide All' : 'View All'}</Text>
|
||||||
|
</Button>
|
||||||
|
</PageHero>
|
||||||
|
</PageHeroSection>
|
||||||
|
</SequenceCard>
|
||||||
|
{showInvites &&
|
||||||
|
invites.map((invite) => (
|
||||||
|
<InviteCard
|
||||||
|
key={invite.roomId}
|
||||||
|
invite={invite}
|
||||||
|
compact={compact}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
hideAvatar
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<PageHeroEmpty>
|
||||||
|
<PageHeroSection>
|
||||||
|
<PageHero
|
||||||
|
icon={<Icon size="600" src={Icons.Warning} />}
|
||||||
|
title="No Spam Invites"
|
||||||
|
subTitle="Invites detected as spam appear here."
|
||||||
|
/>
|
||||||
|
</PageHeroSection>
|
||||||
|
</PageHeroEmpty>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Invites() {
|
export function Invites() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const userId = mx.getUserId()!;
|
const useAuthentication = useMediaAuthentication();
|
||||||
const mDirects = useAtomValue(mDirectAtom);
|
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||||
const directInvites = useDirectInvites(mx, allInvitesAtom, mDirects);
|
const allRooms = useAtomValue(allRoomsAtom);
|
||||||
const spaceInvites = useSpaceInvites(mx, allInvitesAtom);
|
const allInviteIds = useAtomValue(allInvitesAtom);
|
||||||
const roomInvites = useRoomInvites(mx, allInvitesAtom, mDirects);
|
|
||||||
|
const [filter, setFilter] = useState(InviteFilter.Known);
|
||||||
|
|
||||||
|
const invitesData = allInviteIds
|
||||||
|
.map((inviteId) => mx.getRoom(inviteId))
|
||||||
|
.filter((inviteRoom) => !!inviteRoom)
|
||||||
|
.map((inviteRoom) => makeInviteData(mx, inviteRoom, useAuthentication));
|
||||||
|
|
||||||
|
const [knownInvites, unknownInvites, spamInvites] = useMemo(() => {
|
||||||
|
const known: InviteData[] = [];
|
||||||
|
const unknown: InviteData[] = [];
|
||||||
|
const spam: InviteData[] = [];
|
||||||
|
invitesData.forEach((invite) => {
|
||||||
|
if (hasBadWords(invite) || bannedInRooms(mx, allRooms, invite.senderId)) {
|
||||||
|
spam.push(invite);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getCommonRooms(mx, allRooms, invite.senderId).length === 0) {
|
||||||
|
unknown.push(invite);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
known.push(invite);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [known, unknown, spam];
|
||||||
|
}, [mx, allRooms, invitesData]);
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [compact, setCompact] = useState(document.body.clientWidth <= COMPACT_CARD_WIDTH);
|
const [compact, setCompact] = useState(document.body.clientWidth <= COMPACT_CARD_WIDTH);
|
||||||
useElementSizeObserver(
|
useElementSizeObserver(
|
||||||
|
@ -212,21 +669,12 @@ export function Invites() {
|
||||||
);
|
);
|
||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
|
|
||||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
const handleNavigate = (roomId: string, space: boolean) => {
|
||||||
|
if (space) {
|
||||||
const renderInvite = (roomId: string, direct: boolean, handleNavigate: (rId: string) => void) => {
|
navigateSpace(roomId);
|
||||||
const room = mx.getRoom(roomId);
|
return;
|
||||||
if (!room) return null;
|
}
|
||||||
return (
|
navigateRoom(roomId);
|
||||||
<InviteCard
|
|
||||||
key={roomId}
|
|
||||||
room={room}
|
|
||||||
userId={userId}
|
|
||||||
compact={compact}
|
|
||||||
direct={direct}
|
|
||||||
onNavigate={handleNavigate}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -247,7 +695,7 @@ export function Invites() {
|
||||||
<Box alignItems="Center" gap="200">
|
<Box alignItems="Center" gap="200">
|
||||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Mail} />}
|
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Mail} />}
|
||||||
<Text size="H3" truncate>
|
<Text size="H3" truncate>
|
||||||
Invitations
|
Invites
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box grow="Yes" basis="No" />
|
<Box grow="Yes" basis="No" />
|
||||||
|
@ -258,47 +706,40 @@ export function Invites() {
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<PageContentCenter>
|
<PageContentCenter>
|
||||||
<Box ref={containerRef} direction="Column" gap="600">
|
<Box ref={containerRef} direction="Column" gap="600">
|
||||||
{directInvites.length > 0 && (
|
<Box direction="Column" gap="100">
|
||||||
<Box direction="Column" gap="200">
|
<span data-spacing-node />
|
||||||
<Text size="H4">Direct Messages</Text>
|
<Text size="L400">Filter</Text>
|
||||||
<Box direction="Column" gap="100">
|
<InviteFilters
|
||||||
{directInvites.map((roomId) => renderInvite(roomId, true, navigateRoom))}
|
filter={filter}
|
||||||
</Box>
|
onFilter={setFilter}
|
||||||
</Box>
|
knownInvites={knownInvites}
|
||||||
|
unknownInvites={unknownInvites}
|
||||||
|
spamInvites={spamInvites}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{filter === InviteFilter.Known && (
|
||||||
|
<KnownInvites
|
||||||
|
invites={knownInvites}
|
||||||
|
compact={compact}
|
||||||
|
handleNavigate={handleNavigate}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{spaceInvites.length > 0 && (
|
|
||||||
<Box direction="Column" gap="200">
|
{filter === InviteFilter.Unknown && (
|
||||||
<Text size="H4">Spaces</Text>
|
<UnknownInvites
|
||||||
<Box direction="Column" gap="100">
|
invites={unknownInvites}
|
||||||
{spaceInvites.map((roomId) => renderInvite(roomId, false, navigateSpace))}
|
compact={compact}
|
||||||
</Box>
|
handleNavigate={handleNavigate}
|
||||||
</Box>
|
/>
|
||||||
)}
|
)}
|
||||||
{roomInvites.length > 0 && (
|
|
||||||
<Box direction="Column" gap="200">
|
{filter === InviteFilter.Spam && (
|
||||||
<Text size="H4">Rooms</Text>
|
<SpamInvites
|
||||||
<Box direction="Column" gap="100">
|
invites={spamInvites}
|
||||||
{roomInvites.map((roomId) => renderInvite(roomId, false, navigateRoom))}
|
compact={compact}
|
||||||
</Box>
|
handleNavigate={handleNavigate}
|
||||||
</Box>
|
/>
|
||||||
)}
|
)}
|
||||||
{directInvites.length === 0 &&
|
|
||||||
spaceInvites.length === 0 &&
|
|
||||||
roomInvites.length === 0 && (
|
|
||||||
<div>
|
|
||||||
<SequenceCard
|
|
||||||
variant="SurfaceVariant"
|
|
||||||
style={{ padding: config.space.S400 }}
|
|
||||||
direction="Column"
|
|
||||||
gap="200"
|
|
||||||
>
|
|
||||||
<Text>No Pending Invitations</Text>
|
|
||||||
<Text size="T200">
|
|
||||||
You don't have any new pending invitations to display yet.
|
|
||||||
</Text>
|
|
||||||
</SequenceCard>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</PageContentCenter>
|
</PageContentCenter>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
15
src/app/plugins/bad-words.ts
Normal file
15
src/app/plugins/bad-words.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import * as badWords from 'badwords-list';
|
||||||
|
import { sanitizeForRegex } from '../utils/regex';
|
||||||
|
|
||||||
|
const additionalBadWords: string[] = ['Torture', 'T0rture'];
|
||||||
|
|
||||||
|
const fullBadWordList = additionalBadWords.concat(
|
||||||
|
badWords.array.filter((word) => !additionalBadWords.includes(word))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BAD_WORDS_REGEX = new RegExp(
|
||||||
|
`(\\b|_)(${fullBadWordList.map((word) => sanitizeForRegex(word)).join('|')})(\\b|_)`,
|
||||||
|
'g'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const testBadWords = (str: string): boolean => !!str.toLowerCase().match(BAD_WORDS_REGEX);
|
|
@ -304,6 +304,14 @@ export const rateLimitedActions = async <T, R = void>(
|
||||||
maxRetryCount?: number
|
maxRetryCount?: number
|
||||||
) => {
|
) => {
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
|
|
||||||
|
let actionInterval = 0;
|
||||||
|
|
||||||
|
const sleepForMs = (ms: number) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
|
||||||
const performAction = async (dataItem: T) => {
|
const performAction = async (dataItem: T) => {
|
||||||
const [err] = await to<R, MatrixError>(callback(dataItem));
|
const [err] = await to<R, MatrixError>(callback(dataItem));
|
||||||
|
|
||||||
|
@ -312,10 +320,9 @@ export const rateLimitedActions = async <T, R = void>(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const waitMS = err.getRetryAfterMs() ?? 200;
|
const waitMS = err.getRetryAfterMs() ?? 3000;
|
||||||
await new Promise((resolve) => {
|
actionInterval = waitMS + 500;
|
||||||
setTimeout(resolve, waitMS);
|
await sleepForMs(waitMS);
|
||||||
});
|
|
||||||
retryCount += 1;
|
retryCount += 1;
|
||||||
|
|
||||||
await performAction(dataItem);
|
await performAction(dataItem);
|
||||||
|
@ -327,5 +334,9 @@ export const rateLimitedActions = async <T, R = void>(
|
||||||
retryCount = 0;
|
retryCount = 0;
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await performAction(dataItem);
|
await performAction(dataItem);
|
||||||
|
if (actionInterval > 0) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await sleepForMs(actionInterval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
||||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||||
import {
|
import {
|
||||||
|
Membership,
|
||||||
MessageEvent,
|
MessageEvent,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
RoomToParents,
|
RoomToParents,
|
||||||
|
@ -171,7 +172,7 @@ export const getNotificationType = (mx: MatrixClient, roomId: string): Notificat
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!roomPushRule) {
|
if (!roomPushRule) {
|
||||||
const overrideRules = mx.getAccountData('m.push_rules')?.getContent<IPushRules>()
|
const overrideRules = mx.getAccountData(EventType.PushRules)?.getContent<IPushRules>()
|
||||||
?.global?.override;
|
?.global?.override;
|
||||||
if (!overrideRules) return NotificationType.Default;
|
if (!overrideRules) return NotificationType.Default;
|
||||||
|
|
||||||
|
@ -443,3 +444,32 @@ export const getMentionContent = (userIds: string[], room: boolean): IMentions =
|
||||||
|
|
||||||
return mMentions;
|
return mMentions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCommonRooms = (
|
||||||
|
mx: MatrixClient,
|
||||||
|
rooms: string[],
|
||||||
|
otherUserId: string
|
||||||
|
): string[] => {
|
||||||
|
const commonRooms: string[] = [];
|
||||||
|
|
||||||
|
rooms.forEach((roomId) => {
|
||||||
|
const room = mx.getRoom(roomId);
|
||||||
|
if (!room || room.getMyMembership() !== Membership.Join) return;
|
||||||
|
|
||||||
|
const common = room.hasMembershipState(otherUserId, Membership.Join);
|
||||||
|
if (common) {
|
||||||
|
commonRooms.push(roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return commonRooms;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bannedInRooms = (mx: MatrixClient, rooms: string[], otherUserId: string): boolean =>
|
||||||
|
rooms.some((roomId) => {
|
||||||
|
const room = mx.getRoom(roomId);
|
||||||
|
if (!room || room.getMyMembership() !== Membership.Join) return false;
|
||||||
|
|
||||||
|
const banned = room.hasMembershipState(otherUserId, Membership.Ban);
|
||||||
|
return banned;
|
||||||
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue