mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-09 16:50:28 +03:00
Merge branch 'dev' into explore-persistent-server-list
This commit is contained in:
commit
dd09838584
213 changed files with 11222 additions and 2006 deletions
12
src/app/hooks/router/useCreateSelected.ts
Normal file
12
src/app/hooks/router/useCreateSelected.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { useMatch } from 'react-router-dom';
|
||||
import { getCreatePath } from '../../pages/pathUtils';
|
||||
|
||||
export const useCreateSelected = (): boolean => {
|
||||
const match = useMatch({
|
||||
path: getCreatePath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
17
src/app/hooks/useAccountManagement.ts
Normal file
17
src/app/hooks/useAccountManagement.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
export const useAccountManagementActions = () => {
|
||||
const actions = useMemo(
|
||||
() => ({
|
||||
profile: 'org.matrix.profile',
|
||||
sessionsList: 'org.matrix.sessions_list',
|
||||
sessionView: 'org.matrix.session_view',
|
||||
sessionEnd: 'org.matrix.session_end',
|
||||
accountDeactivate: 'org.matrix.account_deactivate',
|
||||
crossSigningReset: 'org.matrix.cross_signing_reset',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return actions;
|
||||
};
|
||||
12
src/app/hooks/useAuthMetadata.ts
Normal file
12
src/app/hooks/useAuthMetadata.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { ValidatedAuthMetadata } from 'matrix-js-sdk';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const AuthMetadataContext = createContext<ValidatedAuthMetadata | undefined>(undefined);
|
||||
|
||||
export const AuthMetadataProvider = AuthMetadataContext.Provider;
|
||||
|
||||
export const useAuthMetadata = (): ValidatedAuthMetadata | undefined => {
|
||||
const metadata = useContext(AuthMetadataContext);
|
||||
|
||||
return metadata;
|
||||
};
|
||||
|
|
@ -1,34 +1,130 @@
|
|||
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 {
|
||||
addRoomIdToMDirect,
|
||||
getDMRoomFor,
|
||||
guessDmRoomUserId,
|
||||
isRoomAlias,
|
||||
isRoomId,
|
||||
isServerName,
|
||||
isUserId,
|
||||
rateLimitedActions,
|
||||
removeRoomIdFromMDirect,
|
||||
} 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 +148,8 @@ export enum Command {
|
|||
ConvertToRoom = 'converttoroom',
|
||||
TableFlip = 'tableflip',
|
||||
UnFlip = 'unflip',
|
||||
Delete = 'delete',
|
||||
Acl = 'acl',
|
||||
}
|
||||
|
||||
export type CommandContent = {
|
||||
|
|
@ -96,7 +194,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 +204,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 +214,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 +229,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 +238,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 +249,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 +315,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 +324,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);
|
||||
},
|
||||
|
|
@ -217,14 +351,133 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.ConvertToDm,
|
||||
description: 'Convert room to direct message',
|
||||
exe: async () => {
|
||||
roomActions.convertToDm(mx, room.roomId);
|
||||
const dmUserId = guessDmRoomUserId(room, mx.getSafeUserId());
|
||||
await addRoomIdToMDirect(mx, room.roomId, dmUserId);
|
||||
},
|
||||
},
|
||||
[Command.ConvertToRoom]: {
|
||||
name: Command.ConvertToRoom,
|
||||
description: 'Convert direct message to room',
|
||||
exe: async () => {
|
||||
roomActions.convertToRoom(mx, room.roomId);
|
||||
await removeRoomIdFromMDirect(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);
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
34
src/app/hooks/useDateFormat.ts
Normal file
34
src/app/hooks/useDateFormat.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { useMemo } from 'react';
|
||||
import { DateFormat } from '../state/settings';
|
||||
|
||||
export type DateFormatItem = {
|
||||
name: string;
|
||||
format: DateFormat;
|
||||
};
|
||||
|
||||
export const useDateFormatItems = (): DateFormatItem[] =>
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
format: 'D MMM YYYY',
|
||||
name: 'D MMM YYYY',
|
||||
},
|
||||
{
|
||||
format: 'DD/MM/YYYY',
|
||||
name: 'DD/MM/YYYY',
|
||||
},
|
||||
{
|
||||
format: 'MM/DD/YYYY',
|
||||
name: 'MM/DD/YYYY',
|
||||
},
|
||||
{
|
||||
format: 'YYYY/MM/DD',
|
||||
name: 'YYYY/MM/DD',
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
name: 'Custom',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
|
@ -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 = (
|
||||
|
|
|
|||
27
src/app/hooks/useDirectUsers.ts
Normal file
27
src/app/hooks/useDirectUsers.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useMemo } from 'react';
|
||||
import { AccountDataEvent, MDirectContent } from '../../types/matrix/accountData';
|
||||
import { useAccountData } from './useAccountData';
|
||||
import { useAllJoinedRoomsSet, useGetRoom } from './useGetRoom';
|
||||
|
||||
export const useDirectUsers = (): string[] => {
|
||||
const directEvent = useAccountData(AccountDataEvent.Direct);
|
||||
const content = directEvent?.getContent<MDirectContent>();
|
||||
|
||||
const allJoinedRooms = useAllJoinedRoomsSet();
|
||||
const getRoom = useGetRoom(allJoinedRooms);
|
||||
|
||||
const users = useMemo(() => {
|
||||
if (typeof content !== 'object') return [];
|
||||
|
||||
const u = Object.keys(content).filter((userId) => {
|
||||
const rooms = content[userId];
|
||||
if (!Array.isArray(rooms)) return false;
|
||||
const hasDM = rooms.some((roomId) => typeof roomId === 'string' && !!getRoom(roomId));
|
||||
return hasDM;
|
||||
});
|
||||
|
||||
return u;
|
||||
}, [content, getRoom]);
|
||||
|
||||
return users;
|
||||
};
|
||||
36
src/app/hooks/useListFocusIndex.ts
Normal file
36
src/app/hooks/useListFocusIndex.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
|
||||
export const useListFocusIndex = (size: number, initialIndex: number) => {
|
||||
const [index, setIndex] = useState(initialIndex);
|
||||
|
||||
const next = useCallback(() => {
|
||||
setIndex((i) => {
|
||||
const nextIndex = i + 1;
|
||||
if (nextIndex >= size) {
|
||||
return 0;
|
||||
}
|
||||
return nextIndex;
|
||||
});
|
||||
}, [size]);
|
||||
|
||||
const previous = useCallback(() => {
|
||||
setIndex((i) => {
|
||||
const previousIndex = i - 1;
|
||||
if (previousIndex < 0) {
|
||||
return size - 1;
|
||||
}
|
||||
return previousIndex;
|
||||
});
|
||||
}, [size]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setIndex(initialIndex);
|
||||
}, [initialIndex]);
|
||||
|
||||
return {
|
||||
index,
|
||||
next,
|
||||
previous,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
28
src/app/hooks/useMemberPowerCompare.ts
Normal file
28
src/app/hooks/useMemberPowerCompare.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useCallback } from 'react';
|
||||
import { IPowerLevels, readPowerLevel } from './usePowerLevels';
|
||||
|
||||
export const useMemberPowerCompare = (creators: Set<string>, powerLevels: IPowerLevels) => {
|
||||
/**
|
||||
* returns `true` if `userIdA` has more power than `userIdB`
|
||||
* returns `false` otherwise
|
||||
*/
|
||||
const hasMorePower = useCallback(
|
||||
(userIdA: string, userIdB: string): boolean => {
|
||||
const aIsCreator = creators.has(userIdA);
|
||||
const bIsCreator = creators.has(userIdB);
|
||||
if (aIsCreator && bIsCreator) return false;
|
||||
if (aIsCreator) return true;
|
||||
if (bIsCreator) return false;
|
||||
|
||||
const aPower = readPowerLevel.user(powerLevels, userIdA);
|
||||
const bPower = readPowerLevel.user(powerLevels, userIdB);
|
||||
|
||||
return aPower > bPower;
|
||||
},
|
||||
[creators, powerLevels]
|
||||
);
|
||||
|
||||
return {
|
||||
hasMorePower,
|
||||
};
|
||||
};
|
||||
87
src/app/hooks/useMemberPowerTag.ts
Normal file
87
src/app/hooks/useMemberPowerTag.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { getPowerLevelTag, PowerLevelTags, usePowerLevelTags } from './usePowerLevelTags';
|
||||
import { IPowerLevels, readPowerLevel } from './usePowerLevels';
|
||||
import { MemberPowerTag, MemberPowerTagIcon } from '../../types/matrix/room';
|
||||
import { useRoomCreatorsTag } from './useRoomCreatorsTag';
|
||||
import { ThemeKind } from './useTheme';
|
||||
import { accessibleColor } from '../plugins/color';
|
||||
|
||||
export type GetMemberPowerTag = (userId: string) => MemberPowerTag;
|
||||
|
||||
export const useGetMemberPowerTag = (
|
||||
room: Room,
|
||||
creators: Set<string>,
|
||||
powerLevels: IPowerLevels
|
||||
) => {
|
||||
const creatorsTag = useRoomCreatorsTag();
|
||||
const powerLevelTags = usePowerLevelTags(room, powerLevels);
|
||||
|
||||
const getMemberPowerTag: GetMemberPowerTag = useCallback(
|
||||
(userId) => {
|
||||
if (creators.has(userId)) {
|
||||
return creatorsTag;
|
||||
}
|
||||
|
||||
const power = readPowerLevel.user(powerLevels, userId);
|
||||
return getPowerLevelTag(powerLevelTags, power);
|
||||
},
|
||||
[creators, creatorsTag, powerLevels, powerLevelTags]
|
||||
);
|
||||
|
||||
return getMemberPowerTag;
|
||||
};
|
||||
|
||||
export const getPowerTagIconSrc = (
|
||||
mx: MatrixClient,
|
||||
useAuthentication: boolean,
|
||||
icon: MemberPowerTagIcon
|
||||
): string | undefined =>
|
||||
icon?.key?.startsWith('mxc://')
|
||||
? mx.mxcUrlToHttp(icon.key, 96, 96, 'scale', undefined, undefined, useAuthentication) ?? '🌻'
|
||||
: icon?.key;
|
||||
|
||||
export const useAccessiblePowerTagColors = (
|
||||
themeKind: ThemeKind,
|
||||
creatorsTag: MemberPowerTag,
|
||||
powerLevelTags: PowerLevelTags
|
||||
): Map<string, string> => {
|
||||
const accessibleColors: Map<string, string> = useMemo(() => {
|
||||
const colors: Map<string, string> = new Map();
|
||||
if (creatorsTag.color) {
|
||||
colors.set(creatorsTag.color, accessibleColor(themeKind, creatorsTag.color));
|
||||
}
|
||||
|
||||
Object.values(powerLevelTags).forEach((tag) => {
|
||||
const { color } = tag;
|
||||
if (!color) return;
|
||||
|
||||
colors.set(color, accessibleColor(themeKind, color));
|
||||
});
|
||||
|
||||
return colors;
|
||||
}, [powerLevelTags, creatorsTag, themeKind]);
|
||||
|
||||
return accessibleColors;
|
||||
};
|
||||
|
||||
export const useFlattenPowerTagMembers = (
|
||||
members: RoomMember[],
|
||||
getTag: GetMemberPowerTag
|
||||
): Array<MemberPowerTag | RoomMember> => {
|
||||
const PLTagOrRoomMember = useMemo(() => {
|
||||
let prevTag: MemberPowerTag | undefined;
|
||||
const tagOrMember: Array<MemberPowerTag | RoomMember> = [];
|
||||
members.forEach((member) => {
|
||||
const tag = getTag(member.userId);
|
||||
if (tag !== prevTag) {
|
||||
prevTag = tag;
|
||||
tagOrMember.push(tag);
|
||||
}
|
||||
tagOrMember.push(member);
|
||||
});
|
||||
return tagOrMember;
|
||||
}, [members, getTag]);
|
||||
|
||||
return PLTagOrRoomMember;
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { RoomMember } from 'matrix-js-sdk';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const MemberSort = {
|
||||
Ascending: (a: RoomMember, b: RoomMember) =>
|
||||
|
|
@ -46,3 +46,20 @@ export const useMemberSort = (index: number, memberSort: MemberSortItem[]): Memb
|
|||
const item = memberSort[index] ?? memberSort[0];
|
||||
return item;
|
||||
};
|
||||
|
||||
export const useMemberPowerSort = (creators: Set<string>): MemberSortFn => {
|
||||
const sort: MemberSortFn = useCallback(
|
||||
(a, b) => {
|
||||
if (creators.has(a.userId) && creators.has(b.userId)) {
|
||||
return 0;
|
||||
}
|
||||
if (creators.has(a.userId)) return -1;
|
||||
if (creators.has(b.userId)) return 1;
|
||||
|
||||
return b.powerLevel - a.powerLevel;
|
||||
},
|
||||
[creators]
|
||||
);
|
||||
|
||||
return sort;
|
||||
};
|
||||
|
|
|
|||
28
src/app/hooks/useMembership.ts
Normal file
28
src/app/hooks/useMembership.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Room, RoomMemberEvent, RoomMemberEventHandlerMap } from 'matrix-js-sdk';
|
||||
import { Membership } from '../../types/matrix/room';
|
||||
|
||||
export const useMembership = (room: Room, userId: string): Membership => {
|
||||
const member = room.getMember(userId);
|
||||
|
||||
const [membership, setMembership] = useState<Membership>(
|
||||
() => (member?.membership as Membership | undefined) ?? Membership.Leave
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMembershipChange: RoomMemberEventHandlerMap[RoomMemberEvent.Membership] = (
|
||||
event,
|
||||
m
|
||||
) => {
|
||||
if (event.getRoomId() === room.roomId && m.userId === userId) {
|
||||
setMembership((m.membership as Membership | undefined) ?? Membership.Leave);
|
||||
}
|
||||
};
|
||||
member?.on(RoomMemberEvent.Membership, handleMembershipChange);
|
||||
return () => {
|
||||
member?.removeListener(RoomMemberEvent.Membership, handleMembershipChange);
|
||||
};
|
||||
}, [room, member, userId]);
|
||||
|
||||
return membership;
|
||||
};
|
||||
|
|
@ -3,14 +3,17 @@ import { useNavigate } from 'react-router-dom';
|
|||
import { useRoomNavigate } from './useRoomNavigate';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { isRoomId, isUserId } from '../utils/matrix';
|
||||
import { openProfileViewer } from '../../client/action/navigation';
|
||||
import { getHomeRoomPath, withSearchParam } from '../pages/pathUtils';
|
||||
import { _RoomSearchParams } from '../pages/paths';
|
||||
import { useOpenUserRoomProfile } from '../state/hooks/userRoomProfile';
|
||||
import { useSpaceOptionally } from './useSpace';
|
||||
|
||||
export const useMentionClickHandler = (roomId: string): ReactEventHandler<HTMLElement> => {
|
||||
const mx = useMatrixClient();
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const navigate = useNavigate();
|
||||
const openProfile = useOpenUserRoomProfile();
|
||||
const space = useSpaceOptionally();
|
||||
|
||||
const handleClick: ReactEventHandler<HTMLElement> = useCallback(
|
||||
(evt) => {
|
||||
|
|
@ -21,7 +24,7 @@ export const useMentionClickHandler = (roomId: string): ReactEventHandler<HTMLEl
|
|||
if (typeof mentionId !== 'string') return;
|
||||
|
||||
if (isUserId(mentionId)) {
|
||||
openProfileViewer(mentionId, roomId);
|
||||
openProfile(roomId, space?.roomId, mentionId, target.getBoundingClientRect());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +40,7 @@ export const useMentionClickHandler = (roomId: string): ReactEventHandler<HTMLEl
|
|||
|
||||
navigate(viaServers ? withSearchParam<_RoomSearchParams>(path, { viaServers }) : path);
|
||||
},
|
||||
[mx, navigate, navigateRoom, navigateSpace, roomId]
|
||||
[mx, navigate, navigateRoom, navigateSpace, roomId, space, openProfile]
|
||||
);
|
||||
|
||||
return handleClick;
|
||||
|
|
|
|||
30
src/app/hooks/useMutualRooms.ts
Normal file
30
src/app/hooks/useMutualRooms.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback';
|
||||
import { useSpecVersions } from './useSpecVersions';
|
||||
|
||||
export const useMutualRoomsSupport = (): boolean => {
|
||||
const { unstable_features: unstableFeatures } = useSpecVersions();
|
||||
|
||||
const supported =
|
||||
unstableFeatures?.['uk.half-shot.msc2666'] ||
|
||||
unstableFeatures?.['uk.half-shot.msc2666.mutual_rooms'] ||
|
||||
unstableFeatures?.['uk.half-shot.msc2666.query_mutual_rooms'];
|
||||
|
||||
return !!supported;
|
||||
};
|
||||
|
||||
export const useMutualRooms = (userId: string): AsyncState<string[], unknown> => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const supported = useMutualRoomsSupport();
|
||||
|
||||
const [mutualRoomsState] = useAsyncCallbackValue(
|
||||
useCallback(
|
||||
() => (supported ? mx._unstable_getSharedRooms(userId) : Promise.resolve([])),
|
||||
[mx, userId, supported]
|
||||
)
|
||||
);
|
||||
|
||||
return mutualRoomsState;
|
||||
};
|
||||
|
|
@ -1,29 +1,24 @@
|
|||
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { useMemo } from 'react';
|
||||
import { IPowerLevels } from './usePowerLevels';
|
||||
import { useStateEvent } from './useStateEvent';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { IImageInfo } from '../../types/matrix/common';
|
||||
import { ThemeKind } from './useTheme';
|
||||
import { accessibleColor } from '../plugins/color';
|
||||
import { MemberPowerTag, StateEvent } from '../../types/matrix/room';
|
||||
|
||||
export type PowerLevelTagIcon = {
|
||||
key?: string;
|
||||
info?: IImageInfo;
|
||||
};
|
||||
export type PowerLevelTag = {
|
||||
name: string;
|
||||
color?: string;
|
||||
icon?: PowerLevelTagIcon;
|
||||
};
|
||||
export type PowerLevelTags = Record<number, MemberPowerTag>;
|
||||
|
||||
export type PowerLevelTags = Record<number, PowerLevelTag>;
|
||||
|
||||
export const powerSortFn = (a: number, b: number) => b - a;
|
||||
export const sortPowers = (powers: number[]): number[] => powers.sort(powerSortFn);
|
||||
const powerSortFn = (a: number, b: number) => b - a;
|
||||
const sortPowers = (powers: number[]): number[] => powers.sort(powerSortFn);
|
||||
|
||||
export const getPowers = (tags: PowerLevelTags): number[] => {
|
||||
const powers: number[] = Object.keys(tags).map((p) => parseInt(p, 10));
|
||||
const powers: number[] = Object.keys(tags)
|
||||
.map((p) => {
|
||||
const power = parseInt(p, 10);
|
||||
if (Number.isNaN(power)) {
|
||||
return undefined;
|
||||
}
|
||||
return power;
|
||||
})
|
||||
.filter((power) => typeof power === 'number');
|
||||
|
||||
return sortPowers(powers);
|
||||
};
|
||||
|
|
@ -55,8 +50,8 @@ const DEFAULT_TAGS: PowerLevelTags = {
|
|||
name: 'Goku',
|
||||
color: '#ff6a00',
|
||||
},
|
||||
102: {
|
||||
name: 'Goku Reborn',
|
||||
150: {
|
||||
name: 'Manager',
|
||||
color: '#ff6a7f',
|
||||
},
|
||||
101: {
|
||||
|
|
@ -81,7 +76,7 @@ const DEFAULT_TAGS: PowerLevelTags = {
|
|||
},
|
||||
};
|
||||
|
||||
const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): PowerLevelTag => {
|
||||
const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): MemberPowerTag => {
|
||||
const highToLow = sortPowers(getPowers(powerLevelTags));
|
||||
|
||||
const tagPower = highToLow.find((p) => p < power);
|
||||
|
|
@ -92,12 +87,7 @@ const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): Pow
|
|||
};
|
||||
};
|
||||
|
||||
export type GetPowerLevelTag = (powerLevel: number) => PowerLevelTag;
|
||||
|
||||
export const usePowerLevelTags = (
|
||||
room: Room,
|
||||
powerLevels: IPowerLevels
|
||||
): [PowerLevelTags, GetPowerLevelTag] => {
|
||||
export const usePowerLevelTags = (room: Room, powerLevels: IPowerLevels): PowerLevelTags => {
|
||||
const tagsEvent = useStateEvent(room, StateEvent.PowerLevelTags);
|
||||
|
||||
const powerLevelTags: PowerLevelTags = useMemo(() => {
|
||||
|
|
@ -114,66 +104,13 @@ export const usePowerLevelTags = (
|
|||
return powerToTags;
|
||||
}, [powerLevels, tagsEvent]);
|
||||
|
||||
const getTag: GetPowerLevelTag = useCallback(
|
||||
(power) => {
|
||||
const tag: PowerLevelTag | undefined = powerLevelTags[power];
|
||||
return tag ?? generateFallbackTag(DEFAULT_TAGS, power);
|
||||
},
|
||||
[powerLevelTags]
|
||||
);
|
||||
|
||||
return [powerLevelTags, getTag];
|
||||
return powerLevelTags;
|
||||
};
|
||||
|
||||
export const useFlattenPowerLevelTagMembers = (
|
||||
members: RoomMember[],
|
||||
getPowerLevel: (userId: string) => number,
|
||||
getTag: GetPowerLevelTag
|
||||
): Array<PowerLevelTag | RoomMember> => {
|
||||
const PLTagOrRoomMember = useMemo(() => {
|
||||
let prevTag: PowerLevelTag | undefined;
|
||||
const tagOrMember: Array<PowerLevelTag | RoomMember> = [];
|
||||
members.forEach((member) => {
|
||||
const memberPL = getPowerLevel(member.userId);
|
||||
const tag = getTag(memberPL);
|
||||
if (tag !== prevTag) {
|
||||
prevTag = tag;
|
||||
tagOrMember.push(tag);
|
||||
}
|
||||
tagOrMember.push(member);
|
||||
});
|
||||
return tagOrMember;
|
||||
}, [members, getTag, getPowerLevel]);
|
||||
|
||||
return PLTagOrRoomMember;
|
||||
};
|
||||
|
||||
export const getTagIconSrc = (
|
||||
mx: MatrixClient,
|
||||
useAuthentication: boolean,
|
||||
icon: PowerLevelTagIcon
|
||||
): string | undefined =>
|
||||
icon?.key?.startsWith('mxc://')
|
||||
? mx.mxcUrlToHttp(icon.key, 96, 96, 'scale', undefined, undefined, useAuthentication) ?? '🌻'
|
||||
: icon?.key;
|
||||
|
||||
export const useAccessibleTagColors = (
|
||||
themeKind: ThemeKind,
|
||||
powerLevelTags: PowerLevelTags
|
||||
): Map<string, string> => {
|
||||
const accessibleColors: Map<string, string> = useMemo(() => {
|
||||
const colors: Map<string, string> = new Map();
|
||||
|
||||
getPowers(powerLevelTags).forEach((power) => {
|
||||
const tag = powerLevelTags[power];
|
||||
const { color } = tag;
|
||||
if (!color) return;
|
||||
|
||||
colors.set(color, accessibleColor(themeKind, color));
|
||||
});
|
||||
|
||||
return colors;
|
||||
}, [powerLevelTags, themeKind]);
|
||||
|
||||
return accessibleColors;
|
||||
export const getPowerLevelTag = (
|
||||
powerLevelTags: PowerLevelTags,
|
||||
powerLevel: number
|
||||
): MemberPowerTag => {
|
||||
const tag: MemberPowerTag | undefined = powerLevelTags[powerLevel];
|
||||
return tag ?? generateFallbackTag(powerLevelTags, powerLevel);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -58,10 +58,11 @@ const fillMissingPowers = (powerLevels: IPowerLevels): IPowerLevels =>
|
|||
});
|
||||
|
||||
const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => {
|
||||
const pl = mEvent?.getContent<IPowerLevels>();
|
||||
if (!pl) return DEFAULT_POWER_LEVELS;
|
||||
const plContent = mEvent?.getContent<IPowerLevels>();
|
||||
|
||||
return fillMissingPowers(pl);
|
||||
const powerLevels = !plContent ? DEFAULT_POWER_LEVELS : fillMissingPowers(plContent);
|
||||
|
||||
return powerLevels;
|
||||
};
|
||||
|
||||
export function usePowerLevels(room: Room): IPowerLevels {
|
||||
|
|
@ -120,33 +121,8 @@ export const useRoomsPowerLevels = (rooms: Room[]): Map<string, IPowerLevels> =>
|
|||
return roomToPowerLevels;
|
||||
};
|
||||
|
||||
export type GetPowerLevel = (powerLevels: IPowerLevels, userId: string | undefined) => number;
|
||||
export type CanSend = (
|
||||
powerLevels: IPowerLevels,
|
||||
eventType: string | undefined,
|
||||
powerLevel: number
|
||||
) => boolean;
|
||||
export type CanDoAction = (
|
||||
powerLevels: IPowerLevels,
|
||||
action: PowerLevelActions,
|
||||
powerLevel: number
|
||||
) => boolean;
|
||||
export type CanDoNotificationAction = (
|
||||
powerLevels: IPowerLevels,
|
||||
action: PowerLevelNotificationsAction,
|
||||
powerLevel: number
|
||||
) => boolean;
|
||||
|
||||
export type PowerLevelsAPI = {
|
||||
getPowerLevel: GetPowerLevel;
|
||||
canSendEvent: CanSend;
|
||||
canSendStateEvent: CanSend;
|
||||
canDoAction: CanDoAction;
|
||||
canDoNotificationAction: CanDoNotificationAction;
|
||||
};
|
||||
|
||||
export type ReadPowerLevelAPI = {
|
||||
user: GetPowerLevel;
|
||||
user: (powerLevels: IPowerLevels, userId: string | undefined) => number;
|
||||
event: (powerLevels: IPowerLevels, eventType: string | undefined) => number;
|
||||
state: (powerLevels: IPowerLevels, eventType: string | undefined) => number;
|
||||
action: (powerLevels: IPowerLevels, action: PowerLevelActions) => number;
|
||||
|
|
@ -156,6 +132,7 @@ export type ReadPowerLevelAPI = {
|
|||
export const readPowerLevel: ReadPowerLevelAPI = {
|
||||
user: (powerLevels, userId) => {
|
||||
const { users_default: usersDefault, users } = powerLevels;
|
||||
|
||||
if (userId && users && typeof users[userId] === 'number') {
|
||||
return users[userId];
|
||||
}
|
||||
|
|
@ -191,63 +168,13 @@ export const readPowerLevel: ReadPowerLevelAPI = {
|
|||
},
|
||||
};
|
||||
|
||||
export const powerLevelAPI: PowerLevelsAPI = {
|
||||
getPowerLevel: (powerLevels, userId) => readPowerLevel.user(powerLevels, userId),
|
||||
canSendEvent: (powerLevels, eventType, powerLevel) => {
|
||||
const requiredPL = readPowerLevel.event(powerLevels, eventType);
|
||||
return powerLevel >= requiredPL;
|
||||
},
|
||||
canSendStateEvent: (powerLevels, eventType, powerLevel) => {
|
||||
const requiredPL = readPowerLevel.state(powerLevels, eventType);
|
||||
return powerLevel >= requiredPL;
|
||||
},
|
||||
canDoAction: (powerLevels, action, powerLevel) => {
|
||||
const requiredPL = readPowerLevel.action(powerLevels, action);
|
||||
return powerLevel >= requiredPL;
|
||||
},
|
||||
canDoNotificationAction: (powerLevels, action, powerLevel) => {
|
||||
const requiredPL = readPowerLevel.notification(powerLevels, action);
|
||||
return powerLevel >= requiredPL;
|
||||
},
|
||||
};
|
||||
|
||||
export const usePowerLevelsAPI = (powerLevels: IPowerLevels) => {
|
||||
const getPowerLevel = useCallback(
|
||||
(userId: string | undefined) => powerLevelAPI.getPowerLevel(powerLevels, userId),
|
||||
export const useGetMemberPowerLevel = (powerLevels: IPowerLevels) => {
|
||||
const callback = useCallback(
|
||||
(userId?: string): number => readPowerLevel.user(powerLevels, userId),
|
||||
[powerLevels]
|
||||
);
|
||||
|
||||
const canSendEvent = useCallback(
|
||||
(eventType: string | undefined, powerLevel: number) =>
|
||||
powerLevelAPI.canSendEvent(powerLevels, eventType, powerLevel),
|
||||
[powerLevels]
|
||||
);
|
||||
|
||||
const canSendStateEvent = useCallback(
|
||||
(eventType: string | undefined, powerLevel: number) =>
|
||||
powerLevelAPI.canSendStateEvent(powerLevels, eventType, powerLevel),
|
||||
[powerLevels]
|
||||
);
|
||||
|
||||
const canDoAction = useCallback(
|
||||
(action: PowerLevelActions, powerLevel: number) =>
|
||||
powerLevelAPI.canDoAction(powerLevels, action, powerLevel),
|
||||
[powerLevels]
|
||||
);
|
||||
|
||||
const canDoNotificationAction = useCallback(
|
||||
(action: PowerLevelNotificationsAction, powerLevel: number) =>
|
||||
powerLevelAPI.canDoNotificationAction(powerLevels, action, powerLevel),
|
||||
[powerLevels]
|
||||
);
|
||||
|
||||
return {
|
||||
getPowerLevel,
|
||||
canSendEvent,
|
||||
canSendStateEvent,
|
||||
canDoAction,
|
||||
canDoNotificationAction,
|
||||
};
|
||||
return callback;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
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;
|
||||
};
|
||||
49
src/app/hooks/useRoomCreators.ts
Normal file
49
src/app/hooks/useRoomCreators.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
|
||||
import { useMemo } from 'react';
|
||||
import { useStateEvent } from './useStateEvent';
|
||||
import { IRoomCreateContent, StateEvent } from '../../types/matrix/room';
|
||||
import { creatorsSupported } from '../utils/matrix';
|
||||
import { getStateEvent } from '../utils/room';
|
||||
|
||||
export const getRoomCreators = (createEvent: MatrixEvent): Set<string> => {
|
||||
const createContent = createEvent.getContent<IRoomCreateContent>();
|
||||
|
||||
const creators: Set<string> = new Set();
|
||||
|
||||
if (!creatorsSupported(createContent.room_version)) return creators;
|
||||
|
||||
if (createEvent.event.sender) {
|
||||
creators.add(createEvent.event.sender);
|
||||
}
|
||||
|
||||
if ('additional_creators' in createContent && Array.isArray(createContent.additional_creators)) {
|
||||
createContent.additional_creators.forEach((creator) => {
|
||||
if (typeof creator === 'string') {
|
||||
creators.add(creator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return creators;
|
||||
};
|
||||
|
||||
export const useRoomCreators = (room: Room): Set<string> => {
|
||||
const createEvent = useStateEvent(room, StateEvent.RoomCreate);
|
||||
|
||||
const creators = useMemo(
|
||||
() => (createEvent ? getRoomCreators(createEvent) : new Set<string>()),
|
||||
[createEvent]
|
||||
);
|
||||
|
||||
return creators;
|
||||
};
|
||||
|
||||
export const getRoomCreatorsForRoomId = (mx: MatrixClient, roomId: string): Set<string> => {
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return new Set();
|
||||
|
||||
const createEvent = getStateEvent(room, StateEvent.RoomCreate);
|
||||
if (!createEvent) return new Set();
|
||||
|
||||
return getRoomCreators(createEvent);
|
||||
};
|
||||
8
src/app/hooks/useRoomCreatorsTag.ts
Normal file
8
src/app/hooks/useRoomCreatorsTag.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { MemberPowerTag } from '../../types/matrix/room';
|
||||
|
||||
const DEFAULT_TAG: MemberPowerTag = {
|
||||
name: 'Founder',
|
||||
color: '#0000ff',
|
||||
};
|
||||
|
||||
export const useRoomCreatorsTag = (): MemberPowerTag => DEFAULT_TAG;
|
||||
|
|
@ -9,10 +9,12 @@ import {
|
|||
getSpaceRoomPath,
|
||||
} from '../pages/pathUtils';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { getOrphanParents } from '../utils/room';
|
||||
import { getOrphanParents, guessPerfectParent } from '../utils/room';
|
||||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||
import { mDirectAtom } from '../state/mDirectList';
|
||||
import { useSelectedSpace } from './router/useSelectedSpace';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
|
||||
export const useRoomNavigate = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -20,6 +22,7 @@ export const useRoomNavigate = () => {
|
|||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const spaceSelectedId = useSelectedSpace();
|
||||
const [developerTools] = useSetting(settingsAtom, 'developerTools');
|
||||
|
||||
const navigateSpace = useCallback(
|
||||
(roomId: string) => {
|
||||
|
|
@ -32,16 +35,23 @@ export const useRoomNavigate = () => {
|
|||
const navigateRoom = useCallback(
|
||||
(roomId: string, eventId?: string, opts?: NavigateOptions) => {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
|
||||
const openSpaceTimeline = developerTools && spaceSelectedId === roomId;
|
||||
|
||||
const orphanParents = getOrphanParents(roomToParents, roomId);
|
||||
const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId);
|
||||
if (orphanParents.length > 0) {
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
|
||||
mx,
|
||||
spaceSelectedId && orphanParents.includes(spaceSelectedId)
|
||||
? spaceSelectedId
|
||||
: orphanParents[0]
|
||||
let parentSpace: string;
|
||||
if (spaceSelectedId && orphanParents.includes(spaceSelectedId)) {
|
||||
parentSpace = spaceSelectedId;
|
||||
} else {
|
||||
parentSpace = guessPerfectParent(mx, roomId, orphanParents) ?? orphanParents[0];
|
||||
}
|
||||
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
|
||||
|
||||
navigate(
|
||||
getSpaceRoomPath(pSpaceIdOrAlias, openSpaceTimeline ? roomId : roomIdOrAlias, eventId),
|
||||
opts
|
||||
);
|
||||
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +62,7 @@ export const useRoomNavigate = () => {
|
|||
|
||||
navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
|
||||
},
|
||||
[mx, navigate, spaceSelectedId, roomToParents, mDirects]
|
||||
[mx, navigate, spaceSelectedId, roomToParents, mDirects, developerTools]
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
60
src/app/hooks/useRoomPermissions.ts
Normal file
60
src/app/hooks/useRoomPermissions.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { useMemo } from 'react';
|
||||
import {
|
||||
IPowerLevels,
|
||||
PowerLevelActions,
|
||||
PowerLevelNotificationsAction,
|
||||
readPowerLevel,
|
||||
} from './usePowerLevels';
|
||||
|
||||
export type RoomPermissionsAPI = {
|
||||
event: (type: string, userId: string) => boolean;
|
||||
stateEvent: (type: string, userId: string) => boolean;
|
||||
action: (action: PowerLevelActions, userId: string) => boolean;
|
||||
notificationAction: (action: PowerLevelNotificationsAction, userId: string) => boolean;
|
||||
};
|
||||
|
||||
export const getRoomPermissionsAPI = (
|
||||
creators: Set<string>,
|
||||
powerLevels: IPowerLevels
|
||||
): RoomPermissionsAPI => {
|
||||
const api: RoomPermissionsAPI = {
|
||||
event: (type, userId) => {
|
||||
if (creators.has(userId)) return true;
|
||||
const userPower = readPowerLevel.user(powerLevels, userId);
|
||||
const requiredPL = readPowerLevel.event(powerLevels, type);
|
||||
return userPower >= requiredPL;
|
||||
},
|
||||
stateEvent: (type, userId) => {
|
||||
if (creators.has(userId)) return true;
|
||||
const userPower = readPowerLevel.user(powerLevels, userId);
|
||||
const requiredPL = readPowerLevel.state(powerLevels, type);
|
||||
return userPower >= requiredPL;
|
||||
},
|
||||
action: (action, userId) => {
|
||||
if (creators.has(userId)) return true;
|
||||
const userPower = readPowerLevel.user(powerLevels, userId);
|
||||
const requiredPL = readPowerLevel.action(powerLevels, action);
|
||||
return userPower >= requiredPL;
|
||||
},
|
||||
notificationAction: (action, userId) => {
|
||||
if (creators.has(userId)) return true;
|
||||
const userPower = readPowerLevel.user(powerLevels, userId);
|
||||
const requiredPL = readPowerLevel.notification(powerLevels, action);
|
||||
return userPower >= requiredPL;
|
||||
},
|
||||
};
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
export const useRoomPermissions = (
|
||||
creators: Set<string>,
|
||||
powerLevels: IPowerLevels
|
||||
): RoomPermissionsAPI => {
|
||||
const api: RoomPermissionsAPI = useMemo(
|
||||
() => getRoomPermissionsAPI(creators, powerLevels),
|
||||
[creators, powerLevels]
|
||||
);
|
||||
|
||||
return api;
|
||||
};
|
||||
37
src/app/hooks/useTimeoutToggle.ts
Normal file
37
src/app/hooks/useTimeoutToggle.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Temporarily sets a boolean state.
|
||||
*
|
||||
* @param duration - Duration in milliseconds before resetting (default: 1500)
|
||||
* @param initial - Initial value (default: false)
|
||||
*/
|
||||
export function useTimeoutToggle(duration = 1500, initial = false): [boolean, () => void] {
|
||||
const [active, setActive] = useState(initial);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
const clear = () => {
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const trigger = useCallback(() => {
|
||||
setActive(!initial);
|
||||
clear();
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
setActive(initial);
|
||||
timeoutRef.current = null;
|
||||
}, duration);
|
||||
}, [duration, initial]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
clear();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return [active, trigger];
|
||||
}
|
||||
58
src/app/hooks/useUserPresence.ts
Normal file
58
src/app/hooks/useUserPresence.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { User, UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export enum Presence {
|
||||
Online = 'online',
|
||||
Unavailable = 'unavailable',
|
||||
Offline = 'offline',
|
||||
}
|
||||
|
||||
export type UserPresence = {
|
||||
presence: Presence;
|
||||
status?: string;
|
||||
active: boolean;
|
||||
lastActiveTs?: number;
|
||||
};
|
||||
|
||||
const getUserPresence = (user: User): UserPresence => ({
|
||||
presence: user.presence as Presence,
|
||||
status: user.presenceStatusMsg,
|
||||
active: user.currentlyActive,
|
||||
lastActiveTs: user.getLastActiveTs(),
|
||||
});
|
||||
|
||||
export const useUserPresence = (userId: string): UserPresence | undefined => {
|
||||
const mx = useMatrixClient();
|
||||
const user = mx.getUser(userId);
|
||||
|
||||
const [presence, setPresence] = useState(() => (user ? getUserPresence(user) : undefined));
|
||||
|
||||
useEffect(() => {
|
||||
const updatePresence: UserEventHandlerMap[UserEvent.Presence] = (event, u) => {
|
||||
if (u.userId === user?.userId) {
|
||||
setPresence(getUserPresence(user));
|
||||
}
|
||||
};
|
||||
user?.on(UserEvent.Presence, updatePresence);
|
||||
user?.on(UserEvent.CurrentlyActive, updatePresence);
|
||||
user?.on(UserEvent.LastPresenceTs, updatePresence);
|
||||
return () => {
|
||||
user?.removeListener(UserEvent.Presence, updatePresence);
|
||||
user?.removeListener(UserEvent.CurrentlyActive, updatePresence);
|
||||
user?.removeListener(UserEvent.LastPresenceTs, updatePresence);
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
return presence;
|
||||
};
|
||||
|
||||
export const usePresenceLabel = (): Record<Presence, string> =>
|
||||
useMemo(
|
||||
() => ({
|
||||
[Presence.Online]: 'Active',
|
||||
[Presence.Unavailable]: 'Busy',
|
||||
[Presence.Offline]: 'Away',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue