import React, { FormEventHandler, MouseEventHandler, useCallback, useMemo, useState } from 'react'; import { Box, Text, Chip, Icon, Icons, IconButton, Scroll, Button, Input, RectCords, PopOut, Menu, config, Spinner, toRem, TooltipProvider, Tooltip, } from 'folds'; import { HexColorPicker } from 'react-colorful'; import { useAtomValue } from 'jotai'; import { Page, PageContent, PageHeader } from '../../../components/page'; import { IPowerLevels } from '../../../hooks/usePowerLevels'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; import { getPowers, getUsedPowers, PowerLevelTags, usePowerLevelTags, } from '../../../hooks/usePowerLevelTags'; import { useRoom } from '../../../hooks/useRoom'; import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut'; import { PowerColorBadge, PowerIcon } from '../../../components/power'; import { UseStateProvider } from '../../../components/UseStateProvider'; import { EmojiBoard } from '../../../components/emoji-board'; import { useImagePackRooms } from '../../../hooks/useImagePackRooms'; import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useFilePicker } from '../../../hooks/useFilePicker'; import { CompactUploadCardRenderer } from '../../../components/upload-card'; import { createUploadAtom, UploadSuccess } from '../../../state/upload'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { MemberPowerTag, MemberPowerTagIcon, StateEvent } from '../../../../types/matrix/room'; import { useAlive } from '../../../hooks/useAlive'; import { BetaNoticeBadge } from '../../../components/BetaNoticeBadge'; import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag'; import { creatorsSupported } from '../../../utils/matrix'; type EditPowerProps = { maxPower: number; power?: number; tag?: MemberPowerTag; onSave: (power: number, tag: MemberPowerTag) => void; onClose: () => void; }; function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) { const mx = useMatrixClient(); const room = useRoom(); const roomToParents = useAtomValue(roomToParentsAtom); const useAuthentication = useMediaAuthentication(); const supportCreators = creatorsSupported(room.getVersion()); const imagePackRooms = useImagePackRooms(room.roomId, roomToParents); const [iconFile, setIconFile] = useState(); const pickFile = useFilePicker(setIconFile, false); const [tagColor, setTagColor] = useState(tag?.color); const [tagIcon, setTagIcon] = useState(tag?.icon); const uploadingIcon = iconFile && !tagIcon; const tagIconSrc = tagIcon && getPowerTagIconSrc(mx, useAuthentication, tagIcon); const iconUploadAtom = useMemo(() => { if (iconFile) return createUploadAtom(iconFile); return undefined; }, [iconFile]); const handleRemoveIconUpload = useCallback(() => { setIconFile(undefined); }, []); const handleIconUploaded = useCallback((upload: UploadSuccess) => { setTagIcon({ key: upload.mxc, }); setIconFile(undefined); }, []); const handleSubmit: FormEventHandler = (evt) => { evt.preventDefault(); if (uploadingIcon) return; const target = evt.target as HTMLFormElement | undefined; const powerInput = target?.powerInput as HTMLInputElement | undefined; const nameInput = target?.nameInput as HTMLInputElement | undefined; if (!powerInput || !nameInput) return; const tagPower = parseInt(powerInput.value, 10); if (Number.isNaN(tagPower)) return; const tagName = nameInput.value.trim(); if (!tagName) return; const editedTag: MemberPowerTag = { name: tagName, color: tagColor, icon: tagIcon, }; onSave(power ?? tagPower, editedTag); onClose(); }; return ( Color } onRemove={() => setTagColor(undefined)} > {(openPicker, opened) => ( )} Name Power Icon {iconUploadAtom && !tagIconSrc ? ( ) : ( {tagIconSrc ? ( <> ) : ( <> {(cords: RectCords | undefined, setCords) => ( { setTagIcon({ key }); setCords(undefined); }} onCustomEmojiSelect={(mxc) => { setTagIcon({ key: mxc }); setCords(undefined); }} requestClose={() => { setCords(undefined); }} /> } > )} )} )} ); } type PowersEditorProps = { powerLevels: IPowerLevels; requestClose: () => void; }; export function PowersEditor({ powerLevels, requestClose }: PowersEditorProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const room = useRoom(); const alive = useAlive(); const [usedPowers, maxPower] = useMemo(() => { const up = getUsedPowers(powerLevels); return [up, Math.max(...Array.from(up))]; }, [powerLevels]); const powerLevelTags = usePowerLevelTags(room, powerLevels); const [editedPowerTags, setEditedPowerTags] = useState(); const [deleted, setDeleted] = useState>(new Set()); const [createTag, setCreateTag] = useState(false); const handleToggleDelete = useCallback((power: number) => { setDeleted((powers) => { const newIds = new Set(powers); if (newIds.has(power)) { newIds.delete(power); } else { newIds.add(power); } return newIds; }); }, []); const handleSaveTag = useCallback( (power: number, tag: MemberPowerTag) => { setEditedPowerTags((tags) => { const editedTags = { ...(tags ?? powerLevelTags) }; editedTags[power] = tag; return editedTags; }); }, [powerLevelTags] ); const [applyState, applyChanges] = useAsyncCallback( useCallback(async () => { const content: PowerLevelTags = { ...(editedPowerTags ?? powerLevelTags) }; deleted.forEach((power) => { delete content[power]; }); await mx.sendStateEvent(room.roomId, StateEvent.PowerLevelTags as any, content); }, [mx, room, powerLevelTags, editedPowerTags, deleted]) ); const resetChanges = useCallback(() => { setEditedPowerTags(undefined); setDeleted(new Set()); }, []); const handleApplyChanges = () => { applyChanges().then(() => { if (alive()) { resetChanges(); } }); }; const applyingChanges = applyState.status === AsyncStatus.Loading; const hasChanges = editedPowerTags || deleted.size > 0; const powerTags = editedPowerTags ?? powerLevelTags; return ( } > Permissions Power Levels setCreateTag(true)} variant="Secondary" fill="Soft" size="300" radii="300" outlined disabled={applyingChanges} > Create ) } /> {createTag && ( setCreateTag(false)} /> )} {getPowers(powerTags).map((power) => { const tag = powerTags[power]; const tagIconSrc = tag.icon && getPowerTagIconSrc(mx, useAuthentication, tag.icon); return ( {(edit, setEdit) => edit ? ( setEdit(false)} /> ) : ( } title={ {deleted.has(power) ? {tag.name} : tag.name} {tagIconSrc && } ({power}) } after={ deleted.has(power) ? ( handleToggleDelete(power)} > Undo ) : ( {usedPowers.has(power) ? ( Used Power Level You have to remove its use before you can delete it. ) : ( Delete )} } > {(triggerRef) => ( handleToggleDelete(power) } > )} setEdit(true)} > Edit ) } /> ) } ); })} {hasChanges && ( {applyState.status === AsyncStatus.Error ? ( Failed to apply changes! Please try again. ) : ( Changes saved! Apply when ready. )} )} ); }