mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-10 01:00:28 +03:00
* 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
355 lines
10 KiB
TypeScript
355 lines
10 KiB
TypeScript
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 { useStateEventCallback } from './useStateEventCallback';
|
|
import { useMatrixClient } from './useMatrixClient';
|
|
import { getStateEvent } from '../utils/room';
|
|
|
|
export type PowerLevelActions = 'invite' | 'redact' | 'kick' | 'ban' | 'historical';
|
|
export type PowerLevelNotificationsAction = 'room';
|
|
|
|
export type IPowerLevels = {
|
|
users_default?: number;
|
|
state_default?: number;
|
|
events_default?: number;
|
|
historical?: number;
|
|
invite?: number;
|
|
redact?: number;
|
|
kick?: number;
|
|
ban?: number;
|
|
|
|
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 = useMemo(
|
|
() => getPowersLevelFromMatrixEvent(powerLevelsEvent),
|
|
[powerLevelsEvent]
|
|
);
|
|
|
|
return powerLevels;
|
|
}
|
|
|
|
export const PowerLevelsContext = createContext<IPowerLevels | null>(null);
|
|
|
|
export const PowerLevelsContextProvider = PowerLevelsContext.Provider;
|
|
|
|
export const usePowerLevelsContext = (): IPowerLevels => {
|
|
const pl = useContext(PowerLevelsContext);
|
|
if (!pl) throw new Error('PowerLevelContext is not initialized!');
|
|
return pl;
|
|
};
|
|
|
|
export const useRoomsPowerLevels = (rooms: Room[]): Map<string, IPowerLevels> => {
|
|
const mx = useMatrixClient();
|
|
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,
|
|
useCallback(
|
|
(event) => {
|
|
const roomId = event.getRoomId();
|
|
if (
|
|
roomId &&
|
|
event.getType() === StateEvent.RoomPowerLevels &&
|
|
event.getStateKey() === '' &&
|
|
rooms.find((r) => r.roomId === roomId)
|
|
) {
|
|
setRoomToPowerLevels(getRoomsPowerLevels());
|
|
}
|
|
},
|
|
[rooms, getRoomsPowerLevels]
|
|
)
|
|
);
|
|
|
|
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;
|
|
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 ?? DEFAULT_POWER_LEVELS.users_default;
|
|
},
|
|
event: (powerLevels, eventType) => {
|
|
const { events, events_default: eventsDefault } = powerLevels;
|
|
if (events && eventType && typeof events[eventType] === 'number') {
|
|
return events[eventType];
|
|
}
|
|
return eventsDefault ?? DEFAULT_POWER_LEVELS.events_default;
|
|
},
|
|
state: (powerLevels, eventType) => {
|
|
const { events, state_default: stateDefault } = powerLevels;
|
|
if (events && eventType && typeof events[eventType] === 'number') {
|
|
return events[eventType];
|
|
}
|
|
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 = 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),
|
|
[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,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
};
|