mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-09-13 14:22:25 +03:00

Some checks are pending
Deploy to Netlify (dev) / Deploy to Netlify (push) Waiting to run
* WIP - support room version 12 * add room creators hook * revert changes from powerlevels * improve use room creators hook * add hook to get dm users * add options to add creators in create room/space * add member item component in member drawer * remove unused import * extract member drawer header component * get room creators as set only if room version support them * add room permissions hook * support room v12 creators power * make predecessor event id optional * add info about founders in permissions * allow to create infinite powers to room creators * allow everyone with permission to create infinite power * handle additional creators in room upgrade * add option to follow space tombstone
436 lines
14 KiB
TypeScript
436 lines
14 KiB
TypeScript
import React, { FormEventHandler, useCallback, useState } from 'react';
|
|
import {
|
|
Badge,
|
|
Box,
|
|
Button,
|
|
Checkbox,
|
|
Chip,
|
|
color,
|
|
config,
|
|
Icon,
|
|
Icons,
|
|
Input,
|
|
Spinner,
|
|
Text,
|
|
toRem,
|
|
} from 'folds';
|
|
import { MatrixError } from 'matrix-js-sdk';
|
|
import { SettingTile } from '../../../components/setting-tile';
|
|
import { SequenceCard } from '../../../components/sequence-card';
|
|
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
|
import { useRoom } from '../../../hooks/useRoom';
|
|
import {
|
|
useLocalAliases,
|
|
usePublishedAliases,
|
|
usePublishUnpublishAliases,
|
|
useSetMainAlias,
|
|
} from '../../../hooks/useRoomAliases';
|
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
|
import { CutoutCard } from '../../../components/cutout-card';
|
|
import { getIdServer } from '../../../../util/matrixUtil';
|
|
import { replaceSpaceWithDash } from '../../../utils/common';
|
|
import { useAlive } from '../../../hooks/useAlive';
|
|
import { StateEvent } from '../../../../types/matrix/room';
|
|
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
|
|
|
|
type RoomPublishedAddressesProps = {
|
|
permissions: RoomPermissionsAPI;
|
|
};
|
|
|
|
export function RoomPublishedAddresses({ permissions }: RoomPublishedAddressesProps) {
|
|
const mx = useMatrixClient();
|
|
const room = useRoom();
|
|
|
|
const canEditCanonical = permissions.stateEvent(
|
|
StateEvent.RoomCanonicalAlias,
|
|
mx.getSafeUserId()
|
|
);
|
|
|
|
const [canonicalAlias, publishedAliases] = usePublishedAliases(room);
|
|
const setMainAlias = useSetMainAlias(room);
|
|
|
|
const [mainState, setMain] = useAsyncCallback(setMainAlias);
|
|
const loading = mainState.status === AsyncStatus.Loading;
|
|
|
|
return (
|
|
<SequenceCard
|
|
className={SequenceCardStyle}
|
|
variant="SurfaceVariant"
|
|
direction="Column"
|
|
gap="400"
|
|
>
|
|
<SettingTile
|
|
title="Published Addresses"
|
|
description={
|
|
<span>
|
|
If access is <b>Public</b>, Published addresses will be used to join by anyone.
|
|
</span>
|
|
}
|
|
/>
|
|
<CutoutCard variant="Surface" style={{ padding: config.space.S300 }}>
|
|
{publishedAliases.length === 0 ? (
|
|
<Box direction="Column" gap="100">
|
|
<Text size="L400">No Addresses</Text>
|
|
<Text size="T200">
|
|
To publish an address, it needs to be set as a local address first
|
|
</Text>
|
|
</Box>
|
|
) : (
|
|
<Box direction="Column" gap="300">
|
|
{publishedAliases.map((alias) => (
|
|
<Box key={alias} as="span" gap="200" alignItems="Center">
|
|
<Box grow="Yes" gap="Inherit" alignItems="Center">
|
|
<Text size="T300" truncate>
|
|
{alias === canonicalAlias ? <b>{alias}</b> : alias}
|
|
</Text>
|
|
{alias === canonicalAlias && (
|
|
<Badge variant="Success" fill="Solid" size="500">
|
|
<Text size="L400">Main</Text>
|
|
</Badge>
|
|
)}
|
|
</Box>
|
|
{canEditCanonical && (
|
|
<Box shrink="No" gap="100">
|
|
{alias === canonicalAlias ? (
|
|
<Chip
|
|
variant="Warning"
|
|
radii="Pill"
|
|
fill="None"
|
|
disabled={loading}
|
|
onClick={() => setMain(undefined)}
|
|
>
|
|
<Text size="B300">Unset Main</Text>
|
|
</Chip>
|
|
) : (
|
|
<Chip
|
|
variant="Success"
|
|
radii="Pill"
|
|
fill={canonicalAlias ? 'None' : 'Soft'}
|
|
disabled={loading}
|
|
onClick={() => setMain(alias)}
|
|
>
|
|
<Text size="B300">Set Main</Text>
|
|
</Chip>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
))}
|
|
|
|
{mainState.status === AsyncStatus.Error && (
|
|
<Text size="T200" style={{ color: color.Critical.Main }}>
|
|
{(mainState.error as MatrixError).message}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</CutoutCard>
|
|
</SequenceCard>
|
|
);
|
|
}
|
|
|
|
function LocalAddressInput({ addLocalAlias }: { addLocalAlias: (alias: string) => Promise<void> }) {
|
|
const mx = useMatrixClient();
|
|
const userId = mx.getSafeUserId();
|
|
const server = getIdServer(userId);
|
|
const alive = useAlive();
|
|
|
|
const [addState, addAlias] = useAsyncCallback(addLocalAlias);
|
|
const adding = addState.status === AsyncStatus.Loading;
|
|
|
|
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
|
if (adding) return;
|
|
evt.preventDefault();
|
|
|
|
const target = evt.target as HTMLFormElement | undefined;
|
|
const aliasInput = target?.aliasInput as HTMLInputElement | undefined;
|
|
if (!aliasInput) return;
|
|
const alias = replaceSpaceWithDash(aliasInput.value.trim());
|
|
if (!alias) return;
|
|
|
|
addAlias(`#${alias}:${server}`).then(() => {
|
|
if (alive()) {
|
|
aliasInput.value = '';
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="200">
|
|
<Box gap="200">
|
|
<Box grow="Yes" direction="Column">
|
|
<Input
|
|
name="aliasInput"
|
|
variant="Secondary"
|
|
size="400"
|
|
radii="300"
|
|
before={<Text size="T200">#</Text>}
|
|
readOnly={adding}
|
|
after={
|
|
<Text style={{ maxWidth: toRem(300) }} size="T200" truncate>
|
|
:{server}
|
|
</Text>
|
|
}
|
|
/>
|
|
</Box>
|
|
<Box shrink="No">
|
|
<Button
|
|
variant="Success"
|
|
size="400"
|
|
radii="300"
|
|
type="submit"
|
|
disabled={adding}
|
|
before={adding && <Spinner size="100" variant="Success" fill="Solid" />}
|
|
>
|
|
<Text size="B400">Save</Text>
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
{addState.status === AsyncStatus.Error && (
|
|
<Text style={{ color: color.Critical.Main }} size="T200">
|
|
{(addState.error as MatrixError).httpStatus === 409
|
|
? 'Address is already in use!'
|
|
: (addState.error as MatrixError).message}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function LocalAddressesList({
|
|
localAliases,
|
|
removeLocalAlias,
|
|
canEditCanonical,
|
|
}: {
|
|
localAliases: string[];
|
|
removeLocalAlias: (alias: string) => Promise<void>;
|
|
canEditCanonical?: boolean;
|
|
}) {
|
|
const room = useRoom();
|
|
const alive = useAlive();
|
|
|
|
const [, publishedAliases] = usePublishedAliases(room);
|
|
const { publishAliases, unpublishAliases } = usePublishUnpublishAliases(room);
|
|
|
|
const [selectedAliases, setSelectedAliases] = useState<string[]>([]);
|
|
const selectHasPublished = selectedAliases.find((alias) => publishedAliases.includes(alias));
|
|
|
|
const toggleSelect = (alias: string) => {
|
|
setSelectedAliases((aliases) => {
|
|
if (aliases.includes(alias)) {
|
|
return aliases.filter((a) => a !== alias);
|
|
}
|
|
const newAliases = [...aliases];
|
|
newAliases.push(alias);
|
|
return newAliases;
|
|
});
|
|
};
|
|
const clearSelected = () => {
|
|
if (alive()) {
|
|
setSelectedAliases([]);
|
|
}
|
|
};
|
|
|
|
const [deleteState, deleteAliases] = useAsyncCallback(
|
|
useCallback(
|
|
async (aliases: string[]) => {
|
|
for (let i = 0; i < aliases.length; i += 1) {
|
|
const alias = aliases[i];
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await removeLocalAlias(alias);
|
|
}
|
|
},
|
|
[removeLocalAlias]
|
|
)
|
|
);
|
|
const [publishState, publish] = useAsyncCallback(publishAliases);
|
|
const [unpublishState, unpublish] = useAsyncCallback(unpublishAliases);
|
|
|
|
const handleDelete = () => {
|
|
deleteAliases(selectedAliases).then(clearSelected);
|
|
};
|
|
const handlePublish = () => {
|
|
publish(selectedAliases).then(clearSelected);
|
|
};
|
|
const handleUnpublish = () => {
|
|
unpublish(selectedAliases).then(clearSelected);
|
|
};
|
|
|
|
const loading =
|
|
deleteState.status === AsyncStatus.Loading ||
|
|
publishState.status === AsyncStatus.Loading ||
|
|
unpublishState.status === AsyncStatus.Loading;
|
|
let error: MatrixError | undefined;
|
|
if (deleteState.status === AsyncStatus.Error) error = deleteState.error as MatrixError;
|
|
if (publishState.status === AsyncStatus.Error) error = publishState.error as MatrixError;
|
|
if (unpublishState.status === AsyncStatus.Error) error = unpublishState.error as MatrixError;
|
|
|
|
return (
|
|
<Box direction="Column" gap="300">
|
|
{selectedAliases.length > 0 && (
|
|
<Box gap="200">
|
|
<Box grow="Yes">
|
|
<Text size="L400">{selectedAliases.length} Selected</Text>
|
|
</Box>
|
|
<Box shrink="No" gap="Inherit">
|
|
{canEditCanonical &&
|
|
(selectHasPublished ? (
|
|
<Chip
|
|
variant="Warning"
|
|
radii="Pill"
|
|
disabled={loading}
|
|
onClick={handleUnpublish}
|
|
before={
|
|
unpublishState.status === AsyncStatus.Loading && (
|
|
<Spinner size="100" variant="Warning" />
|
|
)
|
|
}
|
|
>
|
|
<Text size="B300">Unpublish</Text>
|
|
</Chip>
|
|
) : (
|
|
<Chip
|
|
variant="Success"
|
|
radii="Pill"
|
|
disabled={loading}
|
|
onClick={handlePublish}
|
|
before={
|
|
publishState.status === AsyncStatus.Loading && (
|
|
<Spinner size="100" variant="Success" />
|
|
)
|
|
}
|
|
>
|
|
<Text size="B300">Publish</Text>
|
|
</Chip>
|
|
))}
|
|
<Chip
|
|
variant="Critical"
|
|
radii="Pill"
|
|
disabled={loading}
|
|
onClick={handleDelete}
|
|
before={
|
|
deleteState.status === AsyncStatus.Loading && (
|
|
<Spinner size="100" variant="Critical" />
|
|
)
|
|
}
|
|
>
|
|
<Text size="B300">Delete</Text>
|
|
</Chip>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
{localAliases.map((alias) => {
|
|
const published = publishedAliases.includes(alias);
|
|
const selected = selectedAliases.includes(alias);
|
|
|
|
return (
|
|
<Box key={alias} as="span" alignItems="Center" gap="200">
|
|
<Box shrink="No">
|
|
<Checkbox
|
|
checked={selected}
|
|
onChange={() => toggleSelect(alias)}
|
|
size="50"
|
|
variant="Primary"
|
|
disabled={loading}
|
|
/>
|
|
</Box>
|
|
<Box grow="Yes">
|
|
<Text size="T300" truncate>
|
|
{alias}
|
|
</Text>
|
|
</Box>
|
|
<Box shrink="No" gap="100">
|
|
{published && (
|
|
<Badge variant="Success" fill="Soft" size="500">
|
|
<Text size="L400">Published</Text>
|
|
</Badge>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
})}
|
|
{error && (
|
|
<Text size="T200" style={{ color: color.Critical.Main }}>
|
|
{error.message}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export function RoomLocalAddresses({ permissions }: { permissions: RoomPermissionsAPI }) {
|
|
const mx = useMatrixClient();
|
|
const room = useRoom();
|
|
|
|
const canEditCanonical = permissions.stateEvent(
|
|
StateEvent.RoomCanonicalAlias,
|
|
mx.getSafeUserId()
|
|
);
|
|
|
|
const [expand, setExpand] = useState(false);
|
|
|
|
const { localAliasesState, addLocalAlias, removeLocalAlias } = useLocalAliases(room.roomId);
|
|
|
|
return (
|
|
<SequenceCard
|
|
className={SequenceCardStyle}
|
|
variant="SurfaceVariant"
|
|
direction="Column"
|
|
gap="400"
|
|
>
|
|
<SettingTile
|
|
title="Local Addresses"
|
|
description="Set local address so users can join through your homeserver."
|
|
after={
|
|
<Button
|
|
type="button"
|
|
onClick={() => setExpand(!expand)}
|
|
size="300"
|
|
variant="Secondary"
|
|
fill="Soft"
|
|
outlined
|
|
radii="300"
|
|
before={
|
|
<Icon size="100" src={expand ? Icons.ChevronTop : Icons.ChevronBottom} filled />
|
|
}
|
|
>
|
|
<Text as="span" size="B300" truncate>
|
|
{expand ? 'Collapse' : 'Expand'}
|
|
</Text>
|
|
</Button>
|
|
}
|
|
/>
|
|
{expand && (
|
|
<CutoutCard variant="Surface" style={{ padding: config.space.S300 }}>
|
|
{localAliasesState.status === AsyncStatus.Loading && (
|
|
<Box gap="100">
|
|
<Spinner variant="Secondary" size="100" />
|
|
<Text size="T200">Loading...</Text>
|
|
</Box>
|
|
)}
|
|
{localAliasesState.status === AsyncStatus.Success &&
|
|
(localAliasesState.data.length === 0 ? (
|
|
<Box direction="Column" gap="100">
|
|
<Text size="L400">No Addresses</Text>
|
|
</Box>
|
|
) : (
|
|
<LocalAddressesList
|
|
localAliases={localAliasesState.data}
|
|
removeLocalAlias={removeLocalAlias}
|
|
canEditCanonical={canEditCanonical}
|
|
/>
|
|
))}
|
|
{localAliasesState.status === AsyncStatus.Error && (
|
|
<Box gap="100">
|
|
<Text size="T200" style={{ color: color.Critical.Main }}>
|
|
{localAliasesState.error.message}
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</CutoutCard>
|
|
)}
|
|
{expand && <LocalAddressInput addLocalAlias={addLocalAlias} />}
|
|
</SequenceCard>
|
|
);
|
|
}
|