mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-07 15:50:29 +03:00
Resolved merge conflict
This commit is contained in:
commit
cd0d4c9704
21 changed files with 481 additions and 119 deletions
|
|
@ -17,12 +17,16 @@ import { JoinRule } from 'matrix-js-sdk';
|
|||
import FocusTrap from 'focus-trap-react';
|
||||
import { stopPropagation } from '../utils/keyboard';
|
||||
|
||||
type JoinRuleIcons = Record<JoinRule, IconSrc>;
|
||||
export type ExtraJoinRules = 'knock_restricted';
|
||||
export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
|
||||
|
||||
type JoinRuleIcons = Record<ExtendedJoinRules, IconSrc>;
|
||||
export const useRoomJoinRuleIcon = (): JoinRuleIcons =>
|
||||
useMemo(
|
||||
() => ({
|
||||
[JoinRule.Invite]: Icons.HashLock,
|
||||
[JoinRule.Knock]: Icons.HashLock,
|
||||
knock_restricted: Icons.Hash,
|
||||
[JoinRule.Restricted]: Icons.Hash,
|
||||
[JoinRule.Public]: Icons.HashGlobe,
|
||||
[JoinRule.Private]: Icons.HashLock,
|
||||
|
|
@ -34,6 +38,7 @@ export const useSpaceJoinRuleIcon = (): JoinRuleIcons =>
|
|||
() => ({
|
||||
[JoinRule.Invite]: Icons.SpaceLock,
|
||||
[JoinRule.Knock]: Icons.SpaceLock,
|
||||
knock_restricted: Icons.Space,
|
||||
[JoinRule.Restricted]: Icons.Space,
|
||||
[JoinRule.Public]: Icons.SpaceGlobe,
|
||||
[JoinRule.Private]: Icons.SpaceLock,
|
||||
|
|
@ -41,12 +46,13 @@ export const useSpaceJoinRuleIcon = (): JoinRuleIcons =>
|
|||
[]
|
||||
);
|
||||
|
||||
type JoinRuleLabels = Record<JoinRule, string>;
|
||||
type JoinRuleLabels = Record<ExtendedJoinRules, string>;
|
||||
export const useRoomJoinRuleLabel = (): JoinRuleLabels =>
|
||||
useMemo(
|
||||
() => ({
|
||||
[JoinRule.Invite]: 'Invite Only',
|
||||
[JoinRule.Knock]: 'Knock & Invite',
|
||||
knock_restricted: 'Space Members or Knock',
|
||||
[JoinRule.Restricted]: 'Space Members',
|
||||
[JoinRule.Public]: 'Public',
|
||||
[JoinRule.Private]: 'Invite Only',
|
||||
|
|
@ -54,7 +60,7 @@ export const useRoomJoinRuleLabel = (): JoinRuleLabels =>
|
|||
[]
|
||||
);
|
||||
|
||||
type JoinRulesSwitcherProps<T extends JoinRule[]> = {
|
||||
type JoinRulesSwitcherProps<T extends ExtendedJoinRules[]> = {
|
||||
icons: JoinRuleIcons;
|
||||
labels: JoinRuleLabels;
|
||||
rules: T;
|
||||
|
|
@ -63,7 +69,7 @@ type JoinRulesSwitcherProps<T extends JoinRule[]> = {
|
|||
disabled?: boolean;
|
||||
changing?: boolean;
|
||||
};
|
||||
export function JoinRulesSwitcher<T extends JoinRule[]>({
|
||||
export function JoinRulesSwitcher<T extends ExtendedJoinRules[]>({
|
||||
icons,
|
||||
labels,
|
||||
rules,
|
||||
|
|
@ -79,7 +85,7 @@ export function JoinRulesSwitcher<T extends JoinRule[]>({
|
|||
};
|
||||
|
||||
const handleChange = useCallback(
|
||||
(selectedRule: JoinRule) => {
|
||||
(selectedRule: ExtendedJoinRules) => {
|
||||
setCords(undefined);
|
||||
onChange(selectedRule);
|
||||
},
|
||||
|
|
@ -131,7 +137,7 @@ export function JoinRulesSwitcher<T extends JoinRule[]>({
|
|||
fill="Soft"
|
||||
radii="300"
|
||||
outlined
|
||||
before={<Icon size="100" src={icons[value]} />}
|
||||
before={<Icon size="100" src={icons[value] ?? icons[JoinRule.Restricted]} />}
|
||||
after={
|
||||
changing ? (
|
||||
<Spinner size="100" variant="Secondary" fill="Soft" />
|
||||
|
|
@ -142,7 +148,7 @@ export function JoinRulesSwitcher<T extends JoinRule[]>({
|
|||
onClick={handleOpenMenu}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text size="B300">{labels[value]}</Text>
|
||||
<Text size="B300">{labels[value] ?? 'Unsupported'}</Text>
|
||||
</Button>
|
||||
</PopOut>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { FormEventHandler, useCallback } from 'react';
|
||||
import { Box, Text, Button, Spinner, color } from 'folds';
|
||||
import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
|
||||
import { decodeRecoveryKey, deriveRecoveryKeyFromPassphrase } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { PasswordInput } from './password-input';
|
||||
import {
|
||||
SecretStorageKeyContent,
|
||||
|
|
@ -29,11 +28,16 @@ export function SecretStorageRecoveryPassphrase({
|
|||
const [driveKeyState, submitPassphrase] = useAsyncCallback<
|
||||
Uint8Array,
|
||||
Error,
|
||||
Parameters<typeof deriveKey>
|
||||
Parameters<typeof deriveRecoveryKeyFromPassphrase>
|
||||
>(
|
||||
useCallback(
|
||||
async (passphrase, salt, iterations, bits) => {
|
||||
const decodedRecoveryKey = await deriveKey(passphrase, salt, iterations, bits);
|
||||
const decodedRecoveryKey = await deriveRecoveryKeyFromPassphrase(
|
||||
passphrase,
|
||||
salt,
|
||||
iterations,
|
||||
bits
|
||||
);
|
||||
|
||||
const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { stopPropagation } from '../../../utils/keyboard';
|
|||
import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { ModalWide } from '../../../styles/Modal.css';
|
||||
import { validBlurHash } from '../../../utils/blurHash';
|
||||
|
||||
type RenderViewerProps = {
|
||||
src: string;
|
||||
|
|
@ -77,7 +78,7 @@ export const ImageContent = as<'div', ImageContentProps>(
|
|||
) => {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const blurHash = info?.[MATRIX_BLUR_HASH_PROPERTY_NAME];
|
||||
const blurHash = validBlurHash(info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]);
|
||||
|
||||
const [load, setLoad] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
mxcUrlToHttp,
|
||||
} from '../../../utils/matrix';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { validBlurHash } from '../../../utils/blurHash';
|
||||
|
||||
type RenderVideoProps = {
|
||||
title: string;
|
||||
|
|
@ -68,7 +69,7 @@ export const VideoContent = as<'div', VideoContentProps>(
|
|||
) => {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const blurHash = info.thumbnail_info?.[MATRIX_BLUR_HASH_PROPERTY_NAME];
|
||||
const blurHash = validBlurHash(info.thumbnail_info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]);
|
||||
|
||||
const [load, setLoad] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const TextViewerContent = forwardRef<HTMLPreElement, TextViewerContentPro
|
|||
>
|
||||
<ErrorBoundary fallback={<code>{text}</code>}>
|
||||
<Suspense fallback={<code>{text}</code>}>
|
||||
<ReactPrism>{(codeRef) => <code ref={codeRef}>{text}</code>}</ReactPrism>
|
||||
<ReactPrism key={text}>{(codeRef) => <code ref={codeRef}>{text}</code>}</ReactPrism>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk';
|
|||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import {
|
||||
ExtendedJoinRules,
|
||||
JoinRulesSwitcher,
|
||||
useRoomJoinRuleIcon,
|
||||
useRoomJoinRuleLabel,
|
||||
|
|
@ -32,6 +33,7 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const roomVersion = parseInt(room.getVersion(), 10);
|
||||
const allowKnockRestricted = roomVersion >= 10;
|
||||
const allowRestricted = roomVersion >= 8;
|
||||
const allowKnock = roomVersion >= 7;
|
||||
const space = useSpaceOptionally();
|
||||
|
|
@ -47,18 +49,21 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
const content = joinRuleEvent?.getContent<RoomJoinRulesEventContent>();
|
||||
const rule: JoinRule = content?.join_rule ?? JoinRule.Invite;
|
||||
|
||||
const joinRules: Array<JoinRule> = useMemo(() => {
|
||||
const r: JoinRule[] = [JoinRule.Invite];
|
||||
const joinRules: Array<ExtendedJoinRules> = useMemo(() => {
|
||||
const r: ExtendedJoinRules[] = [JoinRule.Invite];
|
||||
if (allowKnock) {
|
||||
r.push(JoinRule.Knock);
|
||||
}
|
||||
if (allowRestricted && space) {
|
||||
r.push(JoinRule.Restricted);
|
||||
}
|
||||
if (allowKnockRestricted && space) {
|
||||
r.push('knock_restricted');
|
||||
}
|
||||
r.push(JoinRule.Public);
|
||||
|
||||
return r;
|
||||
}, [allowRestricted, allowKnock, space]);
|
||||
}, [allowKnockRestricted, allowRestricted, allowKnock, space]);
|
||||
|
||||
const icons = useRoomJoinRuleIcon();
|
||||
const spaceIcons = useSpaceJoinRuleIcon();
|
||||
|
|
@ -66,9 +71,9 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
|
||||
const [submitState, submit] = useAsyncCallback(
|
||||
useCallback(
|
||||
async (joinRule: JoinRule) => {
|
||||
async (joinRule: ExtendedJoinRules) => {
|
||||
const allow: RestrictedRoomAllowContent[] = [];
|
||||
if (joinRule === JoinRule.Restricted) {
|
||||
if (joinRule === JoinRule.Restricted || joinRule === 'knock_restricted') {
|
||||
const parents = getStateEvents(room, StateEvent.SpaceParent).map((event) =>
|
||||
event.getStateKey()
|
||||
);
|
||||
|
|
@ -82,7 +87,7 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
}
|
||||
|
||||
const c: RoomJoinRulesEventContent = {
|
||||
join_rule: joinRule,
|
||||
join_rule: joinRule as JoinRule,
|
||||
};
|
||||
if (allow.length > 0) c.allow = allow;
|
||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomJoinRules as any, c);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo } from 'react';
|
||||
import { Editor } from 'slate';
|
||||
import { Box, MenuItem, Text } from 'folds';
|
||||
import { Box, config, MenuItem, Text } from 'folds';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { Command, useCommands } from '../../hooks/useCommands';
|
||||
import {
|
||||
|
|
@ -75,9 +75,6 @@ export function CommandAutocomplete({
|
|||
headerContent={
|
||||
<Box grow="Yes" direction="Row" gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="L400">Commands</Text>
|
||||
<Text size="T200" priority="300" truncate>
|
||||
Begin your message with command
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
requestClose={requestClose}
|
||||
|
|
@ -87,17 +84,22 @@ export function CommandAutocomplete({
|
|||
key={commandName}
|
||||
as="button"
|
||||
radii="300"
|
||||
style={{ height: 'unset' }}
|
||||
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
|
||||
onTabPress(evt, () => handleAutocomplete(commandName))
|
||||
}
|
||||
onClick={() => handleAutocomplete(commandName)}
|
||||
>
|
||||
<Box grow="Yes" direction="Row" gap="200" justifyContent="SpaceBetween">
|
||||
<Box shrink="No">
|
||||
<Text style={{ flexGrow: 1 }} size="B400" truncate>
|
||||
{`/${commandName}`}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
style={{ padding: `${config.space.S300} 0` }}
|
||||
grow="Yes"
|
||||
direction="Column"
|
||||
gap="100"
|
||||
justifyContent="SpaceBetween"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} size="B400" truncate>
|
||||
{`/${commandName}`}
|
||||
</Text>
|
||||
<Text truncate priority="300" size="T200">
|
||||
{commands[commandName].description}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,127 @@
|
|||
import { MatrixClient, Room } from 'matrix-js-sdk';
|
||||
import { Direction, IContextResponse, MatrixClient, Method, Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { RoomServerAclEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import { useMemo } from 'react';
|
||||
import { getDMRoomFor, isRoomAlias, isRoomId, isUserId } from '../utils/matrix';
|
||||
import {
|
||||
getDMRoomFor,
|
||||
isRoomAlias,
|
||||
isRoomId,
|
||||
isServerName,
|
||||
isUserId,
|
||||
rateLimitedActions,
|
||||
} from '../utils/matrix';
|
||||
import { hasDevices } from '../../util/matrixUtil';
|
||||
import * as roomActions from '../../client/action/room';
|
||||
import { useRoomNavigate } from './useRoomNavigate';
|
||||
import { Membership, StateEvent } from '../../types/matrix/room';
|
||||
import { getStateEvent } from '../utils/room';
|
||||
import { splitWithSpace } from '../utils/common';
|
||||
|
||||
export const SHRUG = '¯\\_(ツ)_/¯';
|
||||
export const TABLEFLIP = '(╯°□°)╯︵ ┻━┻';
|
||||
export const UNFLIP = '┬─┬ノ( º_ºノ)';
|
||||
|
||||
export function parseUsersAndReason(payload: string): {
|
||||
users: string[];
|
||||
reason?: string;
|
||||
} {
|
||||
let reason: string | undefined;
|
||||
let ids: string = payload;
|
||||
const FLAG_PAT = '(?:^|\\s)-(\\w+)\\b';
|
||||
const FLAG_REG = new RegExp(FLAG_PAT);
|
||||
const FLAG_REG_G = new RegExp(FLAG_PAT, 'g');
|
||||
|
||||
const reasonMatch = payload.match(/\s-r\s/);
|
||||
if (reasonMatch) {
|
||||
ids = payload.slice(0, reasonMatch.index);
|
||||
reason = payload.slice((reasonMatch.index ?? 0) + reasonMatch[0].length);
|
||||
if (reason.trim() === '') reason = undefined;
|
||||
export const splitPayloadContentAndFlags = (payload: string): [string, string | undefined] => {
|
||||
const flagMatch = payload.match(FLAG_REG);
|
||||
|
||||
if (!flagMatch) {
|
||||
return [payload, undefined];
|
||||
}
|
||||
const rawIds = ids.split(' ');
|
||||
const users = rawIds.filter((id) => isUserId(id));
|
||||
return {
|
||||
users,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
const content = payload.slice(0, flagMatch.index);
|
||||
const flags = payload.slice(flagMatch.index);
|
||||
|
||||
return [content, flags];
|
||||
};
|
||||
|
||||
export const parseFlags = (flags: string | undefined): Record<string, string | undefined> => {
|
||||
const result: Record<string, string> = {};
|
||||
if (!flags) return result;
|
||||
|
||||
const matches: { key: string; index: number; match: string }[] = [];
|
||||
|
||||
for (let match = FLAG_REG_G.exec(flags); match !== null; match = FLAG_REG_G.exec(flags)) {
|
||||
matches.push({ key: match[1], index: match.index, match: match[0] });
|
||||
}
|
||||
|
||||
for (let i = 0; i < matches.length; i += 1) {
|
||||
const { key, match } = matches[i];
|
||||
const start = matches[i].index + match.length;
|
||||
const end = i + 1 < matches.length ? matches[i + 1].index : flags.length;
|
||||
const value = flags.slice(start, end).trim();
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const parseUsers = (payload: string): string[] => {
|
||||
const users: string[] = [];
|
||||
|
||||
splitWithSpace(payload).forEach((item) => {
|
||||
if (isUserId(item)) {
|
||||
users.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
export const parseServers = (payload: string): string[] => {
|
||||
const servers: string[] = [];
|
||||
|
||||
splitWithSpace(payload).forEach((item) => {
|
||||
if (isServerName(item)) {
|
||||
servers.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return servers;
|
||||
};
|
||||
|
||||
const getServerMembers = (room: Room, server: string): RoomMember[] => {
|
||||
const members: RoomMember[] = room
|
||||
.getMembers()
|
||||
.filter((member) => member.userId.endsWith(`:${server}`));
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
export const parseTimestampFlag = (input: string): number | undefined => {
|
||||
const match = input.match(/^(\d+(?:\.\d+)?)([dhms])$/); // supports floats like 1.5d
|
||||
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const value = parseFloat(match[1]); // supports decimal values
|
||||
const unit = match[2];
|
||||
|
||||
const now = Date.now(); // in milliseconds
|
||||
let delta = 0;
|
||||
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
delta = value * 24 * 60 * 60 * 1000;
|
||||
break;
|
||||
case 'h':
|
||||
delta = value * 60 * 60 * 1000;
|
||||
break;
|
||||
case 'm':
|
||||
delta = value * 60 * 1000;
|
||||
break;
|
||||
case 's':
|
||||
delta = value * 1000;
|
||||
break;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timestamp = now - delta;
|
||||
return timestamp;
|
||||
};
|
||||
|
||||
export type CommandExe = (payload: string) => Promise<void>;
|
||||
|
||||
|
|
@ -52,6 +145,8 @@ export enum Command {
|
|||
ConvertToRoom = 'converttoroom',
|
||||
TableFlip = 'tableflip',
|
||||
UnFlip = 'unflip',
|
||||
Delete = 'delete',
|
||||
Acl = 'acl',
|
||||
}
|
||||
|
||||
export type CommandContent = {
|
||||
|
|
@ -96,7 +191,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.StartDm,
|
||||
description: 'Start direct message with user. Example: /startdm userId1',
|
||||
exe: async (payload) => {
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const userIds = rawIds.filter((id) => isUserId(id) && id !== mx.getUserId());
|
||||
if (userIds.length === 0) return;
|
||||
if (userIds.length === 1) {
|
||||
|
|
@ -106,7 +201,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
return;
|
||||
}
|
||||
}
|
||||
const devices = await Promise.all(userIds.map(uid => hasDevices(mx, uid)));
|
||||
const devices = await Promise.all(userIds.map((uid) => hasDevices(mx, uid)));
|
||||
const isEncrypt = devices.every((hasDevice) => hasDevice);
|
||||
const result = await roomActions.createDM(mx, userIds, isEncrypt);
|
||||
navigateRoom(result.room_id);
|
||||
|
|
@ -116,7 +211,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.Join,
|
||||
description: 'Join room with address. Example: /join address1 address2',
|
||||
exe: async (payload) => {
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const roomIds = rawIds.filter(
|
||||
(idOrAlias) => isRoomId(idOrAlias) || isRoomAlias(idOrAlias)
|
||||
);
|
||||
|
|
@ -131,7 +226,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
mx.leave(room.roomId);
|
||||
return;
|
||||
}
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const roomIds = rawIds.filter((id) => isRoomId(id));
|
||||
roomIds.map((id) => mx.leave(id));
|
||||
},
|
||||
|
|
@ -140,7 +235,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.Invite,
|
||||
description: 'Invite user to room. Example: /invite userId1 userId2 [-r reason]',
|
||||
exe: async (payload) => {
|
||||
const { users, reason } = parseUsersAndReason(payload);
|
||||
const [content, flags] = splitPayloadContentAndFlags(payload);
|
||||
const users = parseUsers(content);
|
||||
const flagToContent = parseFlags(flags);
|
||||
const reason = flagToContent.r;
|
||||
users.map((id) => mx.invite(room.roomId, id, reason));
|
||||
},
|
||||
},
|
||||
|
|
@ -148,31 +246,64 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.DisInvite,
|
||||
description: 'Disinvite user to room. Example: /disinvite userId1 userId2 [-r reason]',
|
||||
exe: async (payload) => {
|
||||
const { users, reason } = parseUsersAndReason(payload);
|
||||
const [content, flags] = splitPayloadContentAndFlags(payload);
|
||||
const users = parseUsers(content);
|
||||
const flagToContent = parseFlags(flags);
|
||||
const reason = flagToContent.r;
|
||||
users.map((id) => mx.kick(room.roomId, id, reason));
|
||||
},
|
||||
},
|
||||
[Command.Kick]: {
|
||||
name: Command.Kick,
|
||||
description: 'Kick user from room. Example: /kick userId1 userId2 [-r reason]',
|
||||
description: 'Kick user from room. Example: /kick userId1 userId2 servername [-r reason]',
|
||||
exe: async (payload) => {
|
||||
const { users, reason } = parseUsersAndReason(payload);
|
||||
users.map((id) => mx.kick(room.roomId, id, reason));
|
||||
const [content, flags] = splitPayloadContentAndFlags(payload);
|
||||
const users = parseUsers(content);
|
||||
const servers = parseServers(content);
|
||||
const flagToContent = parseFlags(flags);
|
||||
const reason = flagToContent.r;
|
||||
|
||||
const serverMembers = servers?.flatMap((server) => getServerMembers(room, server));
|
||||
const serverUsers = serverMembers
|
||||
?.filter((m) => m.membership !== Membership.Ban)
|
||||
.map((m) => m.userId);
|
||||
|
||||
if (Array.isArray(serverUsers)) {
|
||||
serverUsers.forEach((user) => {
|
||||
if (!users.includes(user)) users.push(user);
|
||||
});
|
||||
}
|
||||
|
||||
rateLimitedActions(users, (id) => mx.kick(room.roomId, id, reason));
|
||||
},
|
||||
},
|
||||
[Command.Ban]: {
|
||||
name: Command.Ban,
|
||||
description: 'Ban user from room. Example: /ban userId1 userId2 [-r reason]',
|
||||
description: 'Ban user from room. Example: /ban userId1 userId2 servername [-r reason]',
|
||||
exe: async (payload) => {
|
||||
const { users, reason } = parseUsersAndReason(payload);
|
||||
users.map((id) => mx.ban(room.roomId, id, reason));
|
||||
const [content, flags] = splitPayloadContentAndFlags(payload);
|
||||
const users = parseUsers(content);
|
||||
const servers = parseServers(content);
|
||||
const flagToContent = parseFlags(flags);
|
||||
const reason = flagToContent.r;
|
||||
|
||||
const serverMembers = servers?.flatMap((server) => getServerMembers(room, server));
|
||||
const serverUsers = serverMembers?.map((m) => m.userId);
|
||||
|
||||
if (Array.isArray(serverUsers)) {
|
||||
serverUsers.forEach((user) => {
|
||||
if (!users.includes(user)) users.push(user);
|
||||
});
|
||||
}
|
||||
|
||||
rateLimitedActions(users, (id) => mx.ban(room.roomId, id, reason));
|
||||
},
|
||||
},
|
||||
[Command.UnBan]: {
|
||||
name: Command.UnBan,
|
||||
description: 'Unban user from room. Example: /unban userId1 userId2',
|
||||
exe: async (payload) => {
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const users = rawIds.filter((id) => isUserId(id));
|
||||
users.map((id) => mx.unban(room.roomId, id));
|
||||
},
|
||||
|
|
@ -181,7 +312,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.Ignore,
|
||||
description: 'Ignore user. Example: /ignore userId1 userId2',
|
||||
exe: async (payload) => {
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const userIds = rawIds.filter((id) => isUserId(id));
|
||||
if (userIds.length > 0) roomActions.ignore(mx, userIds);
|
||||
},
|
||||
|
|
@ -190,7 +321,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.UnIgnore,
|
||||
description: 'Unignore user. Example: /unignore userId1 userId2',
|
||||
exe: async (payload) => {
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const userIds = rawIds.filter((id) => isUserId(id));
|
||||
if (userIds.length > 0) roomActions.unignore(mx, userIds);
|
||||
},
|
||||
|
|
@ -227,6 +358,124 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
roomActions.convertToRoom(mx, room.roomId);
|
||||
},
|
||||
},
|
||||
[Command.Delete]: {
|
||||
name: Command.Delete,
|
||||
description:
|
||||
'Delete messages from users. Example: /delete userId1 servername -past 1d|2h|5m|30s [-t m.room.message] [-r spam]',
|
||||
exe: async (payload) => {
|
||||
const [content, flags] = splitPayloadContentAndFlags(payload);
|
||||
const users = parseUsers(content);
|
||||
const servers = parseServers(content);
|
||||
|
||||
const flagToContent = parseFlags(flags);
|
||||
const reason = flagToContent.r;
|
||||
const pastContent = flagToContent.past ?? '';
|
||||
const msgTypeContent = flagToContent.t;
|
||||
const messageTypes: string[] = msgTypeContent ? splitWithSpace(msgTypeContent) : [];
|
||||
|
||||
const ts = parseTimestampFlag(pastContent);
|
||||
if (!ts) return;
|
||||
|
||||
const serverMembers = servers?.flatMap((server) => getServerMembers(room, server));
|
||||
const serverUsers = serverMembers?.map((m) => m.userId);
|
||||
|
||||
if (Array.isArray(serverUsers)) {
|
||||
serverUsers.forEach((user) => {
|
||||
if (!users.includes(user)) users.push(user);
|
||||
});
|
||||
}
|
||||
|
||||
const result = await mx.timestampToEvent(room.roomId, ts, Direction.Forward);
|
||||
const startEventId = result.event_id;
|
||||
|
||||
const path = `/rooms/${encodeURIComponent(room.roomId)}/context/${encodeURIComponent(
|
||||
startEventId
|
||||
)}`;
|
||||
const eventContext = await mx.http.authedRequest<IContextResponse>(Method.Get, path, {
|
||||
limit: 0,
|
||||
});
|
||||
|
||||
let token: string | undefined = eventContext.start;
|
||||
while (token) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await mx.createMessagesRequest(
|
||||
room.roomId,
|
||||
token,
|
||||
20,
|
||||
Direction.Forward,
|
||||
undefined
|
||||
);
|
||||
const { end, chunk } = response;
|
||||
// remove until the latest event;
|
||||
token = end;
|
||||
|
||||
const eventsToDelete = chunk.filter(
|
||||
(roomEvent) =>
|
||||
(messageTypes.length > 0 ? messageTypes.includes(roomEvent.type) : true) &&
|
||||
users.includes(roomEvent.sender) &&
|
||||
roomEvent.unsigned?.redacted_because === undefined
|
||||
);
|
||||
|
||||
const eventIds = eventsToDelete.map((roomEvent) => roomEvent.event_id);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await rateLimitedActions(eventIds, (eventId) =>
|
||||
mx.redactEvent(room.roomId, eventId, undefined, { reason })
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
[Command.Acl]: {
|
||||
name: Command.Acl,
|
||||
description:
|
||||
'Manage server access control list. Example /acl [-a servername1] [-d servername2] [-ra servername1] [-rd servername2]',
|
||||
exe: async (payload) => {
|
||||
const [, flags] = splitPayloadContentAndFlags(payload);
|
||||
|
||||
const flagToContent = parseFlags(flags);
|
||||
const allowFlag = flagToContent.a;
|
||||
const denyFlag = flagToContent.d;
|
||||
const removeAllowFlag = flagToContent.ra;
|
||||
const removeDenyFlag = flagToContent.rd;
|
||||
|
||||
const allowList = allowFlag ? splitWithSpace(allowFlag) : [];
|
||||
const denyList = denyFlag ? splitWithSpace(denyFlag) : [];
|
||||
const removeAllowList = removeAllowFlag ? splitWithSpace(removeAllowFlag) : [];
|
||||
const removeDenyList = removeDenyFlag ? splitWithSpace(removeDenyFlag) : [];
|
||||
|
||||
const serverAcl = getStateEvent(
|
||||
room,
|
||||
StateEvent.RoomServerAcl
|
||||
)?.getContent<RoomServerAclEventContent>();
|
||||
|
||||
const aclContent: RoomServerAclEventContent = {
|
||||
allow: serverAcl?.allow ? [...serverAcl.allow] : [],
|
||||
allow_ip_literals: serverAcl?.allow_ip_literals,
|
||||
deny: serverAcl?.deny ? [...serverAcl.deny] : [],
|
||||
};
|
||||
|
||||
allowList.forEach((servername) => {
|
||||
if (!Array.isArray(aclContent.allow) || aclContent.allow.includes(servername)) return;
|
||||
aclContent.allow.push(servername);
|
||||
});
|
||||
denyList.forEach((servername) => {
|
||||
if (!Array.isArray(aclContent.deny) || aclContent.deny.includes(servername)) return;
|
||||
aclContent.deny.push(servername);
|
||||
});
|
||||
|
||||
aclContent.allow = aclContent.allow?.filter(
|
||||
(servername) => !removeAllowList.includes(servername)
|
||||
);
|
||||
aclContent.deny = aclContent.deny?.filter(
|
||||
(servername) => !removeDenyList.includes(servername)
|
||||
);
|
||||
|
||||
aclContent.allow?.sort();
|
||||
aclContent.deny?.sort();
|
||||
|
||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomServerAcl as any, aclContent);
|
||||
},
|
||||
},
|
||||
}),
|
||||
[mx, room, navigateRoom]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { IMyDevice } from 'matrix-js-sdk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
|
||||
import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export const useDeviceListChange = (
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function AuthFooter() {
|
|||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
v4.6.0
|
||||
v4.7.1
|
||||
</Text>
|
||||
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
|
||||
Twitter
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export const login = async (
|
|||
}
|
||||
|
||||
const mx = createClient({ baseUrl: url });
|
||||
const [err, res] = await to<LoginResponse, MatrixError>(mx.login(data.type, data));
|
||||
const [err, res] = await to<LoginResponse, MatrixError>(mx.loginRequest(data));
|
||||
|
||||
if (err) {
|
||||
if (err.httpStatus === 400) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function WelcomePage() {
|
|||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
v4.6.0
|
||||
v4.7.1
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { atom } from 'jotai';
|
||||
import { ImportRoomKeyProgressData } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { ImportRoomKeyProgressData, ImportRoomKeyStage } from 'matrix-js-sdk/lib/crypto-api';
|
||||
|
||||
export enum BackupProgressStatus {
|
||||
Idle,
|
||||
|
|
@ -39,22 +39,16 @@ export const backupRestoreProgressAtom = atom<
|
|||
>(
|
||||
(get) => get(baseBackupRestoreProgressAtom),
|
||||
(get, set, progress) => {
|
||||
if (progress.stage === 'fetch') {
|
||||
if (progress.stage === ImportRoomKeyStage.Fetch) {
|
||||
set(baseBackupRestoreProgressAtom, {
|
||||
status: BackupProgressStatus.Fetching,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress.stage === 'load_keys') {
|
||||
if (progress.stage === ImportRoomKeyStage.LoadKeys) {
|
||||
const { total, successes, failures } = progress;
|
||||
if (total === undefined || successes === undefined || failures === undefined) {
|
||||
// Setting to idle as https://github.com/matrix-org/matrix-js-sdk/issues/4703
|
||||
set(baseBackupRestoreProgressAtom, {
|
||||
status: BackupProgressStatus.Idle,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const downloaded = successes + failures;
|
||||
if (downloaded === total) {
|
||||
set(baseBackupRestoreProgressAtom, {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { encode } from 'blurhash';
|
||||
import { encode, isBlurhashValid } from 'blurhash';
|
||||
|
||||
export const encodeBlurHash = (
|
||||
img: HTMLImageElement | HTMLVideoElement,
|
||||
|
|
@ -17,3 +17,13 @@ export const encodeBlurHash = (
|
|||
const data = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
return encode(data.data, data.width, data.height, 4, 4);
|
||||
};
|
||||
|
||||
export const validBlurHash = (hash?: string): string | undefined => {
|
||||
if (typeof hash === 'string') {
|
||||
const validity = isBlurhashValid(hash);
|
||||
|
||||
return validity.result ? hash : undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -125,3 +125,9 @@ export const suffixRename = (name: string, validator: (newName: string) => boole
|
|||
};
|
||||
|
||||
export const replaceSpaceWithDash = (str: string): string => str.replace(/ /g, '-');
|
||||
|
||||
export const splitWithSpace = (content: string): string[] => {
|
||||
const trimmedContent = content.trim();
|
||||
if (trimmedContent === '') return [];
|
||||
return trimmedContent.split(' ');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,11 +13,16 @@ import {
|
|||
UploadProgress,
|
||||
UploadResponse,
|
||||
} from 'matrix-js-sdk';
|
||||
import to from 'await-to-js';
|
||||
import { IImageInfo, IThumbnailContent, IVideoInfo } from '../../types/matrix/common';
|
||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||
import { getStateEvent } from './room';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
|
||||
const DOMAIN_REGEX = /\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b/;
|
||||
|
||||
export const isServerName = (serverName: string): boolean => DOMAIN_REGEX.test(serverName);
|
||||
|
||||
export const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@!$+#])(.+):(\S+)$/);
|
||||
|
||||
export const validMxId = (id: string): boolean => !!matchMxId(id);
|
||||
|
|
@ -292,3 +297,35 @@ export const downloadEncryptedMedia = async (
|
|||
|
||||
return decryptedContent;
|
||||
};
|
||||
|
||||
export const rateLimitedActions = async <T, R = void>(
|
||||
data: T[],
|
||||
callback: (item: T) => Promise<R>,
|
||||
maxRetryCount?: number
|
||||
) => {
|
||||
let retryCount = 0;
|
||||
const performAction = async (dataItem: T) => {
|
||||
const [err] = await to<R, MatrixError>(callback(dataItem));
|
||||
|
||||
if (err?.httpStatus === 429) {
|
||||
if (retryCount === maxRetryCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const waitMS = err.getRetryAfterMs() ?? 200;
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, waitMS);
|
||||
});
|
||||
retryCount += 1;
|
||||
|
||||
await performAction(dataItem);
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const dataItem = data[i];
|
||||
retryCount = 0;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await performAction(dataItem);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
|
||||
import { logger } from 'matrix-js-sdk/lib/logger';
|
||||
|
||||
import { cryptoCallbacks } from './state/secretStorageKeys';
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
logger.disableAll();
|
||||
}
|
||||
|
||||
type Session = {
|
||||
baseUrl: string;
|
||||
accessToken: string;
|
||||
|
|
@ -38,7 +33,6 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
|
|||
await indexedDBStore.startup();
|
||||
await mx.initRustCrypto();
|
||||
|
||||
mx.setGlobalErrorOnUnknownDevices(false);
|
||||
mx.setMaxListeners(50);
|
||||
|
||||
return mx;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const cons = {
|
||||
version: '4.6.0',
|
||||
version: '4.7.1',
|
||||
secretKey: {
|
||||
ACCESS_TOKEN: 'cinny_access_token',
|
||||
DEVICE_ID: 'cinny_device_id',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue