Merge branch 'dev' into explore-persistent-server-list

This commit is contained in:
Ginger 2025-03-20 10:13:49 -04:00 committed by GitHub
commit 8c1de69277
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 7367 additions and 605 deletions

View file

@ -0,0 +1,29 @@
import { Room } from 'matrix-js-sdk';
import { useAtomValue } from 'jotai';
import { useCallback, useMemo } from 'react';
import { allRoomsAtom } from '../state/room-list/roomList';
import { useMatrixClient } from './useMatrixClient';
export const useAllJoinedRoomsSet = () => {
const allRooms = useAtomValue(allRoomsAtom);
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
return allJoinedRooms;
};
export type GetRoomCallback = (roomId: string) => Room | undefined;
export const useGetRoom = (rooms: Set<string>): GetRoomCallback => {
const mx = useMatrixClient();
const getRoom: GetRoomCallback = useCallback(
(rId: string) => {
if (rooms.has(rId)) {
return mx.getRoom(rId) ?? undefined;
}
return undefined;
},
[mx, rooms]
);
return getRoom;
};

View file

@ -0,0 +1,22 @@
import { Room } from 'matrix-js-sdk';
import { useMemo } from 'react';
import { getAllParents } from '../utils/room';
import { useMatrixClient } from './useMatrixClient';
export const useImagePackRooms = (
roomId: string,
roomToParents: Map<string, Set<string>>
): Room[] => {
const mx = useMatrixClient();
const imagePackRooms: Room[] = useMemo(() => {
const allParentSpaces = [roomId].concat(Array.from(getAllParents(roomToParents, roomId)));
return allParentSpaces.reduce<Room[]>((list, rId) => {
const r = mx.getRoom(rId);
if (r) list.push(r);
return list;
}, []);
}, [mx, roomId, roomToParents]);
return imagePackRooms;
};

View file

@ -0,0 +1,57 @@
import { useMemo } from 'react';
import { RoomMember } from 'matrix-js-sdk';
import { Membership } from '../../types/matrix/room';
export const MembershipFilter = {
filterJoined: (m: RoomMember) => m.membership === Membership.Join,
filterInvited: (m: RoomMember) => m.membership === Membership.Invite,
filterLeaved: (m: RoomMember) =>
m.membership === Membership.Leave &&
m.events.member?.getStateKey() === m.events.member?.getSender(),
filterKicked: (m: RoomMember) =>
m.membership === Membership.Leave &&
m.events.member?.getStateKey() !== m.events.member?.getSender(),
filterBanned: (m: RoomMember) => m.membership === Membership.Ban,
};
export type MembershipFilterFn = (m: RoomMember) => boolean;
export type MembershipFilterItem = {
name: string;
filterFn: MembershipFilterFn;
};
export const useMembershipFilterMenu = (): MembershipFilterItem[] =>
useMemo(
() => [
{
name: 'Joined',
filterFn: MembershipFilter.filterJoined,
},
{
name: 'Invited',
filterFn: MembershipFilter.filterInvited,
},
{
name: 'Left',
filterFn: MembershipFilter.filterLeaved,
},
{
name: 'Kicked',
filterFn: MembershipFilter.filterKicked,
},
{
name: 'Banned',
filterFn: MembershipFilter.filterBanned,
},
],
[]
);
export const useMembershipFilter = (
index: number,
membershipFilter: MembershipFilterItem[]
): MembershipFilterItem => {
const filter = membershipFilter[index] ?? membershipFilter[0];
return filter;
};

View file

@ -0,0 +1,48 @@
import { RoomMember } from 'matrix-js-sdk';
import { useMemo } from 'react';
export const MemberSort = {
Ascending: (a: RoomMember, b: RoomMember) =>
a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1,
Descending: (a: RoomMember, b: RoomMember) =>
a.name.toLowerCase() > b.name.toLowerCase() ? -1 : 1,
NewestFirst: (a: RoomMember, b: RoomMember) =>
(b.events.member?.getTs() ?? 0) - (a.events.member?.getTs() ?? 0),
Oldest: (a: RoomMember, b: RoomMember) =>
(a.events.member?.getTs() ?? 0) - (b.events.member?.getTs() ?? 0),
};
export type MemberSortFn = (a: RoomMember, b: RoomMember) => number;
export type MemberSortItem = {
name: string;
sortFn: MemberSortFn;
};
export const useMemberSortMenu = (): MemberSortItem[] =>
useMemo(
() => [
{
name: 'A to Z',
sortFn: MemberSort.Ascending,
},
{
name: 'Z to A',
sortFn: MemberSort.Descending,
},
{
name: 'Newest',
sortFn: MemberSort.NewestFirst,
},
{
name: 'Oldest',
sortFn: MemberSort.Oldest,
},
],
[]
);
export const useMemberSort = (index: number, memberSort: MemberSortItem[]): MemberSortItem => {
const item = memberSort[index] ?? memberSort[0];
return item;
};

View file

@ -7,6 +7,19 @@ export enum NotificationMode {
NotifyLoud = 'NotifyLoud',
}
export const getNotificationMode = (actions: PushRuleAction[]): NotificationMode => {
const soundTweak = actions.find(
(action) => typeof action === 'object' && action.set_tweak === TweakName.Sound
);
const notify = actions.find(
(action) => typeof action === 'string' && action === PushRuleActionName.Notify
);
if (notify && soundTweak) return NotificationMode.NotifyLoud;
if (notify) return NotificationMode.Notify;
return NotificationMode.OFF;
};
export type NotificationModeOptions = {
soundValue?: string;
highlight?: boolean;
@ -49,18 +62,7 @@ export const useNotificationModeActions = (
};
export const useNotificationActionsMode = (actions: PushRuleAction[]): NotificationMode => {
const mode: NotificationMode = useMemo(() => {
const soundTweak = actions.find(
(action) => typeof action === 'object' && action.set_tweak === TweakName.Sound
);
const notify = actions.find(
(action) => typeof action === 'string' && action === PushRuleActionName.Notify
);
if (notify && soundTweak) return NotificationMode.NotifyLoud;
if (notify) return NotificationMode.Notify;
return NotificationMode.OFF;
}, [actions]);
const mode: NotificationMode = useMemo(() => getNotificationMode(actions), [actions]);
return mode;
};

View file

@ -1,38 +1,154 @@
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
import { useCallback, useMemo } from 'react';
import { IPowerLevels } from './usePowerLevels';
import { useStateEvent } from './useStateEvent';
import { StateEvent } from '../../types/matrix/room';
import { IImageInfo } from '../../types/matrix/common';
export type PowerLevelTagIcon = {
key?: string;
info?: IImageInfo;
};
export type PowerLevelTag = {
name: string;
color?: string;
icon?: PowerLevelTagIcon;
};
export const usePowerLevelTags = () => {
const powerLevelTags = useMemo(
() => ({
9000: {
name: 'Goku',
},
101: {
name: 'Founder',
},
100: {
name: 'Admin',
},
50: {
name: 'Moderator',
},
0: {
name: 'Default',
},
}),
[]
);
return useCallback(
(powerLevel: number): PowerLevelTag => {
if (powerLevel >= 9000) return powerLevelTags[9000];
if (powerLevel >= 101) return powerLevelTags[101];
if (powerLevel === 100) return powerLevelTags[100];
if (powerLevel >= 50) return powerLevelTags[50];
return powerLevelTags[0];
export type PowerLevelTags = Record<number, PowerLevelTag>;
export const powerSortFn = (a: number, b: number) => b - a;
export 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));
return sortPowers(powers);
};
export const getUsedPowers = (powerLevels: IPowerLevels): Set<number> => {
const powers: Set<number> = new Set();
const findAndAddPower = (data: Record<string, unknown>) => {
Object.keys(data).forEach((key) => {
const powerOrAny: unknown = data[key];
if (typeof powerOrAny === 'number') {
powers.add(powerOrAny);
return;
}
if (powerOrAny && typeof powerOrAny === 'object') {
findAndAddPower(powerOrAny as Record<string, unknown>);
}
});
};
findAndAddPower(powerLevels);
return powers;
};
const DEFAULT_TAGS: PowerLevelTags = {
9001: {
name: 'Goku',
color: '#ff6a00',
},
102: {
name: 'Goku Reborn',
color: '#ff6a7f',
},
101: {
name: 'Founder',
color: '#0000ff',
},
100: {
name: 'Admin',
color: '#a000e4',
},
50: {
name: 'Moderator',
color: '#1fd81f',
},
0: {
name: 'Member',
},
[-1]: {
name: 'Muted',
},
};
const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): PowerLevelTag => {
const highToLow = sortPowers(getPowers(powerLevelTags));
const tagPower = highToLow.find((p) => p < power);
const tag = typeof tagPower === 'number' ? powerLevelTags[tagPower] : undefined;
return {
name: tag ? `${tag.name} ${power}` : `Team ${power}`,
};
};
export type GetPowerLevelTag = (powerLevel: number) => PowerLevelTag;
export const usePowerLevelTags = (
room: Room,
powerLevels: IPowerLevels
): [PowerLevelTags, GetPowerLevelTag] => {
const tagsEvent = useStateEvent(room, StateEvent.PowerLevelTags);
const powerLevelTags: PowerLevelTags = useMemo(() => {
const content = tagsEvent?.getContent<PowerLevelTags>();
const powerToTags: PowerLevelTags = { ...content };
const powers = getUsedPowers(powerLevels);
Array.from(powers).forEach((power) => {
if (powerToTags[power]?.name === undefined) {
powerToTags[power] = DEFAULT_TAGS[power] ?? generateFallbackTag(DEFAULT_TAGS, power);
}
});
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];
};
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;

View file

@ -1,26 +1,16 @@
import { Room } from 'matrix-js-sdk';
import { createContext, useCallback, useContext, useMemo } from 'react';
import { MatrixEvent, Room } from 'matrix-js-sdk';
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import produce from 'immer';
import { useStateEvent } from './useStateEvent';
import { StateEvent } from '../../types/matrix/room';
import { useForceUpdate } from './useForceUpdate';
import { useStateEventCallback } from './useStateEventCallback';
import { useMatrixClient } from './useMatrixClient';
import { getStateEvent } from '../utils/room';
export type PowerLevelActions = 'invite' | 'redact' | 'kick' | 'ban' | 'historical';
export type PowerLevelNotificationsAction = 'room';
enum DefaultPowerLevels {
usersDefault = 0,
stateDefault = 50,
eventsDefault = 0,
invite = 0,
redact = 50,
kick = 50,
ban = 50,
historical = 0,
}
export interface IPowerLevels {
export type IPowerLevels = {
users_default?: number;
state_default?: number;
events_default?: number;
@ -33,12 +23,53 @@ export interface IPowerLevels {
events?: Record<string, number>;
users?: Record<string, number>;
notifications?: Record<string, number>;
}
};
const DEFAULT_POWER_LEVELS: Required<IPowerLevels> = {
users_default: 0,
state_default: 50,
events_default: 0,
invite: 0,
redact: 50,
kick: 50,
ban: 50,
historical: 0,
events: {},
users: {},
notifications: {
room: 50,
},
};
const fillMissingPowers = (powerLevels: IPowerLevels): IPowerLevels =>
produce(powerLevels, (draftPl: IPowerLevels) => {
const keys = Object.keys(DEFAULT_POWER_LEVELS) as unknown as (keyof IPowerLevels)[];
keys.forEach((key) => {
if (draftPl[key] === undefined) {
// eslint-disable-next-line no-param-reassign
draftPl[key] = DEFAULT_POWER_LEVELS[key] as any;
}
});
if (draftPl.notifications && typeof draftPl.notifications.room !== 'number') {
// eslint-disable-next-line no-param-reassign
draftPl.notifications.room = DEFAULT_POWER_LEVELS.notifications.room;
}
return draftPl;
});
const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => {
const pl = mEvent?.getContent<IPowerLevels>();
if (!pl) return DEFAULT_POWER_LEVELS;
return fillMissingPowers(pl);
};
export function usePowerLevels(room: Room): IPowerLevels {
const powerLevelsEvent = useStateEvent(room, StateEvent.RoomPowerLevels);
const powerLevels: IPowerLevels =
powerLevelsEvent?.getContent<IPowerLevels>() ?? DefaultPowerLevels;
const powerLevels: IPowerLevels = useMemo(
() => getPowersLevelFromMatrixEvent(powerLevelsEvent),
[powerLevelsEvent]
);
return powerLevels;
}
@ -55,7 +86,18 @@ export const usePowerLevelsContext = (): IPowerLevels => {
export const useRoomsPowerLevels = (rooms: Room[]): Map<string, IPowerLevels> => {
const mx = useMatrixClient();
const [updateCount, forceUpdate] = useForceUpdate();
const getRoomsPowerLevels = useCallback(() => {
const rToPl = new Map<string, IPowerLevels>();
rooms.forEach((room) => {
const mEvent = getStateEvent(room, StateEvent.RoomPowerLevels, '');
rToPl.set(room.roomId, getPowersLevelFromMatrixEvent(mEvent));
});
return rToPl;
}, [rooms]);
const [roomToPowerLevels, setRoomToPowerLevels] = useState(() => getRoomsPowerLevels());
useStateEventCallback(
mx,
@ -68,28 +110,13 @@ export const useRoomsPowerLevels = (rooms: Room[]): Map<string, IPowerLevels> =>
event.getStateKey() === '' &&
rooms.find((r) => r.roomId === roomId)
) {
forceUpdate();
setRoomToPowerLevels(getRoomsPowerLevels());
}
},
[rooms, forceUpdate]
[rooms, getRoomsPowerLevels]
)
);
const roomToPowerLevels = useMemo(
() => {
const rToPl = new Map<string, IPowerLevels>();
rooms.forEach((room) => {
const pl = getStateEvent(room, StateEvent.RoomPowerLevels, '')?.getContent<IPowerLevels>();
if (pl) rToPl.set(room.roomId, pl);
});
return rToPl;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[rooms, updateCount]
);
return roomToPowerLevels;
};
@ -104,42 +131,83 @@ export type CanDoAction = (
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 const powerLevelAPI: PowerLevelsAPI = {
getPowerLevel: (powerLevels, userId) => {
export type ReadPowerLevelAPI = {
user: GetPowerLevel;
event: (powerLevels: IPowerLevels, eventType: string | undefined) => number;
state: (powerLevels: IPowerLevels, eventType: string | undefined) => number;
action: (powerLevels: IPowerLevels, action: PowerLevelActions) => number;
notification: (powerLevels: IPowerLevels, action: PowerLevelNotificationsAction) => number;
};
export const readPowerLevel: ReadPowerLevelAPI = {
user: (powerLevels, userId) => {
const { users_default: usersDefault, users } = powerLevels;
if (userId && users && typeof users[userId] === 'number') {
return users[userId];
}
return usersDefault ?? DefaultPowerLevels.usersDefault;
return usersDefault ?? DEFAULT_POWER_LEVELS.users_default;
},
canSendEvent: (powerLevels, eventType, powerLevel) => {
event: (powerLevels, eventType) => {
const { events, events_default: eventsDefault } = powerLevels;
if (events && eventType && typeof events[eventType] === 'number') {
return powerLevel >= events[eventType];
return events[eventType];
}
return powerLevel >= (eventsDefault ?? DefaultPowerLevels.eventsDefault);
return eventsDefault ?? DEFAULT_POWER_LEVELS.events_default;
},
canSendStateEvent: (powerLevels, eventType, powerLevel) => {
state: (powerLevels, eventType) => {
const { events, state_default: stateDefault } = powerLevels;
if (events && eventType && typeof events[eventType] === 'number') {
return powerLevel >= events[eventType];
return events[eventType];
}
return powerLevel >= (stateDefault ?? DefaultPowerLevels.stateDefault);
return stateDefault ?? DEFAULT_POWER_LEVELS.state_default;
},
action: (powerLevels, action) => {
const powerLevel = powerLevels[action];
if (typeof powerLevel === 'number') {
return powerLevel;
}
return DEFAULT_POWER_LEVELS[action];
},
notification: (powerLevels, action) => {
const powerLevel = powerLevels.notifications?.[action];
if (typeof powerLevel === 'number') {
return powerLevel;
}
return DEFAULT_POWER_LEVELS.notifications[action];
},
};
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 = powerLevels[action];
if (typeof requiredPL === 'number') {
return powerLevel >= requiredPL;
}
return powerLevel >= DefaultPowerLevels[action];
const requiredPL = readPowerLevel.action(powerLevels, action);
return powerLevel >= requiredPL;
},
canDoNotificationAction: (powerLevels, action, powerLevel) => {
const requiredPL = readPowerLevel.notification(powerLevels, action);
return powerLevel >= requiredPL;
},
};
@ -167,10 +235,121 @@ export const usePowerLevelsAPI = (powerLevels: IPowerLevels) => {
[powerLevels]
);
const canDoNotificationAction = useCallback(
(action: PowerLevelNotificationsAction, powerLevel: number) =>
powerLevelAPI.canDoNotificationAction(powerLevels, action, powerLevel),
[powerLevels]
);
return {
getPowerLevel,
canSendEvent,
canSendStateEvent,
canDoAction,
canDoNotificationAction,
};
};
/**
* Permissions
*/
type DefaultPermissionLocation = {
user: true;
key?: string;
};
type ActionPermissionLocation = {
action: true;
key: PowerLevelActions;
};
type EventPermissionLocation = {
state?: true;
key?: string;
};
type NotificationPermissionLocation = {
notification: true;
key: PowerLevelNotificationsAction;
};
export type PermissionLocation =
| DefaultPermissionLocation
| ActionPermissionLocation
| EventPermissionLocation
| NotificationPermissionLocation;
export const getPermissionPower = (
powerLevels: IPowerLevels,
location: PermissionLocation
): number => {
if ('user' in location) {
return readPowerLevel.user(powerLevels, location.key);
}
if ('action' in location) {
return readPowerLevel.action(powerLevels, location.key);
}
if ('notification' in location) {
return readPowerLevel.notification(powerLevels, location.key);
}
if ('state' in location) {
return readPowerLevel.state(powerLevels, location.key);
}
return readPowerLevel.event(powerLevels, location.key);
};
export const applyPermissionPower = (
powerLevels: IPowerLevels,
location: PermissionLocation,
power: number
): IPowerLevels => {
if ('user' in location) {
if (typeof location.key === 'string') {
const users = powerLevels.users ?? {};
users[location.key] = power;
// eslint-disable-next-line no-param-reassign
powerLevels.users = users;
return powerLevels;
}
// eslint-disable-next-line no-param-reassign
powerLevels.users_default = power;
return powerLevels;
}
if ('action' in location) {
// eslint-disable-next-line no-param-reassign
powerLevels[location.key] = power;
return powerLevels;
}
if ('notification' in location) {
const notifications = powerLevels.notifications ?? {};
notifications[location.key] = power;
// eslint-disable-next-line no-param-reassign
powerLevels.notifications = notifications;
return powerLevels;
}
if ('state' in location) {
if (typeof location.key === 'string') {
const events = powerLevels.events ?? {};
events[location.key] = power;
// eslint-disable-next-line no-param-reassign
powerLevels.events = events;
return powerLevels;
}
// eslint-disable-next-line no-param-reassign
powerLevels.state_default = power;
return powerLevels;
}
if (typeof location.key === 'string') {
const events = powerLevels.events ?? {};
events[location.key] = power;
// eslint-disable-next-line no-param-reassign
powerLevels.events = events;
return powerLevels;
}
// eslint-disable-next-line no-param-reassign
powerLevels.events_default = power;
return powerLevels;
};

View file

@ -0,0 +1,29 @@
import { Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
import { useCallback, useEffect, useState } from 'react';
export const useRoomAccountData = (room: Room): Map<string, object> => {
const getAccountData = useCallback((): Map<string, object> => {
const accountData = new Map<string, object>();
Array.from(room.accountData.entries()).forEach(([type, mEvent]) => {
const content = mEvent.getContent();
accountData.set(type, content);
});
return accountData;
}, [room]);
const [accountData, setAccountData] = useState<Map<string, object>>(getAccountData);
useEffect(() => {
const handleEvent: RoomEventHandlerMap[RoomEvent.AccountData] = () => {
setAccountData(getAccountData());
};
room.on(RoomEvent.AccountData, handleEvent);
return () => {
room.removeListener(RoomEvent.AccountData, handleEvent);
};
}, [room, getAccountData]);
return accountData;
};

View file

@ -0,0 +1,170 @@
import { useCallback, useEffect, useMemo } from 'react';
import { MatrixError, Room } from 'matrix-js-sdk';
import { RoomCanonicalAliasEventContent } from 'matrix-js-sdk/lib/types';
import { AsyncState, useAsyncCallback } from './useAsyncCallback';
import { useMatrixClient } from './useMatrixClient';
import { useAlive } from './useAlive';
import { useStateEvent } from './useStateEvent';
import { StateEvent } from '../../types/matrix/room';
import { getStateEvent } from '../utils/room';
export const usePublishedAliases = (room: Room): [string | undefined, string[]] => {
const aliasContent = useStateEvent(
room,
StateEvent.RoomCanonicalAlias
)?.getContent<RoomCanonicalAliasEventContent>();
const canonicalAlias = aliasContent?.alias;
const publishedAliases = useMemo(() => {
const aliases: string[] = [];
if (typeof aliasContent?.alias === 'string') {
aliases.push(aliasContent.alias);
}
aliasContent?.alt_aliases?.forEach((alias) => {
if (typeof alias === 'string') {
aliases.push(alias);
}
});
return aliases;
}, [aliasContent]);
return [canonicalAlias, publishedAliases];
};
export const useSetMainAlias = (room: Room): ((alias: string | undefined) => Promise<void>) => {
const mx = useMatrixClient();
const mainAlias = useCallback(
async (alias: string | undefined) => {
const content = getStateEvent(
room,
StateEvent.RoomCanonicalAlias
)?.getContent<RoomCanonicalAliasEventContent>();
const altAliases: string[] = [];
if (content?.alias && content.alias !== alias) {
altAliases.push(content.alias);
}
content?.alt_aliases?.forEach((a) => {
if (a !== alias) {
altAliases.push(a);
}
});
const newContent: RoomCanonicalAliasEventContent = {
alias,
alt_aliases: altAliases,
};
await mx.sendStateEvent(room.roomId, StateEvent.RoomCanonicalAlias as any, newContent);
},
[mx, room]
);
return mainAlias;
};
export const usePublishUnpublishAliases = (
room: Room
): {
publishAliases: (aliases: string[]) => Promise<void>;
unpublishAliases: (aliases: string[]) => Promise<void>;
} => {
const mx = useMatrixClient();
const publishAliases = useCallback(
async (aliases: string[]) => {
const content = getStateEvent(
room,
StateEvent.RoomCanonicalAlias
)?.getContent<RoomCanonicalAliasEventContent>();
const altAliases = content?.alt_aliases ?? [];
aliases.forEach((alias) => {
if (!altAliases.includes(alias)) {
altAliases.push(alias);
}
});
const newContent: RoomCanonicalAliasEventContent = {
alias: content?.alias,
alt_aliases: altAliases,
};
await mx.sendStateEvent(room.roomId, StateEvent.RoomCanonicalAlias as any, newContent);
},
[mx, room]
);
const unpublishAliases = useCallback(
async (aliases: string[]) => {
const content = getStateEvent(
room,
StateEvent.RoomCanonicalAlias
)?.getContent<RoomCanonicalAliasEventContent>();
const altAliases: string[] = [];
content?.alt_aliases?.forEach((alias) => {
if (!aliases.includes(alias)) {
altAliases.push(alias);
}
});
const newContent: RoomCanonicalAliasEventContent = {
alias: content?.alias,
alt_aliases: altAliases,
};
await mx.sendStateEvent(room.roomId, StateEvent.RoomCanonicalAlias as any, newContent);
},
[mx, room]
);
return {
publishAliases,
unpublishAliases,
};
};
export const useLocalAliases = (
roomId: string
): {
localAliasesState: AsyncState<string[], MatrixError>;
addLocalAlias: (alias: string) => Promise<void>;
removeLocalAlias: (alias: string) => Promise<void>;
} => {
const mx = useMatrixClient();
const alive = useAlive();
const [aliasesState, loadAliases] = useAsyncCallback<string[], MatrixError, []>(
useCallback(async () => {
const content = await mx.getLocalAliases(roomId);
return content.aliases;
}, [mx, roomId])
);
useEffect(() => {
loadAliases();
}, [loadAliases]);
const addLocalAlias = useCallback(
async (alias: string) => {
await mx.createAlias(alias, roomId);
if (alive()) await loadAliases();
},
[mx, roomId, loadAliases, alive]
);
const removeLocalAlias = useCallback(
async (alias: string) => {
await mx.deleteAlias(alias);
if (alive()) await loadAliases();
},
[mx, loadAliases, alive]
);
return {
localAliasesState: aliasesState,
addLocalAlias,
removeLocalAlias,
};
};

View file

@ -0,0 +1,35 @@
import { useCallback, useEffect } from 'react';
import { Visibility } from 'matrix-js-sdk';
import { useAsyncCallback } from './useAsyncCallback';
import { useMatrixClient } from './useMatrixClient';
export const useRoomDirectoryVisibility = (roomId: string) => {
const mx = useMatrixClient();
const [visibilityState, loadVisibility] = useAsyncCallback(
useCallback(async () => {
const v = await mx.getRoomDirectoryVisibility(roomId);
return v.visibility === Visibility.Public;
}, [mx, roomId])
);
useEffect(() => {
loadVisibility();
}, [loadVisibility]);
const setVisibility = useCallback(
async (visibility: boolean) => {
await mx.setRoomDirectoryVisibility(
roomId,
visibility ? Visibility.Public : Visibility.Private
);
await loadVisibility();
},
[mx, roomId, loadVisibility]
);
return {
visibilityState,
setVisibility,
};
};

View file

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
import { Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
import { StateEvent } from '../../types/matrix/room';
import { useStateEvent } from './useStateEvent';
@ -39,3 +40,9 @@ export const useRoomTopic = (room: Room): string | undefined => {
return topic;
};
export const useRoomJoinRule = (room: Room): RoomJoinRulesEventContent | undefined => {
const mEvent = useStateEvent(room, StateEvent.RoomJoinRules);
const joinRuleContent = mEvent?.getContent<RoomJoinRulesEventContent>();
return joinRuleContent;
};

View file

@ -0,0 +1,50 @@
import {
Direction,
MatrixEvent,
Room,
RoomStateEvent,
RoomStateEventHandlerMap,
} from 'matrix-js-sdk';
import { useCallback, useEffect, useState } from 'react';
import { StateEvent } from '../../types/matrix/room';
export type StateKeyToEvents = Map<string, MatrixEvent>;
export type StateTypeToState = Map<string, StateKeyToEvents>;
export const useRoomState = (room: Room): StateTypeToState => {
const getState = useCallback((): StateTypeToState => {
const roomState = room.getLiveTimeline().getState(Direction.Forward);
const state: StateTypeToState = new Map();
if (!roomState) return state;
roomState.events.forEach((stateKeyToEvents, eventType) => {
if (eventType === StateEvent.RoomMember) {
// Ignore room members from state on purpose;
return;
}
const kToE: StateKeyToEvents = new Map();
stateKeyToEvents.forEach((mEvent, stateKey) => kToE.set(stateKey, mEvent));
state.set(eventType, kToE);
});
return state;
}, [room]);
const [state, setState] = useState(getState);
useEffect(() => {
const roomState = room.getLiveTimeline().getState(Direction.Forward);
const handler: RoomStateEventHandlerMap[RoomStateEvent.Events] = () => {
setState(getState());
};
roomState?.on(RoomStateEvent.Events, handler);
return () => {
roomState?.removeListener(RoomStateEvent.Events, handler);
};
}, [room, getState]);
return state;
};

View file

@ -0,0 +1,169 @@
import { createContext, useCallback, useContext, useMemo } from 'react';
import { ConditionKind, IPushRules, MatrixClient, PushRuleKind } from 'matrix-js-sdk';
import { Icons, IconSrc } from 'folds';
import { AccountDataEvent } from '../../types/matrix/accountData';
import { useAccountData } from './useAccountData';
import { isRoomId } from '../utils/matrix';
import {
getNotificationMode,
getNotificationModeActions,
NotificationMode,
} from './useNotificationMode';
import { useAsyncCallback } from './useAsyncCallback';
import { useMatrixClient } from './useMatrixClient';
export type RoomsNotificationPreferences = {
mute: Set<string>;
specialMessages: Set<string>;
allMessages: Set<string>;
};
const RoomsNotificationPreferencesContext = createContext<RoomsNotificationPreferences | null>(
null
);
export const RoomsNotificationPreferencesProvider = RoomsNotificationPreferencesContext.Provider;
export const useRoomsNotificationPreferencesContext = (): RoomsNotificationPreferences => {
const preferences = useContext(RoomsNotificationPreferencesContext);
if (!preferences) {
throw new Error('No RoomsNotificationPreferences provided!');
}
return preferences;
};
export const useRoomsNotificationPreferences = (): RoomsNotificationPreferences => {
const pushRules = useAccountData(AccountDataEvent.PushRules)?.getContent<IPushRules>();
const preferences: RoomsNotificationPreferences = useMemo(() => {
const global = pushRules?.global;
const room = global?.room ?? [];
const override = global?.override ?? [];
const pref: RoomsNotificationPreferences = {
mute: new Set(),
specialMessages: new Set(),
allMessages: new Set(),
};
override.forEach((rule) => {
if (isRoomId(rule.rule_id) && getNotificationMode(rule.actions) === NotificationMode.OFF) {
pref.mute.add(rule.rule_id);
}
});
room.forEach((rule) => {
if (getNotificationMode(rule.actions) === NotificationMode.OFF) {
pref.specialMessages.add(rule.rule_id);
}
});
room.forEach((rule) => {
if (getNotificationMode(rule.actions) !== NotificationMode.OFF) {
pref.allMessages.add(rule.rule_id);
}
});
return pref;
}, [pushRules]);
return preferences;
};
export enum RoomNotificationMode {
Unset = 'Unset',
Mute = 'Mute',
SpecialMessages = 'SpecialMessages',
AllMessages = 'AllMessages',
}
export const getRoomNotificationMode = (
preferences: RoomsNotificationPreferences,
roomId: string
): RoomNotificationMode => {
if (preferences.mute.has(roomId)) {
return RoomNotificationMode.Mute;
}
if (preferences.specialMessages.has(roomId)) {
return RoomNotificationMode.SpecialMessages;
}
if (preferences.allMessages.has(roomId)) {
return RoomNotificationMode.AllMessages;
}
return RoomNotificationMode.Unset;
};
export const useRoomNotificationPreference = (
preferences: RoomsNotificationPreferences,
roomId: string
): RoomNotificationMode =>
useMemo(() => getRoomNotificationMode(preferences, roomId), [preferences, roomId]);
export const getRoomNotificationModeIcon = (mode?: RoomNotificationMode): IconSrc => {
if (mode === RoomNotificationMode.Mute) return Icons.BellMute;
if (mode === RoomNotificationMode.SpecialMessages) return Icons.BellPing;
if (mode === RoomNotificationMode.AllMessages) return Icons.BellRing;
return Icons.Bell;
};
export const setRoomNotificationPreference = async (
mx: MatrixClient,
roomId: string,
mode: RoomNotificationMode,
previousMode: RoomNotificationMode
): Promise<void> => {
// remove the old preference
if (
previousMode === RoomNotificationMode.AllMessages ||
previousMode === RoomNotificationMode.SpecialMessages
) {
await mx.deletePushRule('global', PushRuleKind.RoomSpecific, roomId);
}
if (previousMode === RoomNotificationMode.Mute) {
await mx.deletePushRule('global', PushRuleKind.Override, roomId);
}
// set new preference
if (mode === RoomNotificationMode.Unset) {
return;
}
if (mode === RoomNotificationMode.Mute) {
await mx.addPushRule('global', PushRuleKind.Override, roomId, {
conditions: [
{
kind: ConditionKind.EventMatch,
key: 'room_id',
pattern: roomId,
},
],
actions: getNotificationModeActions(NotificationMode.OFF),
});
return;
}
await mx.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
actions:
mode === RoomNotificationMode.AllMessages
? getNotificationModeActions(NotificationMode.NotifyLoud)
: getNotificationModeActions(NotificationMode.OFF),
});
};
export const useSetRoomNotificationPreference = (roomId: string) => {
const mx = useMatrixClient();
const [modeState, setMode] = useAsyncCallback(
useCallback(
(mode: RoomNotificationMode, previousMode: RoomNotificationMode) =>
setRoomNotificationPreference(mx, roomId, mode, previousMode),
[mx, roomId]
)
);
return {
modeState,
setMode,
};
};

View file

@ -0,0 +1,44 @@
import { useMemo, useCallback, KeyboardEventHandler, MutableRefObject } from 'react';
import { isKeyHotkey } from 'is-hotkey';
import { TextArea, Intent, TextAreaOperations, Cursor } from '../plugins/text-area';
import { useTextAreaIntentHandler } from './useTextAreaIntent';
import { GetTarget } from '../plugins/text-area/type';
export const useTextAreaCodeEditor = (
textAreaRef: MutableRefObject<HTMLTextAreaElement | null>,
intentSpaceCount: number
) => {
const getTarget: GetTarget = useCallback(() => {
const target = textAreaRef.current;
if (!target) throw new Error('TextArea element not found!');
return target;
}, [textAreaRef]);
const { textArea, operations, intent } = useMemo(() => {
const ta = new TextArea(getTarget);
const op = new TextAreaOperations(getTarget);
return {
textArea: ta,
operations: op,
intent: new Intent(intentSpaceCount, ta, op),
};
}, [getTarget, intentSpaceCount]);
const intentHandler = useTextAreaIntentHandler(textArea, operations, intent);
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (evt) => {
intentHandler(evt);
if (isKeyHotkey('escape', evt)) {
const cursor = Cursor.fromTextAreaElement(getTarget());
operations.deselect(cursor);
}
};
return {
handleKeyDown,
textArea,
intent,
getTarget,
operations,
};
};