mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-09 16:50:28 +03:00
New room settings, add customizable power levels and dev tools (#2222)
* WIP - add room settings dialog * join rule setting - WIP * show emojis & stickers in room settings - WIP * restyle join rule switcher * Merge branch 'dev' into new-room-settings * add join rule hook * open room settings from global state * open new room settings from all places * rearrange settings menu item * add option for creating new image pack * room devtools - WIP * render room state events as list * add option to open state event * add option to edit state event * refactor text area code editor into hook * add option to send message and state event * add cutout card component * add hook for room account data * display room account data - WIP * refactor global account data editor component * add account data editor in room * fix font style in devtool * show state events in compact form * add option to delete room image pack * add server badge component * add member tile component * render members in room settings * add search in room settings member * add option to reset member search * add filter in room members * fix member virtual item key * remove color from serve badge in room members * show room in settings * fix loading indicator position * power level tags in room setting - WIP * generate fallback tag in backward compatible way * add color picker * add powers editor - WIP * add props to stop adding emoji to recent usage * add beta feature notice badge * add types for power level tag icon * refactor image pack rooms code to hook * option for adding new power levels tags * remove console log * refactor power icon * add option to edit power level tags * remove power level from powers pill * fix power level labels * add option to delete power levels * fix long power level name shrinks power integer * room permissions - WIP * add power level selector component * add room permissions * move user default permission setting to other group * add power permission peek menu * fix weigh of power switch text * hide above for max power in permission switcher * improve beta badge description * render room profile in room settings * add option to edit room profile * make room topic input text area * add option to enable room encryption in room settings * add option to change message history visibility * add option to change join rule * add option for addresses in room settings * close encryption dialog after enabling
This commit is contained in:
parent
00f3df8719
commit
286983c833
73 changed files with 6196 additions and 420 deletions
29
src/app/hooks/useGetRoom.ts
Normal file
29
src/app/hooks/useGetRoom.ts
Normal 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;
|
||||
};
|
||||
22
src/app/hooks/useImagePackRooms.ts
Normal file
22
src/app/hooks/useImagePackRooms.ts
Normal 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;
|
||||
};
|
||||
57
src/app/hooks/useMemberFilter.ts
Normal file
57
src/app/hooks/useMemberFilter.ts
Normal 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;
|
||||
};
|
||||
48
src/app/hooks/useMemberSort.ts
Normal file
48
src/app/hooks/useMemberSort.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
29
src/app/hooks/useRoomAccountData.ts
Normal file
29
src/app/hooks/useRoomAccountData.ts
Normal 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;
|
||||
};
|
||||
170
src/app/hooks/useRoomAliases.ts
Normal file
170
src/app/hooks/useRoomAliases.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
50
src/app/hooks/useRoomState.ts
Normal file
50
src/app/hooks/useRoomState.ts
Normal 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;
|
||||
};
|
||||
44
src/app/hooks/useTextAreaCodeEditor.ts
Normal file
44
src/app/hooks/useTextAreaCodeEditor.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue