diff --git a/package-lock.json b/package-lock.json index 70826ae..3fc2901 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cinny", - "version": "4.9.1", + "version": "4.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cinny", - "version": "4.9.1", + "version": "4.8.1", "license": "AGPL-3.0-only", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "1.1.6", diff --git a/package.json b/package.json index f1816cd..81d0e20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "4.9.1", + "version": "4.8.1", "description": "Yet another matrix client", "main": "index.js", "type": "module", diff --git a/src/app/components/UserRoomProfileRenderer.tsx b/src/app/components/UserRoomProfileRenderer.tsx deleted file mode 100644 index ca7aa83..0000000 --- a/src/app/components/UserRoomProfileRenderer.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { Menu, PopOut, toRem } from 'folds'; -import FocusTrap from 'focus-trap-react'; -import { useCloseUserRoomProfile, useUserRoomProfileState } from '../state/hooks/userRoomProfile'; -import { UserRoomProfile } from './user-profile'; -import { UserRoomProfileState } from '../state/userRoomProfile'; -import { useAllJoinedRoomsSet, useGetRoom } from '../hooks/useGetRoom'; -import { stopPropagation } from '../utils/keyboard'; -import { SpaceProvider } from '../hooks/useSpace'; -import { RoomProvider } from '../hooks/useRoom'; - -function UserRoomProfileContextMenu({ state }: { state: UserRoomProfileState }) { - const { roomId, spaceId, userId, cords, position } = state; - const allJoinedRooms = useAllJoinedRoomsSet(); - const getRoom = useGetRoom(allJoinedRooms); - const room = getRoom(roomId); - const space = spaceId ? getRoom(spaceId) : undefined; - - const close = useCloseUserRoomProfile(); - - if (!room) return null; - - return ( - - - - - - - - - - } - /> - ); -} - -export function UserRoomProfileRenderer() { - const state = useUserRoomProfileState(); - - if (!state) return null; - return ; -} diff --git a/src/app/components/create-room/AdditionalCreatorInput.tsx b/src/app/components/create-room/AdditionalCreatorInput.tsx deleted file mode 100644 index 936b9b9..0000000 --- a/src/app/components/create-room/AdditionalCreatorInput.tsx +++ /dev/null @@ -1,294 +0,0 @@ -import { - Box, - Button, - Chip, - config, - Icon, - Icons, - Input, - Line, - Menu, - MenuItem, - PopOut, - RectCords, - Scroll, - Text, - toRem, -} from 'folds'; -import { isKeyHotkey } from 'is-hotkey'; -import FocusTrap from 'focus-trap-react'; -import React, { - ChangeEventHandler, - KeyboardEventHandler, - MouseEventHandler, - useMemo, - useState, -} from 'react'; -import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; -import { useDirectUsers } from '../../hooks/useDirectUsers'; -import { SettingTile } from '../setting-tile'; -import { useMatrixClient } from '../../hooks/useMatrixClient'; -import { stopPropagation } from '../../utils/keyboard'; -import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; -import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser'; - -export const useAdditionalCreators = (defaultCreators?: string[]) => { - const mx = useMatrixClient(); - const [additionalCreators, setAdditionalCreators] = useState( - () => defaultCreators?.filter((id) => id !== mx.getSafeUserId()) ?? [] - ); - - const addAdditionalCreator = (userId: string) => { - if (userId === mx.getSafeUserId()) return; - - setAdditionalCreators((creators) => { - const creatorsSet = new Set(creators); - creatorsSet.add(userId); - return Array.from(creatorsSet); - }); - }; - - const removeAdditionalCreator = (userId: string) => { - setAdditionalCreators((creators) => { - const creatorsSet = new Set(creators); - creatorsSet.delete(userId); - return Array.from(creatorsSet); - }); - }; - - return { - additionalCreators, - addAdditionalCreator, - removeAdditionalCreator, - }; -}; - -const SEARCH_OPTIONS: UseAsyncSearchOptions = { - limit: 1000, - matchOptions: { - contain: true, - }, -}; -const getUserIdString = (userId: string) => getMxIdLocalPart(userId) ?? userId; - -type AdditionalCreatorInputProps = { - additionalCreators: string[]; - onSelect: (userId: string) => void; - onRemove: (userId: string) => void; - disabled?: boolean; -}; -export function AdditionalCreatorInput({ - additionalCreators, - onSelect, - onRemove, - disabled, -}: AdditionalCreatorInputProps) { - const mx = useMatrixClient(); - const [menuCords, setMenuCords] = useState(); - const directUsers = useDirectUsers(); - - const [validUserId, setValidUserId] = useState(); - const filteredUsers = useMemo( - () => directUsers.filter((userId) => !additionalCreators.includes(userId)), - [directUsers, additionalCreators] - ); - const [result, search, resetSearch] = useAsyncSearch( - filteredUsers, - getUserIdString, - SEARCH_OPTIONS - ); - const queryHighlighRegex = result?.query ? makeHighlightRegex([result.query]) : undefined; - - const suggestionUsers = result - ? result.items - : filteredUsers.sort((a, b) => (a.toLocaleLowerCase() >= b.toLocaleLowerCase() ? 1 : -1)); - - const handleOpenMenu: MouseEventHandler = (evt) => { - setMenuCords(evt.currentTarget.getBoundingClientRect()); - }; - const handleCloseMenu = () => { - setMenuCords(undefined); - setValidUserId(undefined); - resetSearch(); - }; - - const handleCreatorChange: ChangeEventHandler = (evt) => { - const creatorInput = evt.currentTarget; - const creator = creatorInput.value.trim(); - if (isUserId(creator)) { - setValidUserId(creator); - } else { - setValidUserId(undefined); - const term = - getMxIdLocalPart(creator) ?? (creator.startsWith('@') ? creator.slice(1) : creator); - if (term) { - search(term); - } else { - resetSearch(); - } - } - }; - - const handleSelectUserId = (userId?: string) => { - if (userId && isUserId(userId)) { - onSelect(userId); - handleCloseMenu(); - } - }; - - const handleCreatorKeyDown: KeyboardEventHandler = (evt) => { - if (isKeyHotkey('enter', evt)) { - evt.preventDefault(); - const creator = evt.currentTarget.value.trim(); - handleSelectUserId(isUserId(creator) ? creator : suggestionUsers[0]); - } - }; - - const handleEnterClick = () => { - handleSelectUserId(validUserId); - }; - - return ( - - - - - {mx.getSafeUserId()} - - {additionalCreators.map((creator) => ( - } - onClick={() => onRemove(creator)} - disabled={disabled} - > - {creator} - - ))} - evt.key === 'ArrowDown', - isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', - escapeDeactivates: stopPropagation, - }} - > - - - - - - - - - - - {!validUserId && suggestionUsers.length > 0 ? ( - - - {suggestionUsers.map((userId) => ( - handleSelectUserId(userId)} - after={ - - {getMxIdServer(userId)} - - } - > - - - - {queryHighlighRegex - ? highlightText(queryHighlighRegex, [ - getMxIdLocalPart(userId) ?? userId, - ]) - : getMxIdLocalPart(userId)} - - - - - ))} - - - ) : ( - - - No Suggestions - - - Please provide the user ID and hit Enter. - - - )} - - - - - } - > - - - - - - - - ); -} diff --git a/src/app/components/create-room/RoomVersionSelector.tsx b/src/app/components/create-room/RoomVersionSelector.tsx index 219ded0..281f520 100644 --- a/src/app/components/create-room/RoomVersionSelector.tsx +++ b/src/app/components/create-room/RoomVersionSelector.tsx @@ -47,7 +47,7 @@ export function RoomVersionSelector({ gap="500" > { const content: Record = {}; if (typeof type === 'string') { @@ -24,9 +23,6 @@ export const createRoomCreationContent = ( if (allowFederation === false) { content['m.federate'] = false; } - if (Array.isArray(additionalCreators)) { - content.additional_creators = additionalCreators; - } return content; }; @@ -93,7 +89,6 @@ export type CreateRoomData = { encryption?: boolean; knock: boolean; allowFederation: boolean; - additionalCreators?: string[]; }; export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise => { const initialState: ICreateRoomStateEvent[] = []; @@ -113,11 +108,7 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis name: data.name, topic: data.topic, room_alias_name: data.aliasLocalPart, - creation_content: createRoomCreationContent( - data.type, - data.allowFederation, - data.additionalCreators - ), + creation_content: createRoomCreationContent(data.type, data.allowFederation), initial_state: initialState, }; diff --git a/src/app/components/event-readers/EventReaders.tsx b/src/app/components/event-readers/EventReaders.tsx index c790023..de1416b 100644 --- a/src/app/components/event-readers/EventReaders.tsx +++ b/src/app/components/event-readers/EventReaders.tsx @@ -19,11 +19,9 @@ import { getMemberDisplayName } from '../../utils/room'; import { getMxIdLocalPart } from '../../utils/matrix'; import * as css from './EventReaders.css'; import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { openProfileViewer } from '../../../client/action/navigation'; import { UserAvatar } from '../user-avatar'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; -import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile'; -import { useSpaceOptionally } from '../../hooks/useSpace'; -import { getMouseEventCords } from '../../utils/dom'; export type EventReadersProps = { room: Room; @@ -35,8 +33,6 @@ export const EventReaders = as<'div', EventReadersProps>( const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const latestEventReaders = useRoomEventReaders(room, eventId); - const openProfile = useOpenUserRoomProfile(); - const space = useSpaceOptionally(); const getName = (userId: string) => getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId; @@ -61,32 +57,19 @@ export const EventReaders = as<'div', EventReadersProps>( {latestEventReaders.map((readerId) => { const name = getName(readerId); - const avatarMxcUrl = room.getMember(readerId)?.getMxcAvatarUrl(); - const avatarUrl = avatarMxcUrl - ? mx.mxcUrlToHttp( - avatarMxcUrl, - 100, - 100, - 'crop', - undefined, - false, - useAuthentication - ) - : undefined; + const avatarMxcUrl = room + .getMember(readerId) + ?.getMxcAvatarUrl(); + const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(avatarMxcUrl, 100, 100, 'crop', undefined, false, useAuthentication) : undefined; return ( { - openProfile( - room.roomId, - space?.roomId, - readerId, - getMouseEventCords(event.nativeEvent), - 'Bottom' - ); + onClick={() => { + requestClose(); + openProfileViewer(readerId, room.roomId); }} before={ diff --git a/src/app/components/image-pack-view/RoomImagePack.tsx b/src/app/components/image-pack-view/RoomImagePack.tsx index 92b4ff2..9dd45c1 100644 --- a/src/app/components/image-pack-view/RoomImagePack.tsx +++ b/src/app/components/image-pack-view/RoomImagePack.tsx @@ -1,14 +1,12 @@ import React, { useCallback, useMemo } from 'react'; import { Room } from 'matrix-js-sdk'; -import { usePowerLevels } from '../../hooks/usePowerLevels'; +import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { ImagePackContent } from './ImagePackContent'; import { ImagePack, PackContent } from '../../plugins/custom-emoji'; import { StateEvent } from '../../../types/matrix/room'; import { useRoomImagePack } from '../../hooks/useImagePacks'; import { randomStr } from '../../utils/common'; -import { useRoomPermissions } from '../../hooks/useRoomPermissions'; -import { useRoomCreators } from '../../hooks/useRoomCreators'; type RoomImagePackProps = { room: Room; @@ -19,10 +17,9 @@ export function RoomImagePack({ room, stateKey }: RoomImagePackProps) { const mx = useMatrixClient(); const userId = mx.getUserId()!; const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - const permissions = useRoomPermissions(creators, powerLevels); - const canEditImagePack = permissions.stateEvent(StateEvent.PoniesRoomEmotes, userId); + const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); + const canEditImagePack = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(userId)); const fallbackPack = useMemo(() => { const fakePackId = randomStr(4); diff --git a/src/app/components/join-address-prompt/JoinAddressPrompt.tsx b/src/app/components/join-address-prompt/JoinAddressPrompt.tsx deleted file mode 100644 index 50a8941..0000000 --- a/src/app/components/join-address-prompt/JoinAddressPrompt.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import React, { FormEventHandler, useState } from 'react'; -import FocusTrap from 'focus-trap-react'; -import { - Dialog, - Overlay, - OverlayCenter, - OverlayBackdrop, - Header, - config, - Box, - Text, - IconButton, - Icon, - Icons, - Button, - Input, - color, -} from 'folds'; -import { stopPropagation } from '../../utils/keyboard'; -import { isRoomAlias, isRoomId } from '../../utils/matrix'; -import { parseMatrixToRoom, parseMatrixToRoomEvent, testMatrixTo } from '../../plugins/matrix-to'; -import { tryDecodeURIComponent } from '../../utils/dom'; - -type JoinAddressProps = { - onOpen: (roomIdOrAlias: string, via?: string[], eventId?: string) => void; - onCancel: () => void; -}; -export function JoinAddressPrompt({ onOpen, onCancel }: JoinAddressProps) { - const [invalid, setInvalid] = useState(false); - - const handleSubmit: FormEventHandler = (evt) => { - evt.preventDefault(); - setInvalid(false); - - const target = evt.target as HTMLFormElement | undefined; - const addressInput = target?.addressInput as HTMLInputElement | undefined; - const address = addressInput?.value.trim(); - if (!address) return; - - if (isRoomId(address) || isRoomAlias(address)) { - onOpen(address); - return; - } - - if (testMatrixTo(address)) { - const decodedAddress = tryDecodeURIComponent(address); - const toRoom = parseMatrixToRoom(decodedAddress); - if (toRoom) { - onOpen(toRoom.roomIdOrAlias, toRoom.viaServers); - return; - } - - const toEvent = parseMatrixToRoomEvent(decodedAddress); - if (toEvent) { - onOpen(toEvent.roomIdOrAlias, toEvent.viaServers, toEvent.eventId); - return; - } - } - - setInvalid(true); - }; - - return ( - }> - - - -
- - Join with Address - - - - -
- - - - Enter public address to join the community. Addresses looks like: - - -
  • #community:server
  • -
  • https://matrix.to/#/#community:server
  • -
  • https://matrix.to/#/!xYzAj?via=server
  • -
    -
    - - Address - - {invalid && ( - - Invalid Address - - )} - - -
    -
    -
    -
    -
    - ); -} diff --git a/src/app/components/join-address-prompt/index.ts b/src/app/components/join-address-prompt/index.ts deleted file mode 100644 index b14b8a6..0000000 --- a/src/app/components/join-address-prompt/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './JoinAddressPrompt'; diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 57bf2af..dc92bf8 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -10,8 +10,8 @@ import * as css from './Reply.css'; import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content'; import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser'; import { useRoomEvent } from '../../hooks/useRoomEvent'; +import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags'; import colorMXID from '../../../util/colorMXID'; -import { GetMemberPowerTag } from '../../hooks/useMemberPowerTag'; type ReplyLayoutProps = { userColor?: string; @@ -57,7 +57,8 @@ type ReplyProps = { replyEventId: string; threadRootId?: string | undefined; onClick?: MouseEventHandler | undefined; - getMemberPowerTag?: GetMemberPowerTag; + getPowerLevel?: (userId: string) => number; + getPowerLevelTag?: GetPowerLevelTag; accessibleTagColors?: Map; legacyUsernameColor?: boolean; }; @@ -70,7 +71,8 @@ export const Reply = as<'div', ReplyProps>( replyEventId, threadRootId, onClick, - getMemberPowerTag, + getPowerLevel, + getPowerLevelTag, accessibleTagColors, legacyUsernameColor, ...props @@ -86,7 +88,8 @@ export const Reply = as<'div', ReplyProps>( const { body } = replyEvent?.getContent() ?? {}; const sender = replyEvent?.getSender(); - const powerTag = sender ? getMemberPowerTag?.(sender) : undefined; + const senderPL = sender && getPowerLevel?.(sender); + const powerTag = typeof senderPL === 'number' ? getPowerLevelTag?.(senderPL) : undefined; const tagColor = powerTag?.color ? accessibleTagColors?.get(powerTag.color) : undefined; const usernameColor = legacyUsernameColor ? colorMXID(sender ?? replyEventId) : tagColor; diff --git a/src/app/components/message/layout/layout.css.ts b/src/app/components/message/layout/layout.css.ts index 43949ce..a9b3f35 100644 --- a/src/app/components/message/layout/layout.css.ts +++ b/src/app/components/message/layout/layout.css.ts @@ -124,7 +124,7 @@ export const AvatarBase = style({ selectors: { '&:hover': { - transform: `translateY(${toRem(-2)})`, + transform: `translateY(${toRem(-4)})`, }, }, }); diff --git a/src/app/components/presence/Presence.tsx b/src/app/components/presence/Presence.tsx deleted file mode 100644 index 108852f..0000000 --- a/src/app/components/presence/Presence.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { - as, - Badge, - Box, - color, - ContainerColor, - MainColor, - Text, - Tooltip, - TooltipProvider, - toRem, -} from 'folds'; -import React, { ReactNode, useId } from 'react'; -import * as css from './styles.css'; -import { Presence, usePresenceLabel } from '../../hooks/useUserPresence'; - -const PresenceToColor: Record = { - [Presence.Online]: 'Success', - [Presence.Unavailable]: 'Warning', - [Presence.Offline]: 'Secondary', -}; - -type PresenceBadgeProps = { - presence: Presence; - status?: string; - size?: '200' | '300' | '400' | '500'; -}; -export function PresenceBadge({ presence, status, size }: PresenceBadgeProps) { - const label = usePresenceLabel(); - const badgeLabelId = useId(); - - return ( - - - {label[presence]} - {status && } - {status && {status}} - - - } - > - {(triggerRef) => ( - - )} - - ); -} - -type AvatarPresenceProps = { - badge: ReactNode; - variant?: ContainerColor; -}; -export const AvatarPresence = as<'div', AvatarPresenceProps>( - ({ as: AsAvatarPresence, badge, variant = 'Surface', children, ...props }, ref) => ( - - {badge && ( -
    - {badge} -
    - )} - {children} -
    - ) -); diff --git a/src/app/components/presence/index.ts b/src/app/components/presence/index.ts deleted file mode 100644 index 88fcdf7..0000000 --- a/src/app/components/presence/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Presence'; diff --git a/src/app/components/presence/styles.css.ts b/src/app/components/presence/styles.css.ts deleted file mode 100644 index 12ea7f1..0000000 --- a/src/app/components/presence/styles.css.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { config } from 'folds'; - -export const AvatarPresence = style({ - display: 'flex', - position: 'relative', - flexShrink: 0, -}); - -export const AvatarPresenceBadge = style({ - position: 'absolute', - bottom: 0, - right: 0, - transform: 'translate(25%, 25%)', - zIndex: 1, - - display: 'flex', - padding: config.borderWidth.B600, - backgroundColor: 'inherit', - borderRadius: config.radii.Pill, - overflow: 'hidden', -}); diff --git a/src/app/components/room-intro/RoomIntro.tsx b/src/app/components/room-intro/RoomIntro.tsx index c388efd..b02d9f5 100644 --- a/src/app/components/room-intro/RoomIntro.tsx +++ b/src/app/components/room-intro/RoomIntro.tsx @@ -87,7 +87,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) => {typeof prevRoomId === 'string' && (mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? ( -
    - - - - - - ); -} - -type SharedPowerAlertProps = { - power: number; - onCancel: () => void; - onChange: (power: number) => void; -}; -function SharedPowerAlert({ power, onCancel, onChange }: SharedPowerAlertProps) { - return ( - }> - - - -
    - - Shared Power - - - - -
    - - - - You are promoting the user to have the same power as yourself! You will not be - able to change their power afterward. Are you sure? - - - - - - -
    -
    -
    -
    - ); -} - -export function PowerChip({ userId }: { userId: string }) { - const mx = useMatrixClient(); - const room = useRoom(); - const space = useSpaceOptionally(); - const useAuthentication = useMediaAuthentication(); - const openRoomSettings = useOpenRoomSettings(); - const openSpaceSettings = useOpenSpaceSettings(); - - const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - const getMemberPowerLevel = useGetMemberPowerLevel(powerLevels); - const { hasMorePower } = useMemberPowerCompare(creators, powerLevels); - - const powerLevelTags = usePowerLevelTags(room, powerLevels); - const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels); - - const myUserId = mx.getSafeUserId(); - const canChangePowers = - permissions.stateEvent(StateEvent.RoomPowerLevels, myUserId) && - (myUserId === userId ? true : hasMorePower(myUserId, userId)); - - const tag = getMemberPowerTag(userId); - const tagIconSrc = tag.icon && getPowerTagIconSrc(mx, useAuthentication, tag.icon); - - const [cords, setCords] = useState(); - - const open: MouseEventHandler = (evt) => { - setCords(evt.currentTarget.getBoundingClientRect()); - }; - - const close = () => setCords(undefined); - - const [powerState, changePower] = useAsyncCallback( - useCallback( - async (power: number) => { - await mx.setPowerLevel(room.roomId, userId, power); - }, - [mx, userId, room] - ) - ); - const changing = powerState.status === AsyncStatus.Loading; - const error = powerState.status === AsyncStatus.Error; - const [selfDemote, setSelfDemote] = useState(); - const [sharedPower, setSharedPower] = useState(); - - const handlePowerSelect = (power: number): void => { - close(); - if (!canChangePowers) return; - if (power === getMemberPowerLevel(userId)) return; - - if (userId === mx.getSafeUserId()) { - setSelfDemote(power); - return; - } - if (!creators.has(myUserId) && power === getMemberPowerLevel(myUserId)) { - setSharedPower(power); - return; - } - - changePower(power); - }; - - const handleSelfDemote = (power: number) => { - setSelfDemote(undefined); - changePower(power); - }; - const handleSharedPower = (power: number) => { - setSharedPower(undefined); - changePower(power); - }; - - return ( - <> - isKeyHotkey('arrowdown', evt), - isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), - }} - > - - - {error && ( - - Error: {powerState.error.name} - - {powerState.error.message} - - - )} - {getPowers(powerLevelTags).map((power) => { - const powerTag = powerLevelTags[power]; - const powerTagIconSrc = - powerTag.icon && getPowerTagIconSrc(mx, useAuthentication, powerTag.icon); - - const selected = getMemberPowerLevel(userId) === power; - const canAssignPower = creators.has(myUserId) - ? true - : power <= getMemberPowerLevel(myUserId); - - return ( - } - after={ - powerTagIconSrc ? ( - - ) : undefined - } - onClick={ - canChangePowers && canAssignPower - ? () => handlePowerSelect(power) - : undefined - } - > - {powerTag.name} - - ); - })} - - -
    - { - if (room.isSpaceRoom()) { - openSpaceSettings( - room.roomId, - space?.roomId, - SpaceSettingsPage.PermissionsPage - ); - } else { - openRoomSettings( - room.roomId, - space?.roomId, - RoomSettingsPage.PermissionsPage - ); - } - close(); - }} - > - Manage Powers - -
    -
    - - } - > - - ) : ( - <> - {!changing && } - {changing && } - - ) - } - after={tagIconSrc ? : undefined} - onClick={open} - aria-pressed={!!cords} - > - - {tag.name} - - -
    - {typeof selfDemote === 'number' ? ( - setSelfDemote(undefined)} - onChange={handleSelfDemote} - /> - ) : null} - {typeof sharedPower === 'number' ? ( - setSharedPower(undefined)} - onChange={handleSharedPower} - /> - ) : null} - - ); -} diff --git a/src/app/components/user-profile/UserChips.tsx b/src/app/components/user-profile/UserChips.tsx deleted file mode 100644 index 53e6618..0000000 --- a/src/app/components/user-profile/UserChips.tsx +++ /dev/null @@ -1,514 +0,0 @@ -import React, { MouseEventHandler, useCallback, useMemo, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import FocusTrap from 'focus-trap-react'; -import { isKeyHotkey } from 'is-hotkey'; -import { Room } from 'matrix-js-sdk'; -import { - PopOut, - Menu, - MenuItem, - config, - Text, - Line, - Chip, - Icon, - Icons, - RectCords, - Spinner, - toRem, - Box, - Scroll, - Avatar, -} from 'folds'; -import { useMatrixClient } from '../../hooks/useMatrixClient'; -import { getMxIdServer } from '../../utils/matrix'; -import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile'; -import { stopPropagation } from '../../utils/keyboard'; -import { copyToClipboard } from '../../utils/dom'; -import { getExploreServerPath } from '../../pages/pathUtils'; -import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; -import { factoryRoomIdByAtoZ } from '../../utils/sort'; -import { useMutualRooms, useMutualRoomsSupport } from '../../hooks/useMutualRooms'; -import { useRoomNavigate } from '../../hooks/useRoomNavigate'; -import { useDirectRooms } from '../../pages/client/direct/useDirectRooms'; -import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; -import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom'; -import { RoomAvatar, RoomIcon } from '../room-avatar'; -import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room'; -import { nameInitials } from '../../utils/common'; -import { getMatrixToUser } from '../../plugins/matrix-to'; -import { useTimeoutToggle } from '../../hooks/useTimeoutToggle'; -import { useIgnoredUsers } from '../../hooks/useIgnoredUsers'; -import { CutoutCard } from '../cutout-card'; -import { SettingTile } from '../setting-tile'; - -export function ServerChip({ server }: { server: string }) { - const mx = useMatrixClient(); - const myServer = getMxIdServer(mx.getSafeUserId()); - const navigate = useNavigate(); - const closeProfile = useCloseUserRoomProfile(); - const [copied, setCopied] = useTimeoutToggle(); - - const [cords, setCords] = useState(); - - const open: MouseEventHandler = (evt) => { - setCords(evt.currentTarget.getBoundingClientRect()); - }; - - const close = () => setCords(undefined); - - return ( - isKeyHotkey('arrowdown', evt), - isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), - }} - > - -
    - { - copyToClipboard(server); - setCopied(); - close(); - }} - > - Copy Server - - { - navigate(getExploreServerPath(server)); - closeProfile(); - }} - > - Explore Community - -
    - -
    - { - window.open(`https://${server}`, '_blank'); - close(); - }} - > - Open in Browser - -
    -
    - - } - > - - ) : ( - - ) - } - onClick={open} - aria-pressed={!!cords} - > - - {server} - - -
    - ); -} - -export function ShareChip({ userId }: { userId: string }) { - const [cords, setCords] = useState(); - - const [copied, setCopied] = useTimeoutToggle(); - - const open: MouseEventHandler = (evt) => { - setCords(evt.currentTarget.getBoundingClientRect()); - }; - - const close = () => setCords(undefined); - - return ( - isKeyHotkey('arrowdown', evt), - isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), - }} - > - -
    - { - copyToClipboard(userId); - setCopied(); - close(); - }} - > - Copy User ID - - { - copyToClipboard(getMatrixToUser(userId)); - setCopied(); - close(); - }} - > - Copy User Link - -
    -
    - - } - > - - ) : ( - - ) - } - onClick={open} - aria-pressed={!!cords} - > - - Share - - -
    - ); -} - -type MutualRoomsData = { - rooms: Room[]; - spaces: Room[]; - directs: Room[]; -}; - -export function MutualRoomsChip({ userId }: { userId: string }) { - const mx = useMatrixClient(); - const mutualRoomSupported = useMutualRoomsSupport(); - const mutualRoomsState = useMutualRooms(userId); - const { navigateRoom, navigateSpace } = useRoomNavigate(); - const closeUserRoomProfile = useCloseUserRoomProfile(); - const directs = useDirectRooms(); - const useAuthentication = useMediaAuthentication(); - - const allJoinedRooms = useAllJoinedRoomsSet(); - const getRoom = useGetRoom(allJoinedRooms); - - const [cords, setCords] = useState(); - - const open: MouseEventHandler = (evt) => { - setCords(evt.currentTarget.getBoundingClientRect()); - }; - - const close = () => setCords(undefined); - - const mutual: MutualRoomsData = useMemo(() => { - const data: MutualRoomsData = { - rooms: [], - spaces: [], - directs: [], - }; - - if (mutualRoomsState.status === AsyncStatus.Success) { - const mutualRooms = mutualRoomsState.data - .sort(factoryRoomIdByAtoZ(mx)) - .map(getRoom) - .filter((room) => !!room); - mutualRooms.forEach((room) => { - if (room.isSpaceRoom()) { - data.spaces.push(room); - return; - } - if (directs.includes(room.roomId)) { - data.directs.push(room); - return; - } - data.rooms.push(room); - }); - } - return data; - }, [mutualRoomsState, getRoom, directs, mx]); - - if ( - userId === mx.getSafeUserId() || - !mutualRoomSupported || - mutualRoomsState.status === AsyncStatus.Error - ) { - return null; - } - - const renderItem = (room: Room) => { - const { roomId } = room; - const dm = directs.includes(roomId); - - return ( - { - if (room.isSpaceRoom()) { - navigateSpace(roomId); - } else { - navigateRoom(roomId); - } - closeUserRoomProfile(); - }} - before={ - - {dm || room.isSpaceRoom() ? ( - ( - - {nameInitials(room.name)} - - )} - /> - ) : ( - - )} - - } - > - - {room.name} - - - ); - }; - - return ( - isKeyHotkey('arrowdown', evt), - isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), - }} - > - - - - - {mutual.spaces.length > 0 && ( - - - Spaces - - {mutual.spaces.map(renderItem)} - - )} - {mutual.rooms.length > 0 && ( - - - Rooms - - {mutual.rooms.map(renderItem)} - - )} - {mutual.directs.length > 0 && ( - - - Direct Messages - - {mutual.directs.map(renderItem)} - - )} - - - - - - ) : null - } - > - } - disabled={ - mutualRoomsState.status !== AsyncStatus.Success || mutualRoomsState.data.length === 0 - } - onClick={open} - aria-pressed={!!cords} - > - - {mutualRoomsState.status === AsyncStatus.Success && - `${mutualRoomsState.data.length} Mutual Rooms`} - {mutualRoomsState.status === AsyncStatus.Loading && 'Mutual Rooms'} - - - - ); -} - -export function IgnoredUserAlert() { - return ( - - - - - Blocked User - - - You do not receive any messages or invites from this user. - - - - - ); -} - -export function OptionsChip({ userId }: { userId: string }) { - const mx = useMatrixClient(); - const [cords, setCords] = useState(); - - const open: MouseEventHandler = (evt) => { - setCords(evt.currentTarget.getBoundingClientRect()); - }; - - const close = () => setCords(undefined); - - const ignoredUsers = useIgnoredUsers(); - const ignored = ignoredUsers.includes(userId); - - const [ignoreState, toggleIgnore] = useAsyncCallback( - useCallback(async () => { - const users = ignoredUsers.filter((u) => u !== userId); - if (!ignored) users.push(userId); - await mx.setIgnoredUsers(users); - }, [mx, ignoredUsers, userId, ignored]) - ); - const ignoring = ignoreState.status === AsyncStatus.Loading; - - return ( - isKeyHotkey('arrowdown', evt), - isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), - }} - > - -
    - { - toggleIgnore(); - close(); - }} - before={ - ignoring ? ( - - ) : ( - - ) - } - disabled={ignoring} - > - {ignored ? 'Unblock User' : 'Block User'} - -
    -
    - - } - > - - {ignoring ? ( - - ) : ( - - )} - -
    - ); -} diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx deleted file mode 100644 index cf4c815..0000000 --- a/src/app/components/user-profile/UserHero.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import { Avatar, Box, Icon, Icons, Text } from 'folds'; -import classNames from 'classnames'; -import * as css from './styles.css'; -import { UserAvatar } from '../user-avatar'; -import colorMXID from '../../../util/colorMXID'; -import { getMxIdLocalPart } from '../../utils/matrix'; -import { BreakWord, LineClamp3 } from '../../styles/Text.css'; -import { UserPresence } from '../../hooks/useUserPresence'; -import { AvatarPresence, PresenceBadge } from '../presence'; - -type UserHeroProps = { - userId: string; - avatarUrl?: string; - presence?: UserPresence; -}; -export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) { - return ( - -
    - {avatarUrl && {userId}} -
    -
    - - } - > - - } - /> - - -
    -
    - ); -} - -type UserHeroNameProps = { - displayName?: string; - userId: string; -}; -export function UserHeroName({ displayName, userId }: UserHeroNameProps) { - const username = getMxIdLocalPart(userId); - - return ( - - - - {displayName ?? username ?? userId} - - - - - @{username} - - - - ); -} diff --git a/src/app/components/user-profile/UserModeration.tsx b/src/app/components/user-profile/UserModeration.tsx deleted file mode 100644 index 814bb5b..0000000 --- a/src/app/components/user-profile/UserModeration.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import { Box, Button, color, config, Icon, Icons, Spinner, Text, Input } from 'folds'; -import React, { useCallback, useRef } from 'react'; -import { useRoom } from '../../hooks/useRoom'; -import { CutoutCard } from '../cutout-card'; -import { SettingTile } from '../setting-tile'; -import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; -import { useMatrixClient } from '../../hooks/useMatrixClient'; -import { BreakWord } from '../../styles/Text.css'; -import { useSetting } from '../../state/hooks/settings'; -import { settingsAtom } from '../../state/settings'; -import { timeDayMonYear, timeHourMinute } from '../../utils/time'; - -type UserKickAlertProps = { - reason?: string; - kickedBy?: string; - ts?: number; -}; -export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) { - const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); - const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); - - const time = ts ? timeHourMinute(ts, hour24Clock) : undefined; - const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined; - - return ( - - - - - Kicked User - {time && date && ( - - {date} {time} - - )} - - - {kickedBy && ( - - Kicked by: {kickedBy} - - )} - - {reason ? ( - <> - Reason: {reason} - - ) : ( - No Reason Provided. - )} - - - - - - ); -} - -type UserBanAlertProps = { - userId: string; - reason?: string; - canUnban?: boolean; - bannedBy?: string; - ts?: number; -}; -export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBanAlertProps) { - const mx = useMatrixClient(); - const room = useRoom(); - const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); - const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); - - const time = ts ? timeHourMinute(ts, hour24Clock) : undefined; - const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined; - - const [unbanState, unban] = useAsyncCallback( - useCallback(async () => { - await mx.unban(room.roomId, userId); - }, [mx, room, userId]) - ); - const banning = unbanState.status === AsyncStatus.Loading; - const error = unbanState.status === AsyncStatus.Error; - - return ( - - - - - Banned User - {time && date && ( - - {date} {time} - - )} - - - {bannedBy && ( - - Banned by: {bannedBy} - - )} - - {reason ? ( - <> - Reason: {reason} - - ) : ( - No Reason Provided. - )} - - - {error && ( - - {unbanState.error.message} - - )} - {canUnban && ( - - )} - - - - ); -} - -type UserInviteAlertProps = { - userId: string; - reason?: string; - canKick?: boolean; - invitedBy?: string; - ts?: number; -}; -export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: UserInviteAlertProps) { - const mx = useMatrixClient(); - const room = useRoom(); - const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); - const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); - - const time = ts ? timeHourMinute(ts, hour24Clock) : undefined; - const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined; - - const [kickState, kick] = useAsyncCallback( - useCallback(async () => { - await mx.kick(room.roomId, userId); - }, [mx, room, userId]) - ); - const kicking = kickState.status === AsyncStatus.Loading; - const error = kickState.status === AsyncStatus.Error; - - return ( - - - - - Invited User - {time && date && ( - - {date} {time} - - )} - - - {invitedBy && ( - - Invited by: {invitedBy} - - )} - - {reason ? ( - <> - Reason: {reason} - - ) : ( - No Reason Provided. - )} - - - {error && ( - - {kickState.error.message} - - )} - {canKick && ( - - )} - - - - ); -} - -type UserModerationProps = { - userId: string; - canKick: boolean; - canBan: boolean; - canInvite: boolean; -}; -export function UserModeration({ userId, canKick, canBan, canInvite }: UserModerationProps) { - const mx = useMatrixClient(); - const room = useRoom(); - const reasonInputRef = useRef(null); - - const getReason = useCallback((): string | undefined => { - const reason = reasonInputRef.current?.value.trim() || undefined; - if (reasonInputRef.current) { - reasonInputRef.current.value = ''; - } - return reason; - }, []); - - const [kickState, kick] = useAsyncCallback( - useCallback(async () => { - await mx.kick(room.roomId, userId, getReason()); - }, [mx, room, userId, getReason]) - ); - - const [banState, ban] = useAsyncCallback( - useCallback(async () => { - await mx.ban(room.roomId, userId, getReason()); - }, [mx, room, userId, getReason]) - ); - - const [inviteState, invite] = useAsyncCallback( - useCallback(async () => { - await mx.invite(room.roomId, userId, getReason()); - }, [mx, room, userId, getReason]) - ); - - const disabled = - kickState.status === AsyncStatus.Loading || - banState.status === AsyncStatus.Loading || - inviteState.status === AsyncStatus.Loading; - - if (!canBan && !canKick && !canInvite) return null; - - return ( - - - - Moderation - - {kickState.status === AsyncStatus.Error && ( - - {kickState.error.message} - - )} - {banState.status === AsyncStatus.Error && ( - - {banState.error.message} - - )} - {inviteState.status === AsyncStatus.Error && ( - - {inviteState.error.message} - - )} - - - {canInvite && ( - - )} - {canKick && ( - - )} - {canBan && ( - - )} - - - - ); -} diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx deleted file mode 100644 index e92d909..0000000 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { Box, Button, color, config, Icon, Icons, Spinner, Text } from 'folds'; -import React, { useCallback } from 'react'; -import { UserHero, UserHeroName } from './UserHero'; -import { getDMRoomFor, getMxIdServer, mxcUrlToHttp } from '../../utils/matrix'; -import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room'; -import { useMatrixClient } from '../../hooks/useMatrixClient'; -import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; -import { usePowerLevels } from '../../hooks/usePowerLevels'; -import { useRoom } from '../../hooks/useRoom'; -import { useUserPresence } from '../../hooks/useUserPresence'; -import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips'; -import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; -import { createDM } from '../../../client/action/room'; -import { hasDevices } from '../../../util/matrixUtil'; -import { useRoomNavigate } from '../../hooks/useRoomNavigate'; -import { useAlive } from '../../hooks/useAlive'; -import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile'; -import { PowerChip } from './PowerChip'; -import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; -import { useIgnoredUsers } from '../../hooks/useIgnoredUsers'; -import { useMembership } from '../../hooks/useMembership'; -import { Membership } from '../../../types/matrix/room'; -import { useRoomCreators } from '../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../hooks/useRoomPermissions'; -import { useMemberPowerCompare } from '../../hooks/useMemberPowerCompare'; -import { CreatorChip } from './CreatorChip'; - -type UserRoomProfileProps = { - userId: string; -}; -export function UserRoomProfile({ userId }: UserRoomProfileProps) { - const mx = useMatrixClient(); - const useAuthentication = useMediaAuthentication(); - const { navigateRoom } = useRoomNavigate(); - const alive = useAlive(); - const closeUserRoomProfile = useCloseUserRoomProfile(); - const ignoredUsers = useIgnoredUsers(); - const ignored = ignoredUsers.includes(userId); - - const room = useRoom(); - const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - const { hasMorePower } = useMemberPowerCompare(creators, powerLevels); - - const myUserId = mx.getSafeUserId(); - const creator = creators.has(userId); - - const canKickUser = permissions.action('kick', myUserId) && hasMorePower(myUserId, userId); - const canBanUser = permissions.action('ban', myUserId) && hasMorePower(myUserId, userId); - const canUnban = permissions.action('ban', myUserId); - const canInvite = permissions.action('invite', myUserId); - - const member = room.getMember(userId); - const membership = useMembership(room, userId); - - const server = getMxIdServer(userId); - const displayName = getMemberDisplayName(room, userId); - const avatarMxc = getMemberAvatarMxc(room, userId); - const avatarUrl = (avatarMxc && mxcUrlToHttp(mx, avatarMxc, useAuthentication)) ?? undefined; - - const presence = useUserPresence(userId); - - const [directMessageState, directMessage] = useAsyncCallback( - useCallback(async () => { - const result = await createDM(mx, userId, await hasDevices(mx, userId)); - return result.room_id as string; - }, [userId, mx]) - ); - - const handleMessage = () => { - const dmRoomId = getDMRoomFor(mx, userId)?.roomId; - if (dmRoomId) { - navigateRoom(dmRoomId); - closeUserRoomProfile(); - return; - } - directMessage().then((rId) => { - if (alive()) { - navigateRoom(rId); - closeUserRoomProfile(); - } - }); - }; - - return ( - - - - - - - - - - - {directMessageState.status === AsyncStatus.Error && ( - - {directMessageState.error.message} - - )} - - {server && } - - {creator ? : } - {userId !== myUserId && } - {userId !== myUserId && } - - - {ignored && } - {member && membership === Membership.Ban && ( - - )} - {member && - membership === Membership.Leave && - member.events.member && - member.events.member.getSender() !== userId && ( - - )} - {member && membership === Membership.Invite && ( - - )} - - - - ); -} diff --git a/src/app/components/user-profile/index.ts b/src/app/components/user-profile/index.ts deleted file mode 100644 index 54542c7..0000000 --- a/src/app/components/user-profile/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './UserRoomProfile'; diff --git a/src/app/components/user-profile/styles.css.ts b/src/app/components/user-profile/styles.css.ts deleted file mode 100644 index ad6d5a9..0000000 --- a/src/app/components/user-profile/styles.css.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { color, config, toRem } from 'folds'; - -export const UserHeader = style({ - position: 'absolute', - top: 0, - left: 0, - right: 0, - zIndex: 1, - padding: config.space.S200, -}); - -export const UserHero = style({ - position: 'relative', -}); - -export const UserHeroCoverContainer = style({ - height: toRem(96), - overflow: 'hidden', -}); -export const UserHeroCover = style({ - height: '100%', - width: '100%', - objectFit: 'cover', - filter: 'blur(16px)', - transform: 'scale(2)', -}); - -export const UserHeroAvatarContainer = style({ - position: 'relative', - height: toRem(29), -}); -export const UserAvatarContainer = style({ - position: 'absolute', - left: config.space.S400, - top: 0, - transform: 'translateY(-50%)', - backgroundColor: color.Surface.Container, -}); -export const UserHeroAvatar = style({ - outline: `${config.borderWidth.B600} solid ${color.Surface.Container}`, -}); diff --git a/src/app/features/add-existing/AddExisting.tsx b/src/app/features/add-existing/AddExisting.tsx deleted file mode 100644 index cbae018..0000000 --- a/src/app/features/add-existing/AddExisting.tsx +++ /dev/null @@ -1,375 +0,0 @@ -import FocusTrap from 'focus-trap-react'; -import { - Avatar, - Box, - Button, - config, - Header, - Icon, - IconButton, - Icons, - Input, - Menu, - MenuItem, - Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, - Scroll, - Spinner, - Text, -} from 'folds'; -import React, { - ChangeEventHandler, - MouseEventHandler, - useCallback, - useMemo, - useRef, - useState, -} from 'react'; -import { useAtomValue } from 'jotai'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import { Room } from 'matrix-js-sdk'; -import { stopPropagation } from '../../utils/keyboard'; -import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList'; -import { useMatrixClient } from '../../hooks/useMatrixClient'; -import { allRoomsAtom } from '../../state/room-list/roomList'; -import { mDirectAtom } from '../../state/mDirectList'; -import { roomToParentsAtom } from '../../state/room/roomToParents'; -import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom'; -import { VirtualTile } from '../../components/virtualizer'; -import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room'; -import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; -import { nameInitials } from '../../utils/common'; -import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; -import { factoryRoomIdByAtoZ } from '../../utils/sort'; -import { - SearchItemStrGetter, - useAsyncSearch, - UseAsyncSearchOptions, -} from '../../hooks/useAsyncSearch'; -import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser'; -import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; -import { StateEvent } from '../../../types/matrix/room'; -import { getViaServers } from '../../plugins/via-servers'; -import { rateLimitedActions } from '../../utils/matrix'; -import { useAlive } from '../../hooks/useAlive'; - -const SEARCH_OPTS: UseAsyncSearchOptions = { - limit: 500, - matchOptions: { - contain: true, - }, - normalizeOptions: { - ignoreWhitespace: false, - }, -}; - -type AddExistingModalProps = { - parentId: string; - space?: boolean; - requestClose: () => void; -}; -export function AddExistingModal({ parentId, space, requestClose }: AddExistingModalProps) { - const mx = useMatrixClient(); - const useAuthentication = useMediaAuthentication(); - const alive = useAlive(); - - const mDirects = useAtomValue(mDirectAtom); - const spaces = useSpaces(mx, allRoomsAtom); - const rooms = useRooms(mx, allRoomsAtom, mDirects); - const directs = useDirects(mx, allRoomsAtom, mDirects); - const roomIdToParents = useAtomValue(roomToParentsAtom); - const scrollRef = useRef(null); - - const [selected, setSelected] = useState([]); - - const allRoomsSet = useAllJoinedRoomsSet(); - const getRoom = useGetRoom(allRoomsSet); - - const allItems: string[] = useMemo(() => { - const rIds = space ? [...spaces] : [...rooms, ...directs]; - - return rIds - .filter((rId) => rId !== parentId && !roomIdToParents.get(rId)?.has(parentId)) - .sort(factoryRoomIdByAtoZ(mx)); - }, [spaces, rooms, directs, space, parentId, roomIdToParents, mx]); - - const getRoomNameStr: SearchItemStrGetter = useCallback( - (rId) => getRoom(rId)?.name ?? rId, - [getRoom] - ); - - const [searchResult, searchRoom, resetSearch] = useAsyncSearch( - allItems, - getRoomNameStr, - SEARCH_OPTS - ); - const queryHighlighRegex = searchResult?.query - ? makeHighlightRegex(searchResult.query.split(' ')) - : undefined; - - const items = searchResult ? searchResult.items : allItems; - - const virtualizer = useVirtualizer({ - count: items.length, - getScrollElement: () => scrollRef.current, - estimateSize: () => 32, - overscan: 5, - }); - const vItems = virtualizer.getVirtualItems(); - - const handleSearchChange: ChangeEventHandler = (evt) => { - const value = evt.currentTarget.value.trim(); - if (!value) { - resetSearch(); - return; - } - searchRoom(value); - }; - - const [applyState, applyChanges] = useAsyncCallback( - useCallback( - async (selectedRooms) => { - await rateLimitedActions(selectedRooms, async (room) => { - const via = getViaServers(room); - - await mx.sendStateEvent( - parentId, - StateEvent.SpaceChild as any, - { - auto_join: false, - suggested: false, - via, - }, - room.roomId - ); - }); - }, - [mx, parentId] - ) - ); - const applyingChanges = applyState.status === AsyncStatus.Loading; - - const handleRoomClick: MouseEventHandler = (evt) => { - const roomId = evt.currentTarget.getAttribute('data-room-id'); - if (!roomId) return; - if (selected?.includes(roomId)) { - setSelected(selected?.filter((rId) => rId !== roomId)); - return; - } - const addedRooms = [...(selected ?? [])]; - addedRooms.push(roomId); - setSelected(addedRooms); - }; - - const handleApplyChanges = () => { - const selectedRooms = selected.map((rId) => getRoom(rId)).filter((room) => room !== undefined); - applyChanges(selectedRooms).then(() => { - if (alive()) { - setSelected([]); - requestClose(); - } - }); - }; - - const resetChanges = () => { - setSelected([]); - }; - - return ( - }> - - - - -
    - - Add Existing - - - - - - -
    - - - - - } - placeholder="Search" - size="400" - variant="Background" - outlined - /> - - {vItems.length === 0 && ( - - - {searchResult ? 'No Match Found' : `No ${space ? 'Spaces' : 'Rooms'}`} - - - {searchResult - ? `No match found for "${searchResult.query}".` - : `You do not have any ${space ? 'Spaces' : 'Rooms'} to display yet.`} - - - )} - - {vItems.map((vItem) => { - const roomId = items[vItem.index]; - const room = getRoom(roomId); - if (!room) return null; - const selectedItem = selected?.includes(roomId); - const dm = mDirects.has(room.roomId); - - return ( - - - {dm || room.isSpaceRoom() ? ( - ( - - {nameInitials(room.name)} - - )} - /> - ) : ( - - )} - - } - after={selectedItem && } - > - - - {queryHighlighRegex - ? highlightText(queryHighlighRegex, [room.name]) - : room.name} - - - - - ); - })} - - {selected.length > 0 && ( - - - - {applyState.status === AsyncStatus.Error ? ( - - Failed to apply changes! Please try again. - - ) : ( - - Apply when ready. ({selected.length} Selected) - - )} - - - - - - - - )} - - - -
    -
    -
    -
    -
    - ); -} diff --git a/src/app/features/add-existing/index.ts b/src/app/features/add-existing/index.ts deleted file mode 100644 index 3607c06..0000000 --- a/src/app/features/add-existing/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AddExisting'; diff --git a/src/app/features/common-settings/developer-tools/StateEventEditor.tsx b/src/app/features/common-settings/developer-tools/StateEventEditor.tsx index 0ea9690..6ee19be 100644 --- a/src/app/features/common-settings/developer-tools/StateEventEditor.tsx +++ b/src/app/features/common-settings/developer-tools/StateEventEditor.tsx @@ -27,10 +27,8 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { syntaxErrorPosition } from '../../../utils/dom'; import { SettingTile } from '../../../components/setting-tile'; import { SequenceCardStyle } from '../styles.css'; -import { usePowerLevels } from '../../../hooks/usePowerLevels'; +import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { useTextAreaCodeEditor } from '../../../hooks/useTextAreaCodeEditor'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; const EDITOR_INTENT_SPACE_COUNT = 2; @@ -246,10 +244,8 @@ export function StateEventEditor({ type, stateKey, requestClose }: StateEventEdi const stateEvent = useStateEvent(room, type as unknown as StateEvent, stateKey); const [editContent, setEditContent] = useState(); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - const canEdit = permissions.stateEvent(type, mx.getSafeUserId()); + const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); + const canEdit = canSendStateEvent(type, getPowerLevel(mx.getSafeUserId())); const eventJSONStr = useMemo(() => { if (!stateEvent) return ''; diff --git a/src/app/features/common-settings/emojis-stickers/RoomPacks.tsx b/src/app/features/common-settings/emojis-stickers/RoomPacks.tsx index fdbe546..56dda54 100644 --- a/src/app/features/common-settings/emojis-stickers/RoomPacks.tsx +++ b/src/app/features/common-settings/emojis-stickers/RoomPacks.tsx @@ -33,13 +33,11 @@ import { SequenceCardStyle } from '../styles.css'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { mxcUrlToHttp } from '../../../utils/matrix'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; -import { usePowerLevels } from '../../../hooks/usePowerLevels'; +import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { StateEvent } from '../../../../types/matrix/room'; import { suffixRename } from '../../../utils/common'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useAlive } from '../../../hooks/useAlive'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; type CreatePackTileProps = { packs: ImagePack[]; @@ -148,10 +146,8 @@ export function RoomPacks({ onViewPack }: RoomPacksProps) { const alive = useAlive(); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - const canEdit = permissions.stateEvent(StateEvent.PoniesRoomEmotes, mx.getSafeUserId()); + const { canSendStateEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels); + const canEdit = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(mx.getSafeUserId())); const unfilteredPacks = useRoomImagePacks(room); const packs = useMemo(() => unfilteredPacks.filter((pack) => !pack.deleted), [unfilteredPacks]); diff --git a/src/app/features/common-settings/general/RoomAddress.tsx b/src/app/features/common-settings/general/RoomAddress.tsx index 400e73a..9e1f1a9 100644 --- a/src/app/features/common-settings/general/RoomAddress.tsx +++ b/src/app/features/common-settings/general/RoomAddress.tsx @@ -15,6 +15,7 @@ import { toRem, } from 'folds'; import { MatrixError } from 'matrix-js-sdk'; +import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels'; import { SettingTile } from '../../../components/setting-tile'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../../room-settings/styles.css'; @@ -32,19 +33,19 @@ import { getIdServer } from '../../../../util/matrixUtil'; import { replaceSpaceWithDash } from '../../../utils/common'; import { useAlive } from '../../../hooks/useAlive'; import { StateEvent } from '../../../../types/matrix/room'; -import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions'; type RoomPublishedAddressesProps = { - permissions: RoomPermissionsAPI; + powerLevels: IPowerLevels; }; -export function RoomPublishedAddresses({ permissions }: RoomPublishedAddressesProps) { +export function RoomPublishedAddresses({ powerLevels }: RoomPublishedAddressesProps) { const mx = useMatrixClient(); const room = useRoom(); - - const canEditCanonical = permissions.stateEvent( + const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId()); + const canEditCanonical = powerLevelAPI.canSendStateEvent( + powerLevels, StateEvent.RoomCanonicalAlias, - mx.getSafeUserId() + userPowerLevel ); const [canonicalAlias, publishedAliases] = usePublishedAliases(room); @@ -359,13 +360,14 @@ function LocalAddressesList({ ); } -export function RoomLocalAddresses({ permissions }: { permissions: RoomPermissionsAPI }) { +export function RoomLocalAddresses({ powerLevels }: { powerLevels: IPowerLevels }) { const mx = useMatrixClient(); const room = useRoom(); - - const canEditCanonical = permissions.stateEvent( + const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId()); + const canEditCanonical = powerLevelAPI.canSendStateEvent( + powerLevels, StateEvent.RoomCanonicalAlias, - mx.getSafeUserId() + userPowerLevel ); const [expand, setExpand] = useState(false); diff --git a/src/app/features/common-settings/general/RoomEncryption.tsx b/src/app/features/common-settings/general/RoomEncryption.tsx index 15b1f15..1bb7339 100644 --- a/src/app/features/common-settings/general/RoomEncryption.tsx +++ b/src/app/features/common-settings/general/RoomEncryption.tsx @@ -21,24 +21,28 @@ import FocusTrap from 'focus-trap-react'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SettingTile } from '../../../components/setting-tile'; +import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { StateEvent } from '../../../../types/matrix/room'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useRoom } from '../../../hooks/useRoom'; import { useStateEvent } from '../../../hooks/useStateEvent'; import { stopPropagation } from '../../../utils/keyboard'; -import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions'; const ROOM_ENC_ALGO = 'm.megolm.v1.aes-sha2'; type RoomEncryptionProps = { - permissions: RoomPermissionsAPI; + powerLevels: IPowerLevels; }; -export function RoomEncryption({ permissions }: RoomEncryptionProps) { +export function RoomEncryption({ powerLevels }: RoomEncryptionProps) { const mx = useMatrixClient(); const room = useRoom(); - - const canEnable = permissions.stateEvent(StateEvent.RoomEncryption, mx.getSafeUserId()); + const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId()); + const canEnable = powerLevelAPI.canSendStateEvent( + powerLevels, + StateEvent.RoomEncryption, + userPowerLevel + ); const content = useStateEvent(room, StateEvent.RoomEncryption)?.getContent<{ algorithm: string; }>(); diff --git a/src/app/features/common-settings/general/RoomHistoryVisibility.tsx b/src/app/features/common-settings/general/RoomHistoryVisibility.tsx index 2e42785..7b329b1 100644 --- a/src/app/features/common-settings/general/RoomHistoryVisibility.tsx +++ b/src/app/features/common-settings/general/RoomHistoryVisibility.tsx @@ -18,13 +18,13 @@ import FocusTrap from 'focus-trap-react'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SettingTile } from '../../../components/setting-tile'; +import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useRoom } from '../../../hooks/useRoom'; import { StateEvent } from '../../../../types/matrix/room'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useStateEvent } from '../../../hooks/useStateEvent'; import { stopPropagation } from '../../../utils/keyboard'; -import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions'; const useVisibilityStr = () => useMemo( @@ -49,13 +49,17 @@ const useVisibilityMenu = () => ); type RoomHistoryVisibilityProps = { - permissions: RoomPermissionsAPI; + powerLevels: IPowerLevels; }; -export function RoomHistoryVisibility({ permissions }: RoomHistoryVisibilityProps) { +export function RoomHistoryVisibility({ powerLevels }: RoomHistoryVisibilityProps) { const mx = useMatrixClient(); const room = useRoom(); - - const canEdit = permissions.stateEvent(StateEvent.RoomHistoryVisibility, mx.getSafeUserId()); + const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId()); + const canEdit = powerLevelAPI.canSendStateEvent( + powerLevels, + StateEvent.RoomHistoryVisibility, + userPowerLevel + ); const visibilityEvent = useStateEvent(room, StateEvent.RoomHistoryVisibility); const historyVisibility: HistoryVisibility = diff --git a/src/app/features/common-settings/general/RoomJoinRules.tsx b/src/app/features/common-settings/general/RoomJoinRules.tsx index b9e7549..f47ff75 100644 --- a/src/app/features/common-settings/general/RoomJoinRules.tsx +++ b/src/app/features/common-settings/general/RoomJoinRules.tsx @@ -3,6 +3,7 @@ import { color, Text } from 'folds'; import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk'; import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types'; import { useAtomValue } from 'jotai'; +import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels'; import { ExtendedJoinRules, JoinRulesSwitcher, @@ -31,7 +32,6 @@ import { knockSupported, restrictedSupported, } from '../../../utils/matrix'; -import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions'; type RestrictedRoomAllowContent = { room_id: string; @@ -39,9 +39,9 @@ type RestrictedRoomAllowContent = { }; type RoomJoinRulesProps = { - permissions: RoomPermissionsAPI; + powerLevels: IPowerLevels; }; -export function RoomJoinRules({ permissions }: RoomJoinRulesProps) { +export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) { const mx = useMatrixClient(); const room = useRoom(); const allowKnockRestricted = knockRestrictedSupported(room.getVersion()); @@ -53,7 +53,12 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) { const subspacesScope = useRecursiveChildSpaceScopeFactory(mx, roomIdToParents); const subspaces = useSpaceChildren(allRoomsAtom, space?.roomId ?? '', subspacesScope); - const canEdit = permissions.stateEvent(StateEvent.RoomHistoryVisibility, mx.getSafeUserId()); + const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId()); + const canEdit = powerLevelAPI.canSendStateEvent( + powerLevels, + StateEvent.RoomHistoryVisibility, + userPowerLevel + ); const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules); const content = joinRuleEvent?.getContent(); diff --git a/src/app/features/common-settings/general/RoomProfile.tsx b/src/app/features/common-settings/general/RoomProfile.tsx index 0f515c3..a3a62e1 100644 --- a/src/app/features/common-settings/general/RoomProfile.tsx +++ b/src/app/features/common-settings/general/RoomProfile.tsx @@ -32,6 +32,7 @@ import { RoomAvatar, RoomIcon } from '../../../components/room-avatar'; import { mxcUrlToHttp } from '../../../utils/matrix'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; +import { IPowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { StateEvent } from '../../../../types/matrix/room'; import { CompactUploadCardRenderer } from '../../../components/upload-card'; import { useObjectURL } from '../../../hooks/useObjectURL'; @@ -39,7 +40,6 @@ import { createUploadAtom, UploadSuccess } from '../../../state/upload'; import { useFilePicker } from '../../../hooks/useFilePicker'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useAlive } from '../../../hooks/useAlive'; -import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions'; type RoomProfileEditProps = { canEditAvatar: boolean; @@ -261,22 +261,24 @@ export function RoomProfileEdit({ } type RoomProfileProps = { - permissions: RoomPermissionsAPI; + powerLevels: IPowerLevels; }; -export function RoomProfile({ permissions }: RoomProfileProps) { +export function RoomProfile({ powerLevels }: RoomProfileProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const room = useRoom(); const directs = useAtomValue(mDirectAtom); + const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); + const userPowerLevel = getPowerLevel(mx.getSafeUserId()); const avatar = useRoomAvatar(room, directs.has(room.roomId)); const name = useRoomName(room); const topic = useRoomTopic(room); const joinRule = useRoomJoinRule(room); - const canEditAvatar = permissions.stateEvent(StateEvent.RoomAvatar, mx.getSafeUserId()); - const canEditName = permissions.stateEvent(StateEvent.RoomName, mx.getSafeUserId()); - const canEditTopic = permissions.stateEvent(StateEvent.RoomTopic, mx.getSafeUserId()); + const canEditAvatar = canSendStateEvent(StateEvent.RoomAvatar, userPowerLevel); + const canEditName = canSendStateEvent(StateEvent.RoomName, userPowerLevel); + const canEditTopic = canSendStateEvent(StateEvent.RoomTopic, userPowerLevel); const canEdit = canEditAvatar || canEditName || canEditTopic; const avatarUrl = avatar diff --git a/src/app/features/common-settings/general/RoomPublish.tsx b/src/app/features/common-settings/general/RoomPublish.tsx index ce01421..9edfe89 100644 --- a/src/app/features/common-settings/general/RoomPublish.tsx +++ b/src/app/features/common-settings/general/RoomPublish.tsx @@ -8,22 +8,23 @@ import { SettingTile } from '../../../components/setting-tile'; import { useRoom } from '../../../hooks/useRoom'; import { useRoomDirectoryVisibility } from '../../../hooks/useRoomDirectoryVisibility'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels'; import { StateEvent } from '../../../../types/matrix/room'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useStateEvent } from '../../../hooks/useStateEvent'; import { ExtendedJoinRules } from '../../../components/JoinRulesSwitcher'; -import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions'; type RoomPublishProps = { - permissions: RoomPermissionsAPI; + powerLevels: IPowerLevels; }; -export function RoomPublish({ permissions }: RoomPublishProps) { +export function RoomPublish({ powerLevels }: RoomPublishProps) { const mx = useMatrixClient(); const room = useRoom(); - - const canEditCanonical = permissions.stateEvent( + const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId()); + const canEditCanonical = powerLevelAPI.canSendStateEvent( + powerLevels, StateEvent.RoomCanonicalAlias, - mx.getSafeUserId() + userPowerLevel ); const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules); const content = joinRuleEvent?.getContent(); diff --git a/src/app/features/common-settings/general/RoomUpgrade.tsx b/src/app/features/common-settings/general/RoomUpgrade.tsx index 45a480a..5d6bc5e 100644 --- a/src/app/features/common-settings/general/RoomUpgrade.tsx +++ b/src/app/features/common-settings/general/RoomUpgrade.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { FormEventHandler, useCallback, useState } from 'react'; import { Button, color, @@ -14,172 +14,54 @@ import { IconButton, Icon, Icons, + Input, } from 'folds'; import FocusTrap from 'focus-trap-react'; -import { MatrixError, Method } from 'matrix-js-sdk'; -import { RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types'; +import { MatrixError } from 'matrix-js-sdk'; +import { RoomCreateEventContent, RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SettingTile } from '../../../components/setting-tile'; import { useRoom } from '../../../hooks/useRoom'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; -import { IRoomCreateContent, StateEvent } from '../../../../types/matrix/room'; +import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels'; +import { StateEvent } from '../../../../types/matrix/room'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useStateEvent } from '../../../hooks/useStateEvent'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { useCapabilities } from '../../../hooks/useCapabilities'; import { stopPropagation } from '../../../utils/keyboard'; -import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions'; -import { - AdditionalCreatorInput, - RoomVersionSelector, - useAdditionalCreators, -} from '../../../components/create-room'; -import { useAlive } from '../../../hooks/useAlive'; -import { creatorsSupported } from '../../../utils/matrix'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { BreakWord } from '../../../styles/Text.css'; - -function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) { - const mx = useMatrixClient(); - const room = useRoom(); - const alive = useAlive(); - const creators = useRoomCreators(room); - - const capabilities = useCapabilities(); - const roomVersions = capabilities['m.room_versions']; - const [selectedRoomVersion, selectRoomVersion] = useState(roomVersions?.default ?? '1'); - useEffect(() => { - // capabilities load async - selectRoomVersion(roomVersions?.default ?? '1'); - }, [roomVersions?.default]); - - const allowAdditionalCreators = creatorsSupported(selectedRoomVersion); - const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } = - useAdditionalCreators(Array.from(creators)); - - const [upgradeState, upgrade] = useAsyncCallback( - useCallback( - async (version: string, newAdditionalCreators?: string[]) => { - await mx.http.authedRequest(Method.Post, `/rooms/${room.roomId}/upgrade`, undefined, { - new_version: version, - additional_creators: newAdditionalCreators, - }); - }, - [mx, room] - ) - ); - - const upgrading = upgradeState.status === AsyncStatus.Loading; - - const handleUpgradeRoom = () => { - const version = selectedRoomVersion; - - upgrade(version, allowAdditionalCreators ? additionalCreators : undefined).then(() => { - if (alive()) { - requestClose(); - } - }); - }; - - return ( - }> - - - -
    - - {room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'} - - - - -
    - - - This action is irreversible! - - - Options - - {allowAdditionalCreators && ( - - - - )} - - {upgradeState.status === AsyncStatus.Error && ( - - {(upgradeState.error as MatrixError).message} - - )} - - -
    -
    -
    -
    - ); -} type RoomUpgradeProps = { - permissions: RoomPermissionsAPI; + powerLevels: IPowerLevels; requestClose: () => void; }; -export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) { +export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) { const mx = useMatrixClient(); const room = useRoom(); const { navigateRoom, navigateSpace } = useRoomNavigate(); const createContent = useStateEvent( room, StateEvent.RoomCreate - )?.getContent(); - const roomVersion = createContent?.room_version ?? '1'; + )?.getContent(); + const roomVersion = createContent?.room_version ?? 1; const predecessorRoomId = createContent?.predecessor?.room_id; + const capabilities = useCapabilities(); + const defaultRoomVersion = capabilities['m.room_versions']?.default; + const tombstoneContent = useStateEvent( room, StateEvent.RoomTombstone )?.getContent(); const replacementRoom = tombstoneContent?.replacement_room; - const canUpgrade = permissions.stateEvent(StateEvent.RoomTombstone, mx.getSafeUserId()); + const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId()); + const canUpgrade = powerLevelAPI.canSendStateEvent( + powerLevels, + StateEvent.RoomTombstone, + userPowerLevel + ); const handleOpenRoom = () => { if (replacementRoom) { @@ -203,8 +85,31 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) { } }; + const [upgradeState, upgrade] = useAsyncCallback( + useCallback( + async (version: string) => { + await mx.upgradeRoom(room.roomId, version); + }, + [mx, room] + ) + ); + + const upgrading = upgradeState.status === AsyncStatus.Loading; + const [prompt, setPrompt] = useState(false); + const handleSubmitUpgrade: FormEventHandler = (evt) => { + evt.preventDefault(); + + const target = evt.target as HTMLFormElement | undefined; + const versionInput = target?.versionInput as HTMLInputElement | undefined; + const version = versionInput?.value.trim(); + if (!version) return; + + upgrade(version); + setPrompt(false); + }; + return ( @@ -250,7 +155,8 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) { variant="Secondary" fill="Solid" radii="300" - disabled={!canUpgrade} + disabled={upgrading || !canUpgrade} + before={upgrading && } onClick={() => setPrompt(true)} > Upgrade @@ -259,7 +165,63 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) { } > - {prompt && setPrompt(false)} />} + {upgradeState.status === AsyncStatus.Error && ( + + {(upgradeState.error as MatrixError).message} + + )} + + {prompt && ( + }> + + setPrompt(false), + clickOutsideDeactivates: true, + escapeDeactivates: stopPropagation, + }} + > + +
    + + {room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'} + + setPrompt(false)} radii="300"> + + +
    + + + This action is irreversible! + + + Version + + + + +
    +
    +
    +
    + )}
    ); diff --git a/src/app/features/common-settings/members/Members.tsx b/src/app/features/common-settings/members/Members.tsx index 9940a75..8d7f89f 100644 --- a/src/app/features/common-settings/members/Members.tsx +++ b/src/app/features/common-settings/members/Members.tsx @@ -27,12 +27,17 @@ import { Page, PageContent, PageHeader } from '../../../components/page'; import { useRoom } from '../../../hooks/useRoom'; import { useRoomMembers } from '../../../hooks/useRoomMembers'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; -import { usePowerLevels } from '../../../hooks/usePowerLevels'; +import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; +import { + useFlattenPowerLevelTagMembers, + usePowerLevelTags, +} from '../../../hooks/usePowerLevelTags'; import { VirtualTile } from '../../../components/virtualizer'; import { MemberTile } from '../../../components/member-tile'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { getMxIdLocalPart, getMxIdServer } from '../../../utils/matrix'; import { ServerBadge } from '../../../components/server-badge'; +import { openProfileViewer } from '../../../../client/action/navigation'; import { useDebounce } from '../../../hooks/useDebounce'; import { SearchItemStrGetter, @@ -41,21 +46,13 @@ import { } from '../../../hooks/useAsyncSearch'; import { getMemberSearchStr } from '../../../utils/room'; import { useMembershipFilter, useMembershipFilterMenu } from '../../../hooks/useMemberFilter'; -import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../../hooks/useMemberSort'; +import { useMemberSort, useMemberSortMenu } from '../../../hooks/useMemberSort'; import { settingsAtom } from '../../../state/settings'; import { useSetting } from '../../../state/hooks/settings'; import { UseStateProvider } from '../../../components/UseStateProvider'; import { MembershipFilterMenu } from '../../../components/MembershipFilterMenu'; import { MemberSortMenu } from '../../../components/MemberSortMenu'; import { ScrollTopContainer } from '../../../components/scroll-top-container'; -import { - useOpenUserRoomProfile, - useUserRoomProfileState, -} from '../../../state/hooks/userRoomProfile'; -import { useSpaceOptionally } from '../../../hooks/useSpace'; -import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../../hooks/useMemberPowerTag'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { getMouseEventCords } from '../../../utils/dom'; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, @@ -80,19 +77,15 @@ export function Members({ requestClose }: MembersProps) { const room = useRoom(); const members = useRoomMembers(mx, room.roomId); const fetchingMembers = members.length < room.getJoinedMemberCount(); - const openProfile = useOpenUserRoomProfile(); - const profileUser = useUserRoomProfileState(); - const space = useSpaceOptionally(); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels); + const { getPowerLevel } = usePowerLevelsAPI(powerLevels); + const [, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); const [membershipFilterIndex, setMembershipFilterIndex] = useState(0); const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex'); const membershipFilter = useMembershipFilter(membershipFilterIndex, useMembershipFilterMenu()); const memberSort = useMemberSort(sortFilterIndex, useMemberSortMenu()); - const memberPowerSort = useMemberPowerSort(creators); const scrollRef = useRef(null); const searchInputRef = useRef(null); @@ -103,8 +96,8 @@ export function Members({ requestClose }: MembersProps) { Array.from(members) .filter(membershipFilter.filterFn) .sort(memberSort.sortFn) - .sort(memberPowerSort), - [members, membershipFilter, memberSort, memberPowerSort] + .sort((a, b) => b.powerLevel - a.powerLevel), + [members, membershipFilter, memberSort] ); const [result, search, resetSearch] = useAsyncSearch( @@ -114,7 +107,11 @@ export function Members({ requestClose }: MembersProps) { ); if (!result && searchInputRef.current?.value) search(searchInputRef.current.value); - const flattenTagMembers = useFlattenPowerTagMembers(result?.items ?? sortedMembers, getPowerTag); + const flattenTagMembers = useFlattenPowerLevelTagMembers( + result?.items ?? sortedMembers, + getPowerLevel, + getPowerLevelTag + ); const virtualizer = useVirtualizer({ count: flattenTagMembers.length, @@ -145,9 +142,8 @@ export function Members({ requestClose }: MembersProps) { const handleMemberClick: MouseEventHandler = (evt) => { const btn = evt.currentTarget as HTMLButtonElement; const userId = btn.getAttribute('data-user-id'); - if (userId) { - openProfile(room.roomId, space?.roomId, userId, getMouseEventCords(evt.nativeEvent)); - } + openProfileViewer(userId, room.roomId); + requestClose(); }; return ( @@ -321,7 +317,6 @@ export function Members({ requestClose }: MembersProps) { Math.max(...getPowers(powerLevelTags)), [powerLevelTags]); const [permissionUpdate, setPermissionUpdate] = useState>( @@ -82,7 +82,6 @@ export function PermissionGroups({ permissionUpdate.forEach((power, location) => applyPermissionPower(draftPowerLevels, location, power) ); - return draftPowerLevels; }); await mx.sendStateEvent(room.roomId, StateEvent.RoomPowerLevels as any, editedPowerLevels); @@ -109,7 +108,7 @@ export function PermissionGroups({ const powerUpdate = permissionUpdate.get(USER_DEFAULT_LOCATION); const value = powerUpdate ?? power; - const tag = getPowerLevelTag(powerLevelTags, value); + const tag = getPowerLevelTag(value); const powerChanges = value !== power; return ( @@ -137,14 +136,14 @@ export function PermissionGroups({ fill="Soft" radii="Pill" aria-selected={opened} - disabled={!canEdit || applyingChanges} + disabled={!canChangePermission || applyingChanges} after={ powerChanges && ( ) } before={ - canEdit && ( + canChangePermission && ( ) } @@ -174,7 +173,7 @@ export function PermissionGroups({ const powerUpdate = permissionUpdate.get(item.location); const value = powerUpdate ?? power; - const tag = getPowerLevelTag(powerLevelTags, value); + const tag = getPowerLevelTag(value); const powerChanges = value !== power; return ( @@ -201,14 +200,14 @@ export function PermissionGroups({ fill="Soft" radii="Pill" aria-selected={opened} - disabled={!canEdit || applyingChanges} + disabled={!canChangePermission || applyingChanges} after={ powerChanges && ( ) } before={ - canEdit && ( + canChangePermission && ( - {creators.size > 0 && ( - - - - - - } - after={creatorTagIconSrc && } - > - - {creatorsTag.name} - - - - - - )} {getPowers(powerLevelTags).map((power) => { const tag = powerLevelTags[power]; - const tagIconSrc = tag.icon && getPowerTagIconSrc(mx, useAuthentication, tag.icon); + const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon); return ( void; + tag?: PowerLevelTag; + onSave: (power: number, tag: PowerLevelTag) => void; onClose: () => void; }; function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) { @@ -62,7 +63,6 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) { const room = useRoom(); const roomToParents = useAtomValue(roomToParentsAtom); const useAuthentication = useMediaAuthentication(); - const supportCreators = creatorsSupported(room.getVersion()); const imagePackRooms = useImagePackRooms(room.roomId, roomToParents); @@ -70,9 +70,9 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) { const pickFile = useFilePicker(setIconFile, false); const [tagColor, setTagColor] = useState(tag?.color); - const [tagIcon, setTagIcon] = useState(tag?.icon); + const [tagIcon, setTagIcon] = useState(tag?.icon); const uploadingIcon = iconFile && !tagIcon; - const tagIconSrc = tagIcon && getPowerTagIconSrc(mx, useAuthentication, tagIcon); + const tagIconSrc = tagIcon && getTagIconSrc(mx, useAuthentication, tagIcon); const iconUploadAtom = useMemo(() => { if (iconFile) return createUploadAtom(iconFile); @@ -101,11 +101,11 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) { const tagPower = parseInt(powerInput.value, 10); if (Number.isNaN(tagPower)) return; - + if (tagPower > maxPower) return; const tagName = nameInput.value.trim(); if (!tagName) return; - const editedTag: MemberPowerTag = { + const editedTag: PowerLevelTag = { name: tagName, color: tagColor, icon: tagIcon, @@ -165,7 +165,7 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) { radii="300" type="number" placeholder="75" - max={supportCreators ? undefined : maxPower} + max={maxPower} outlined={typeof power === 'number'} readOnly={typeof power === 'number'} required @@ -298,7 +298,7 @@ export function PowersEditor({ powerLevels, requestClose }: PowersEditorProps) { return [up, Math.max(...Array.from(up))]; }, [powerLevels]); - const powerLevelTags = usePowerLevelTags(room, powerLevels); + const [powerLevelTags] = usePowerLevelTags(room, powerLevels); const [editedPowerTags, setEditedPowerTags] = useState(); const [deleted, setDeleted] = useState>(new Set()); @@ -317,7 +317,7 @@ export function PowersEditor({ powerLevels, requestClose }: PowersEditorProps) { }, []); const handleSaveTag = useCallback( - (power: number, tag: MemberPowerTag) => { + (power: number, tag: PowerLevelTag) => { setEditedPowerTags((tags) => { const editedTags = { ...(tags ?? powerLevelTags) }; editedTags[power] = tag; @@ -419,8 +419,7 @@ export function PowersEditor({ powerLevels, requestClose }: PowersEditorProps) { {getPowers(powerTags).map((power) => { const tag = powerTags[power]; - const tagIconSrc = - tag.icon && getPowerTagIconSrc(mx, useAuthentication, tag.icon); + const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon); return ( { @@ -57,19 +50,12 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP const capabilities = useCapabilities(); const roomVersions = capabilities['m.room_versions']; const [selectedRoomVersion, selectRoomVersion] = useState(roomVersions?.default ?? '1'); - useEffect(() => { - // capabilities load async - selectRoomVersion(roomVersions?.default ?? '1'); - }, [roomVersions?.default]); const allowRestricted = space && restrictedSupported(selectedRoomVersion); const [kind, setKind] = useState( defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private ); - const allowAdditionalCreators = creatorsSupported(selectedRoomVersion); - const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } = - useAdditionalCreators(); const [federation, setFederation] = useState(true); const [encryption, setEncryption] = useState(false); const [knock, setKnock] = useState(false); @@ -126,7 +112,6 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP encryption: publicRoom ? false : encryption, knock: roomKnock, allowFederation: federation, - additionalCreators: allowAdditionalCreators ? additionalCreators : undefined, }).then((roomId) => { if (alive()) { onCreate?.(roomId); @@ -187,20 +172,6 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP - {allowAdditionalCreators && ( - - - - )} {kind !== CreateRoomKind.Public && ( <> diff --git a/src/app/features/create-space/CreateSpace.tsx b/src/app/features/create-space/CreateSpace.tsx index b1f9f64..d964152 100644 --- a/src/app/features/create-space/CreateSpace.tsx +++ b/src/app/features/create-space/CreateSpace.tsx @@ -1,4 +1,4 @@ -import React, { FormEventHandler, useCallback, useEffect, useState } from 'react'; +import React, { FormEventHandler, useCallback, useState } from 'react'; import { MatrixError, Room } from 'matrix-js-sdk'; import { Box, @@ -16,12 +16,7 @@ import { } from 'folds'; import { SettingTile } from '../../components/setting-tile'; import { SequenceCard } from '../../components/sequence-card'; -import { - creatorsSupported, - knockRestrictedSupported, - knockSupported, - restrictedSupported, -} from '../../utils/matrix'; +import { knockRestrictedSupported, knockSupported, restrictedSupported } from '../../utils/matrix'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { millisecondsToMinutes, replaceSpaceWithDash } from '../../utils/common'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; @@ -29,14 +24,12 @@ import { useCapabilities } from '../../hooks/useCapabilities'; import { useAlive } from '../../hooks/useAlive'; import { ErrorCode } from '../../cs-errorcode'; import { - AdditionalCreatorInput, createRoom, CreateRoomAliasInput, CreateRoomData, CreateRoomKind, CreateRoomKindSelector, RoomVersionSelector, - useAdditionalCreators, } from '../../components/create-room'; import { RoomType } from '../../../types/matrix/room'; @@ -58,20 +51,12 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor const capabilities = useCapabilities(); const roomVersions = capabilities['m.room_versions']; const [selectedRoomVersion, selectRoomVersion] = useState(roomVersions?.default ?? '1'); - useEffect(() => { - // capabilities load async - selectRoomVersion(roomVersions?.default ?? '1'); - }, [roomVersions?.default]); const allowRestricted = space && restrictedSupported(selectedRoomVersion); const [kind, setKind] = useState( defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private ); - - const allowAdditionalCreators = creatorsSupported(selectedRoomVersion); - const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } = - useAdditionalCreators(); const [federation, setFederation] = useState(true); const [knock, setKnock] = useState(false); const [advance, setAdvance] = useState(false); @@ -127,7 +112,6 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor aliasLocalPart: publicRoom ? aliasLocalPart : undefined, knock: roomKnock, allowFederation: federation, - additionalCreators: allowAdditionalCreators ? additionalCreators : undefined, }).then((roomId) => { if (alive()) { onCreate?.(roomId); @@ -188,20 +172,6 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor - {allowAdditionalCreators && ( - - - - )} {kind !== CreateRoomKind.Public && advance && (allowKnock || allowKnockRestricted) && ( { const newContent: MSpaceChildContent = { ...content, suggested: !content.suggested }; - return mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, newContent, roomId); + return mx.sendStateEvent(parentId, StateEvent.SpaceChild, newContent, roomId); }, [mx, parentId, roomId, content]) ); @@ -85,7 +82,7 @@ function RemoveMenuItem({ const [removeState, handleRemove] = useAsyncCallback( useCallback( - () => mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, {}, roomId), + () => mx.sendStateEvent(parentId, StateEvent.SpaceChild, {}, roomId), [mx, parentId, roomId] ) ); @@ -183,7 +180,7 @@ type HierarchyItemMenuProps = { parentId: string; }; joined: boolean; - powerLevels?: IPowerLevels; + canInvite: boolean; canEditChild: boolean; pinned?: boolean; onTogglePin?: (roomId: string) => void; @@ -191,22 +188,13 @@ type HierarchyItemMenuProps = { export function HierarchyItemMenu({ item, joined, - powerLevels, + canInvite, canEditChild, pinned, onTogglePin, }: HierarchyItemMenuProps) { - const mx = useMatrixClient(); const [menuAnchor, setMenuAnchor] = useState(); - const canInvite = (): boolean => { - if (!powerLevels) return false; - const creators = getRoomCreatorsForRoomId(mx, item.roomId); - const permissions = getRoomPermissionsAPI(creators, powerLevels); - - return permissions.action('invite', mx.getSafeUserId()); - }; - const handleOpenMenu: MouseEventHandler = (evt) => { setMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; @@ -266,7 +254,7 @@ export function HierarchyItemMenu({ diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index 4b19e51..45610ff 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -27,6 +27,7 @@ import { useElementSizeObserver } from '../../hooks/useElementSizeObserver'; import { IPowerLevels, PowerLevelsContextProvider, + powerLevelAPI, usePowerLevels, useRoomsPowerLevels, } from '../../hooks/usePowerLevels'; @@ -54,13 +55,12 @@ import { useRoomMembers } from '../../hooks/useRoomMembers'; import { SpaceHierarchy } from './SpaceHierarchy'; import { useGetRoom } from '../../hooks/useGetRoom'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; -import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions'; -import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators'; const useCanDropLobbyItem = ( space: Room, roomsPowerLevels: Map, - getRoom: (roomId: string) => Room | undefined + getRoom: (roomId: string) => Room | undefined, + canEditSpaceChild: (powerLevels: IPowerLevels) => boolean ): CanDropCallback => { const mx = useMatrixClient(); @@ -74,20 +74,16 @@ const useCanDropLobbyItem = ( const containerSpaceId = space.roomId; - const powerLevels = roomsPowerLevels.get(containerSpaceId) ?? {}; - const creators = getRoomCreatorsForRoomId(mx, containerSpaceId); - const permissions = getRoomPermissionsAPI(creators, powerLevels); - if ( getRoom(containerSpaceId) === undefined || - !permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()) + !canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {}) ) { return false; } return true; }, - [space, roomsPowerLevels, getRoom, mx] + [space, roomsPowerLevels, getRoom, canEditSpaceChild] ); const canDropRoom: CanDropCallback = useCallback( @@ -101,31 +97,30 @@ const useCanDropLobbyItem = ( // check and do not allow restricted room to be dragged outside // current space if can't change `m.room.join_rules` `content.allow` if (draggingOutsideSpace && restrictedItem) { - const itemPowerLevels = roomsPowerLevels.get(item.roomId) ?? {}; - const itemCreators = getRoomCreatorsForRoomId(mx, item.roomId); - const itemPermissions = getRoomPermissionsAPI(itemCreators, itemPowerLevels); - - const canChangeJoinRuleAllow = itemPermissions.stateEvent( + const itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {}; + const userPLInItem = powerLevelAPI.getPowerLevel( + itemPowerLevel, + mx.getUserId() ?? undefined + ); + const canChangeJoinRuleAllow = powerLevelAPI.canSendStateEvent( + itemPowerLevel, StateEvent.RoomJoinRules, - mx.getSafeUserId() + userPLInItem ); if (!canChangeJoinRuleAllow) { return false; } } - const powerLevels = roomsPowerLevels.get(containerSpaceId) ?? {}; - const creators = getRoomCreatorsForRoomId(mx, containerSpaceId); - const permissions = getRoomPermissionsAPI(creators, powerLevels); if ( getRoom(containerSpaceId) === undefined || - !permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()) + !canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {}) ) { return false; } return true; }, - [mx, getRoom, roomsPowerLevels] + [mx, getRoom, canEditSpaceChild, roomsPowerLevels] ); const canDrop: CanDropCallback = useCallback( @@ -188,6 +183,16 @@ export function Lobby() { const getRoom = useGetRoom(allJoinedRooms); + const canEditSpaceChild = useCallback( + (powerLevels: IPowerLevels) => + powerLevelAPI.canSendStateEvent( + powerLevels, + StateEvent.SpaceChild, + powerLevelAPI.getPowerLevel(powerLevels, mx.getUserId() ?? undefined) + ), + [mx] + ); + const [draggingItem, setDraggingItem] = useState(); const hierarchy = useSpaceHierarchy( space.roomId, @@ -224,7 +229,12 @@ export function Lobby() { ) ); - const canDrop: CanDropCallback = useCanDropLobbyItem(space, roomsPowerLevels, getRoom); + const canDrop: CanDropCallback = useCanDropLobbyItem( + space, + roomsPowerLevels, + getRoom, + canEditSpaceChild + ); const [reorderSpaceState, reorderSpace] = useAsyncCallback( useCallback( @@ -260,11 +270,7 @@ export function Lobby() { .filter((reorder, index) => { if (!reorder.item.parentId) return false; const parentPL = roomsPowerLevels.get(reorder.item.parentId); - if (!parentPL) return false; - - const creators = getRoomCreatorsForRoomId(mx, reorder.item.parentId); - const permissions = getRoomPermissionsAPI(creators, parentPL); - const canEdit = permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()); + const canEdit = parentPL && canEditSpaceChild(parentPL); return canEdit && reorder.orderKey !== currentOrders[index]; }); @@ -280,7 +286,7 @@ export function Lobby() { }); } }, - [mx, hierarchy, lex, roomsPowerLevels] + [mx, hierarchy, lex, roomsPowerLevels, canEditSpaceChild] ) ); const reorderingSpace = reorderSpaceState.status === AsyncStatus.Loading; @@ -422,7 +428,7 @@ export function Lobby() { newItems.push(rId); } const newSpacesContent = makeCinnySpacesContent(mx, newItems); - mx.setAccountData(AccountDataEvent.CinnySpaces as any, newSpacesContent as any); + mx.setAccountData(AccountDataEvent.CinnySpaces, newSpacesContent); }, [mx, sidebarItems, sidebarSpaces] ); @@ -487,6 +493,7 @@ export function Lobby() { allJoinedRooms={allJoinedRooms} mDirects={mDirects} roomsPowerLevels={roomsPowerLevels} + canEditSpaceChild={canEditSpaceChild} categoryId={categoryId} closed={ closedCategories.has(categoryId) || diff --git a/src/app/features/lobby/LobbyHeader.tsx b/src/app/features/lobby/LobbyHeader.tsx index 7728712..bc4c46f 100644 --- a/src/app/features/lobby/LobbyHeader.tsx +++ b/src/app/features/lobby/LobbyHeader.tsx @@ -27,7 +27,7 @@ import { RoomAvatar } from '../../components/room-avatar'; import { nameInitials } from '../../utils/common'; import * as css from './LobbyHeader.css'; import { openInviteUser } from '../../../client/action/navigation'; -import { IPowerLevels } from '../../hooks/usePowerLevels'; +import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { UseStateProvider } from '../../components/UseStateProvider'; import { LeaveSpacePrompt } from '../../components/leave-space-prompt'; import { stopPropagation } from '../../utils/keyboard'; @@ -36,30 +36,26 @@ import { BackRouteHandler } from '../../components/BackRouteHandler'; import { mxcUrlToHttp } from '../../utils/matrix'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings'; -import { useRoomCreators } from '../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../hooks/useRoomPermissions'; type LobbyMenuProps = { + roomId: string; powerLevels: IPowerLevels; requestClose: () => void; }; const LobbyMenu = forwardRef( - ({ powerLevels, requestClose }, ref) => { + ({ roomId, powerLevels, requestClose }, ref) => { const mx = useMatrixClient(); - const space = useSpace(); - const creators = useRoomCreators(space); - - const permissions = useRoomPermissions(creators, powerLevels); - const canInvite = permissions.action('invite', mx.getSafeUserId()); + const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); + const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const openSpaceSettings = useOpenSpaceSettings(); const handleInvite = () => { - openInviteUser(space.roomId); + openInviteUser(roomId); requestClose(); }; const handleRoomSettings = () => { - openSpaceSettings(space.roomId); + openSpaceSettings(roomId); requestClose(); }; @@ -110,7 +106,7 @@ const LobbyMenu = forwardRef( {promptLeave && ( setPromptLeave(false)} /> @@ -246,6 +242,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) { }} > setMenuAnchor(undefined)} /> diff --git a/src/app/features/lobby/SpaceHierarchy.tsx b/src/app/features/lobby/SpaceHierarchy.tsx index 280b8a5..a152bc1 100644 --- a/src/app/features/lobby/SpaceHierarchy.tsx +++ b/src/app/features/lobby/SpaceHierarchy.tsx @@ -8,16 +8,14 @@ import { HierarchyItemSpace, useFetchSpaceHierarchyLevel, } from '../../hooks/useSpaceHierarchy'; -import { IPowerLevels } from '../../hooks/usePowerLevels'; +import { IPowerLevels, powerLevelAPI } from '../../hooks/usePowerLevels'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { SpaceItemCard } from './SpaceItem'; import { AfterItemDropTarget, CanDropCallback } from './DnD'; import { HierarchyItemMenu } from './HierarchyItemMenu'; import { RoomItemCard } from './RoomItem'; -import { RoomType, StateEvent } from '../../../types/matrix/room'; +import { RoomType } from '../../../types/matrix/room'; import { SequenceCard } from '../../components/sequence-card'; -import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators'; -import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions'; type SpaceHierarchyProps = { summary: IHierarchyRoom | undefined; @@ -26,6 +24,7 @@ type SpaceHierarchyProps = { allJoinedRooms: Set; mDirects: Set; roomsPowerLevels: Map; + canEditSpaceChild: (powerLevels: IPowerLevels) => boolean; categoryId: string; closed: boolean; handleClose: MouseEventHandler; @@ -49,6 +48,7 @@ export const SpaceHierarchy = forwardRef( allJoinedRooms, mDirects, roomsPowerLevels, + canEditSpaceChild, categoryId, closed, handleClose, @@ -79,28 +79,25 @@ export const SpaceHierarchy = forwardRef( return s; }, [rooms]); - const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId); - const spaceCreators = getRoomCreatorsForRoomId(mx, spaceItem.roomId); - const spacePermissions = - spacePowerLevels && getRoomPermissionsAPI(spaceCreators, spacePowerLevels); + const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId) ?? {}; + const userPLInSpace = powerLevelAPI.getPowerLevel( + spacePowerLevels, + mx.getUserId() ?? undefined + ); + const canInviteInSpace = powerLevelAPI.canDoAction(spacePowerLevels, 'invite', userPLInSpace); const draggingSpace = draggingItem?.roomId === spaceItem.roomId && draggingItem.parentId === spaceItem.parentId; const { parentId } = spaceItem; - const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) : undefined; - const parentCreators = parentId ? getRoomCreatorsForRoomId(mx, parentId) : undefined; - const parentPermissions = - parentCreators && - parentPowerLevels && - getRoomPermissionsAPI(parentCreators, parentPowerLevels); + const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) ?? {} : undefined; useEffect(() => { onSpacesFound(Array.from(subspaces.values())); }, [subspaces, onSpacesFound]); let childItems = roomItems?.filter((i) => !subspaces.has(i.roomId)); - if (!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())) { + if (!canEditSpaceChild(spacePowerLevels)) { // hide unknown rooms for normal user childItems = childItems?.filter((i) => { const forbidden = error instanceof MatrixError ? error.errcode === 'M_FORBIDDEN' : false; @@ -120,22 +117,18 @@ export const SpaceHierarchy = forwardRef( closed={closed} handleClose={handleClose} getRoom={getRoom} - canEditChild={!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())} + canEditChild={canEditSpaceChild(spacePowerLevels)} canReorder={ - parentPowerLevels && !disabledReorder && parentPermissions - ? parentPermissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()) - : false + parentPowerLevels && !disabledReorder ? canEditSpaceChild(parentPowerLevels) : false } options={ parentId && parentPowerLevels && ( @@ -158,6 +151,15 @@ export const SpaceHierarchy = forwardRef( const roomSummary = rooms.get(roomItem.roomId); const roomPowerLevels = roomsPowerLevels.get(roomItem.roomId) ?? {}; + const userPLInRoom = powerLevelAPI.getPowerLevel( + roomPowerLevels, + mx.getUserId() ?? undefined + ); + const canInviteInRoom = powerLevelAPI.canDoAction( + roomPowerLevels, + 'invite', + userPLInRoom + ); const lastItem = index === childItems.length; const nextRoomId = lastItem ? nextSpaceId : childItems[index + 1]?.roomId; @@ -176,18 +178,13 @@ export const SpaceHierarchy = forwardRef( dm={mDirects.has(roomItem.roomId)} onOpen={onOpenRoom} getRoom={getRoom} - canReorder={ - !!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()) && - !disabledReorder - } + canReorder={canEditSpaceChild(spacePowerLevels) && !disabledReorder} options={ } after={ diff --git a/src/app/features/lobby/SpaceItem.tsx b/src/app/features/lobby/SpaceItem.tsx index 64a9790..e881a97 100644 --- a/src/app/features/lobby/SpaceItem.tsx +++ b/src/app/features/lobby/SpaceItem.tsx @@ -30,12 +30,12 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import * as css from './SpaceItem.css'; import * as styleCss from './style.css'; import { useDraggableItem } from './DnD'; +import { openSpaceAddExisting } from '../../../client/action/navigation'; import { stopPropagation } from '../../utils/keyboard'; import { mxcUrlToHttp } from '../../utils/matrix'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal'; import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal'; -import { AddExistingModal } from '../add-existing'; function SpaceProfileLoading() { return ( @@ -243,7 +243,6 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP function AddRoomButton({ item }: { item: HierarchyItem }) { const [cords, setCords] = useState(); const openCreateRoomModal = useOpenCreateRoomModal(); - const [addExisting, setAddExisting] = useState(false); const handleAddRoom: MouseEventHandler = (evt) => { setCords(evt.currentTarget.getBoundingClientRect()); @@ -255,7 +254,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) { }; const handleAddExisting = () => { - setAddExisting(true); + openSpaceAddExisting(item.roomId); setCords(undefined); }; @@ -301,9 +300,6 @@ function AddRoomButton({ item }: { item: HierarchyItem }) { > Add Room - {addExisting && ( - setAddExisting(false)} /> - )} ); } @@ -311,7 +307,6 @@ function AddRoomButton({ item }: { item: HierarchyItem }) { function AddSpaceButton({ item }: { item: HierarchyItem }) { const [cords, setCords] = useState(); const openCreateSpaceModal = useOpenCreateSpaceModal(); - const [addExisting, setAddExisting] = useState(false); const handleAddSpace: MouseEventHandler = (evt) => { setCords(evt.currentTarget.getBoundingClientRect()); @@ -323,7 +318,7 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) { }; const handleAddExisting = () => { - setAddExisting(true); + openSpaceAddExisting(item.roomId, true); setCords(undefined); }; return ( @@ -368,9 +363,6 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) { > Add Space - {addExisting && ( - setAddExisting(false)} /> - )} ); } diff --git a/src/app/features/message-search/SearchResultGroup.tsx b/src/app/features/message-search/SearchResultGroup.tsx index 62ef9c4..bc94092 100644 --- a/src/app/features/message-search/SearchResultGroup.tsx +++ b/src/app/features/message-search/SearchResultGroup.tsx @@ -39,18 +39,15 @@ import { UserAvatar } from '../../components/user-avatar'; import { useMentionClickHandler } from '../../hooks/useMentionClickHandler'; import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; -import { usePowerLevels } from '../../hooks/usePowerLevels'; -import { usePowerLevelTags } from '../../hooks/usePowerLevelTags'; +import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; +import { + getTagIconSrc, + useAccessibleTagColors, + usePowerLevelTags, +} from '../../hooks/usePowerLevelTags'; import { useTheme } from '../../hooks/useTheme'; import { PowerIcon } from '../../components/power'; import colorMXID from '../../../util/colorMXID'; -import { - getPowerTagIconSrc, - useAccessiblePowerTagColors, - useGetMemberPowerTag, -} from '../../hooks/useMemberPowerTag'; -import { useRoomCreators } from '../../hooks/useRoomCreators'; -import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag'; type SearchResultGroupProps = { room: Room; @@ -79,14 +76,10 @@ export function SearchResultGroup({ const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const creatorsTag = useRoomCreatorsTag(); - const powerLevelTags = usePowerLevelTags(room, powerLevels); - const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels); - + const { getPowerLevel } = usePowerLevelsAPI(powerLevels); + const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); const theme = useTheme(); - const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags); + const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags); const mentionClickHandler = useMentionClickHandler(room.roomId); const spoilerClickHandler = useSpoilerClickHandler(); @@ -233,12 +226,13 @@ export function SearchResultGroup({ const threadRootId = relation?.rel_type === RelationType.Thread ? relation.event_id : undefined; - const memberPowerTag = getMemberPowerTag(event.sender); - const tagColor = memberPowerTag?.color - ? accessibleTagColors?.get(memberPowerTag.color) + const senderPowerLevel = getPowerLevel(event.sender); + const powerLevelTag = getPowerLevelTag(senderPowerLevel); + const tagColor = powerLevelTag?.color + ? accessibleTagColors?.get(powerLevelTag.color) : undefined; - const tagIconSrc = memberPowerTag?.icon - ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon) + const tagIconSrc = powerLevelTag?.icon + ? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon) : undefined; const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor; @@ -308,7 +302,8 @@ export function SearchResultGroup({ replyEventId={replyEventId} threadRootId={threadRootId} onClick={handleOpenClick} - getMemberPowerTag={getMemberPowerTag} + getPowerLevel={getPowerLevel} + getPowerLevelTag={getPowerLevelTag} accessibleTagColors={accessibleTagColors} legacyUsernameColor={legacyUsernameColor} /> diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index ee8b678..bdb8141 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -27,7 +27,7 @@ import { nameInitials } from '../../utils/common'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRoomUnread } from '../../state/hooks/unread'; import { roomToUnreadAtom } from '../../state/room/roomToUnread'; -import { usePowerLevels } from '../../hooks/usePowerLevels'; +import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { copyToClipboard } from '../../utils/dom'; import { markAsRead } from '../../../client/action/notifications'; import { openInviteUser } from '../../../client/action/navigation'; @@ -49,8 +49,6 @@ import { RoomNotificationMode, } from '../../hooks/useRoomsNotificationPreferences'; import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher'; -import { useRoomCreators } from '../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../hooks/useRoomPermissions'; type RoomNavItemMenuProps = { room: Room; @@ -63,10 +61,8 @@ const RoomNavItemMenu = forwardRef( const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - const canInvite = permissions.action('invite', mx.getSafeUserId()); + const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); + const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const openRoomSettings = useOpenRoomSettings(); const space = useSpaceOptionally(); diff --git a/src/app/features/room-settings/general/General.tsx b/src/app/features/room-settings/general/General.tsx index d9c16c9..0c3152c 100644 --- a/src/app/features/room-settings/general/General.tsx +++ b/src/app/features/room-settings/general/General.tsx @@ -13,8 +13,6 @@ import { RoomPublish, RoomUpgrade, } from '../../common-settings/general'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; type GeneralProps = { requestClose: () => void; @@ -22,8 +20,6 @@ type GeneralProps = { export function General({ requestClose }: GeneralProps) { const room = useRoom(); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - const permissions = useRoomPermissions(creators, powerLevels); return ( @@ -45,22 +41,22 @@ export function General({ requestClose }: GeneralProps) { - + Options - - - - + + + + Addresses - - + + Advance Options - + diff --git a/src/app/features/room-settings/permissions/Permissions.tsx b/src/app/features/room-settings/permissions/Permissions.tsx index 7572a71..ae3769b 100644 --- a/src/app/features/room-settings/permissions/Permissions.tsx +++ b/src/app/features/room-settings/permissions/Permissions.tsx @@ -2,13 +2,11 @@ import React, { useState } from 'react'; import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds'; import { Page, PageContent, PageHeader } from '../../../components/page'; import { useRoom } from '../../../hooks/useRoom'; -import { usePowerLevels } from '../../../hooks/usePowerLevels'; +import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { StateEvent } from '../../../../types/matrix/room'; import { usePermissionGroups } from './usePermissionItems'; import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; type PermissionsProps = { requestClose: () => void; @@ -17,12 +15,11 @@ export function Permissions({ requestClose }: PermissionsProps) { const mx = useMatrixClient(); const room = useRoom(); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - - const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId()); - const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId()); + const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); + const canEditPowers = canSendStateEvent( + StateEvent.PowerLevelTags, + getPowerLevel(mx.getSafeUserId()) + ); const permissionGroups = usePermissionGroups(); const [powerEditor, setPowerEditor] = useState(false); @@ -60,11 +57,7 @@ export function Permissions({ requestClose }: PermissionsProps) { onEdit={canEditPowers ? handleEditPowers : undefined} permissionGroups={permissionGroups} /> - + diff --git a/src/app/features/room/MembersDrawer.css.ts b/src/app/features/room/MembersDrawer.css.ts index 860ceda..a1f4153 100644 --- a/src/app/features/room/MembersDrawer.css.ts +++ b/src/app/features/room/MembersDrawer.css.ts @@ -1,8 +1,10 @@ import { keyframes, style } from '@vanilla-extract/css'; -import { config, toRem } from 'folds'; +import { color, config, toRem } from 'folds'; export const MembersDrawer = style({ width: toRem(266), + backgroundColor: color.Background.Container, + color: color.Background.OnContainer, }); export const MembersDrawerHeader = style({ diff --git a/src/app/features/room/MembersDrawer.tsx b/src/app/features/room/MembersDrawer.tsx index 46d2238..5edb4f2 100644 --- a/src/app/features/room/MembersDrawer.tsx +++ b/src/app/features/room/MembersDrawer.tsx @@ -26,10 +26,11 @@ import { TooltipProvider, config, } from 'folds'; -import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; +import { Room, RoomMember } from 'matrix-js-sdk'; import { useVirtualizer } from '@tanstack/react-virtual'; import classNames from 'classnames'; +import { openProfileViewer } from '../../../client/action/navigation'; import * as css from './MembersDrawer.css'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { UseStateProvider } from '../../components/UseStateProvider'; @@ -39,6 +40,7 @@ import { useAsyncSearch, } from '../../hooks/useAsyncSearch'; import { useDebounce } from '../../hooks/useDebounce'; +import { usePowerLevelTags, useFlattenPowerLevelTagMembers } from '../../hooks/usePowerLevelTags'; import { TypingIndicator } from '../../components/typing-indicator'; import { getMemberDisplayName, getMemberSearchStr } from '../../utils/room'; import { getMxIdLocalPart } from '../../utils/matrix'; @@ -50,116 +52,10 @@ import { UserAvatar } from '../../components/user-avatar'; import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMembershipFilter, useMembershipFilterMenu } from '../../hooks/useMemberFilter'; -import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort'; -import { usePowerLevelsContext } from '../../hooks/usePowerLevels'; +import { useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort'; +import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels'; import { MembershipFilterMenu } from '../../components/MembershipFilterMenu'; import { MemberSortMenu } from '../../components/MemberSortMenu'; -import { useOpenUserRoomProfile, useUserRoomProfileState } from '../../state/hooks/userRoomProfile'; -import { useSpaceOptionally } from '../../hooks/useSpace'; -import { ContainerColor } from '../../styles/ContainerColor.css'; -import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag'; -import { useRoomCreators } from '../../hooks/useRoomCreators'; - -type MemberDrawerHeaderProps = { - room: Room; -}; -function MemberDrawerHeader({ room }: MemberDrawerHeaderProps) { - const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); - - return ( -
    - - - - {`${millify(room.getJoinedMemberCount())} Members`} - - - - - Close - - } - > - {(triggerRef) => ( - setPeopleDrawer(false)} - > - - - )} - - - -
    - ); -} - -type MemberItemProps = { - mx: MatrixClient; - useAuthentication: boolean; - room: Room; - member: RoomMember; - onClick: MouseEventHandler; - pressed?: boolean; - typing?: boolean; -}; -function MemberItem({ - mx, - useAuthentication, - room, - member, - onClick, - pressed, - typing, -}: MemberItemProps) { - const name = - getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId; - const avatarMxcUrl = member.getMxcAvatarUrl(); - const avatarUrl = avatarMxcUrl - ? mx.mxcUrlToHttp(avatarMxcUrl, 100, 100, 'crop', undefined, false, useAuthentication) - : undefined; - - return ( - - } - /> - - } - after={ - typing && ( - - - - ) - } - > - - - {name} - - - - ); -} const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, @@ -183,28 +79,28 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) { const searchInputRef = useRef(null); const scrollTopAnchorRef = useRef(null); const powerLevels = usePowerLevelsContext(); - const creators = useRoomCreators(room); - const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels); - + const [, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); const fetchingMembers = members.length < room.getJoinedMemberCount(); - const openUserRoomProfile = useOpenUserRoomProfile(); - const space = useSpaceOptionally(); - const openProfileUserId = useUserRoomProfileState()?.userId; + const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); const membershipFilterMenu = useMembershipFilterMenu(); const sortFilterMenu = useMemberSortMenu(); const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex'); const [membershipFilterIndex, setMembershipFilterIndex] = useState(0); + const { getPowerLevel } = usePowerLevelsAPI(powerLevels); const membershipFilter = useMembershipFilter(membershipFilterIndex, membershipFilterMenu); const memberSort = useMemberSort(sortFilterIndex, sortFilterMenu); - const memberPowerSort = useMemberPowerSort(creators); const typingMembers = useRoomTypingMember(room.roomId); const filteredMembers = useMemo( - () => members.filter(membershipFilter.filterFn).sort(memberSort.sortFn).sort(memberPowerSort), - [members, membershipFilter, memberSort, memberPowerSort] + () => + members + .filter(membershipFilter.filterFn) + .sort(memberSort.sortFn) + .sort((a, b) => b.powerLevel - a.powerLevel), + [members, membershipFilter, memberSort] ); const [result, search, resetSearch] = useAsyncSearch( @@ -216,7 +112,11 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) { const processMembers = result ? result.items : filteredMembers; - const PLTagOrRoomMember = useFlattenPowerTagMembers(processMembers, getPowerTag); + const PLTagOrRoomMember = useFlattenPowerLevelTagMembers( + processMembers, + getPowerLevel, + getPowerLevelTag + ); const virtualizer = useVirtualizer({ count: PLTagOrRoomMember.length, @@ -236,20 +136,48 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) { { wait: 200 } ); + const getName = (member: RoomMember) => + getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId; + const handleMemberClick: MouseEventHandler = (evt) => { const btn = evt.currentTarget as HTMLButtonElement; const userId = btn.getAttribute('data-user-id'); - if (!userId) return; - openUserRoomProfile(room.roomId, space?.roomId, userId, btn.getBoundingClientRect(), 'Left'); + openProfileViewer(userId, room.roomId); }; return ( - - + +
    + + + + {`${millify(room.getJoinedMemberCount())} Members`} + + + + + Close + + } + > + {(triggerRef) => ( + setPeopleDrawer(false)} + > + + + )} + + + +
    @@ -401,28 +329,59 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) { ); } + const member = tagOrMember; + const name = getName(member); + const avatarMxcUrl = member.getMxcAvatarUrl(); + const avatarUrl = avatarMxcUrl + ? mx.mxcUrlToHttp( + avatarMxcUrl, + 100, + 100, + 'crop', + undefined, + false, + useAuthentication + ) + : undefined; + return ( -
    + } + /> + + } + after={ + typingMembers.find((receipt) => receipt.userId === member.userId) && ( + + + + ) + } > - receipt.userId === tagOrMember.userId - )} - /> -
    + + + {name} + + + ); })} diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 76bafc9..1399ec1 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -108,23 +108,21 @@ import { ReplyLayout, ThreadIndicator } from '../../components/message'; import { roomToParentsAtom } from '../../state/room/roomToParents'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useImagePackRooms } from '../../hooks/useImagePackRooms'; -import { usePowerLevelsContext } from '../../hooks/usePowerLevels'; +import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags'; +import { powerLevelAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels'; import colorMXID from '../../../util/colorMXID'; import { useIsDirectRoom } from '../../hooks/useRoom'; -import { useAccessiblePowerTagColors, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag'; -import { useRoomCreators } from '../../hooks/useRoomCreators'; -import { useTheme } from '../../hooks/useTheme'; -import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag'; -import { usePowerLevelTags } from '../../hooks/usePowerLevelTags'; interface RoomInputProps { editor: Editor; fileDropContainerRef: RefObject; roomId: string; room: Room; + getPowerLevelTag: GetPowerLevelTag; + accessibleTagColors: Map; } export const RoomInput = forwardRef( - ({ editor, fileDropContainerRef, roomId, room }, ref) => { + ({ editor, fileDropContainerRef, roomId, room, getPowerLevelTag, accessibleTagColors }, ref) => { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline'); @@ -136,24 +134,13 @@ export const RoomInput = forwardRef( const emojiBtnRef = useRef(null); const roomToParents = useAtomValue(roomToParentsAtom); const powerLevels = usePowerLevelsContext(); - const creators = useRoomCreators(room); const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId)); const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId)); const replyUserID = replyDraft?.userId; - const powerLevelTags = usePowerLevelTags(room, powerLevels); - const creatorsTag = useRoomCreatorsTag(); - const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels); - const theme = useTheme(); - const accessibleTagColors = useAccessiblePowerTagColors( - theme.kind, - creatorsTag, - powerLevelTags - ); - - const replyPowerTag = replyUserID ? getMemberPowerTag(replyUserID) : undefined; - const replyPowerColor = replyPowerTag?.color + const replyPowerTag = getPowerLevelTag(powerLevelAPI.getPowerLevel(powerLevels, replyUserID)); + const replyPowerColor = replyPowerTag.color ? accessibleTagColors.get(replyPowerTag.color) : undefined; const replyUsernameColor = @@ -290,7 +277,7 @@ export const RoomInput = forwardRef( }); handleCancelUpload(uploads); const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises)); - contents.forEach((content) => mx.sendMessage(roomId, content as any)); + contents.forEach((content) => mx.sendMessage(roomId, content)); }; const submit = useCallback(() => { @@ -369,7 +356,7 @@ export const RoomInput = forwardRef( content['m.relates_to'].is_falling_back = false; } } - mx.sendMessage(roomId, content as any); + mx.sendMessage(roomId, content); resetEditor(editor); resetEditorHistory(editor); setReplyDraft(undefined); diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 7a012e4..244eb32 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -85,6 +85,7 @@ import { } from '../../utils/room'; import { useSetting } from '../../state/hooks/settings'; import { MessageLayout, settingsAtom } from '../../state/settings'; +import { openProfileViewer } from '../../../client/action/navigation'; import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer'; import { Reactions, Message, Event, EncryptedContent } from './message'; import { useMemberEventParser } from '../../hooks/useMemberEventParser'; @@ -101,7 +102,7 @@ import * as css from './RoomTimeline.css'; import { inSameDay, minuteDifference, timeDayMonthYear, today, yesterday } from '../../utils/time'; import { createMentionElement, isEmptyEditor, moveCursor } from '../../components/editor'; import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts'; -import { usePowerLevelsContext } from '../../hooks/usePowerLevels'; +import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels'; import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room'; import { useKeyDown } from '../../hooks/useKeyDown'; import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange'; @@ -117,15 +118,8 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useIgnoredUsers } from '../../hooks/useIgnoredUsers'; import { useImagePackRooms } from '../../hooks/useImagePackRooms'; +import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags'; import { useIsDirectRoom } from '../../hooks/useRoom'; -import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile'; -import { useSpaceOptionally } from '../../hooks/useSpace'; -import { useRoomCreators } from '../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../hooks/useRoomPermissions'; -import { useAccessiblePowerTagColors, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag'; -import { useTheme } from '../../hooks/useTheme'; -import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag'; -import { usePowerLevelTags } from '../../hooks/usePowerLevelTags'; const TimelineFloat = as<'div', css.TimelineFloatVariants>( ({ position, className, ...props }, ref) => ( @@ -228,6 +222,8 @@ type RoomTimelineProps = { eventId?: string; roomInputRef: RefObject; editor: Editor; + getPowerLevelTag: GetPowerLevelTag; + accessibleTagColors: Map; }; const PAGINATION_LIMIT = 80; @@ -430,7 +426,14 @@ const getRoomUnreadInfo = (room: Room, scrollTo = false) => { }; }; -export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) { +export function RoomTimeline({ + room, + eventId, + roomInputRef, + editor, + getPowerLevelTag, + accessibleTagColors, +}: RoomTimelineProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); @@ -455,24 +458,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const setReplyDraft = useSetAtom(roomIdToReplyDraftAtomFamily(room.roomId)); const powerLevels = usePowerLevelsContext(); - const creators = useRoomCreators(room); + const { canDoAction, canSendEvent, canSendStateEvent, getPowerLevel } = + usePowerLevelsAPI(powerLevels); - const creatorsTag = useRoomCreatorsTag(); - const powerLevelTags = usePowerLevelTags(room, powerLevels); - const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels); - - const theme = useTheme(); - const accessiblePowerTagColors = useAccessiblePowerTagColors( - theme.kind, - creatorsTag, - powerLevelTags - ); - - const permissions = useRoomPermissions(creators, powerLevels); - - const canRedact = permissions.action('redact', mx.getSafeUserId()); - const canSendReaction = permissions.event(MessageEvent.Reaction, mx.getSafeUserId()); - const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, mx.getSafeUserId()); + const myPowerLevel = getPowerLevel(mx.getUserId() ?? ''); + const canRedact = canDoAction('redact', myPowerLevel); + const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel); + const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, myPowerLevel); const [editId, setEditId] = useState(); const roomToParents = useAtomValue(roomToParentsAtom); @@ -480,8 +472,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const { navigateRoom } = useRoomNavigate(); const mentionClickHandler = useMentionClickHandler(room.roomId); const spoilerClickHandler = useSpoilerClickHandler(); - const openUserRoomProfile = useOpenUserRoomProfile(); - const space = useSpaceOptionally(); const imagePackRooms: Room[] = useImagePackRooms(room.roomId, roomToParents); @@ -919,14 +909,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli console.warn('Button should have "data-user-id" attribute!'); return; } - openUserRoomProfile( - room.roomId, - space?.roomId, - userId, - evt.currentTarget.getBoundingClientRect() - ); + openProfileViewer(userId, room.roomId); }, - [room, space, openUserRoomProfile] + [room] ); const handleUsernameClick: MouseEventHandler = useCallback( (evt) => { @@ -997,7 +982,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli (reactions.find(eventWithShortcode)?.getContent().shortcode as string | undefined); mx.sendEvent( room.roomId, - MessageEvent.Reaction as any, + MessageEvent.Reaction, getReactionContent(targetEventId, key, rShortcode) ); }, @@ -1032,6 +1017,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent()) as GetContentCallback; const senderId = mEvent.getSender() ?? ''; + const senderPowerLevel = getPowerLevel(mEvent.getSender()); const senderDisplayName = getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; @@ -1065,8 +1051,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli replyEventId={replyEventId} threadRootId={threadRootId} onClick={handleOpenReply} - getMemberPowerTag={getMemberPowerTag} - accessibleTagColors={accessiblePowerTagColors} + getPowerLevel={getPowerLevel} + getPowerLevelTag={getPowerLevelTag} + accessibleTagColors={accessibleTagColors} legacyUsernameColor={legacyUsernameColor || direct} /> ) @@ -1085,8 +1072,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli } hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools} - memberPowerTag={getMemberPowerTag(senderId)} - accessibleTagColors={accessiblePowerTagColors} + powerLevelTag={getPowerLevelTag(senderPowerLevel)} + accessibleTagColors={accessibleTagColors} legacyUsernameColor={legacyUsernameColor || direct} hour24Clock={hour24Clock} dateFormatString={dateFormatString} @@ -1116,6 +1103,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const hasReactions = reactions && reactions.length > 0; const { replyEventId, threadRootId } = mEvent; const highlighted = focusItem?.index === item && focusItem.highlight; + const senderPowerLevel = getPowerLevel(mEvent.getSender()); return ( ) @@ -1167,8 +1156,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli } hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools} - memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')} - accessibleTagColors={accessiblePowerTagColors} + powerLevelTag={getPowerLevelTag(senderPowerLevel)} + accessibleTagColors={accessibleTagColors} legacyUsernameColor={legacyUsernameColor || direct} hour24Clock={hour24Clock} dateFormatString={dateFormatString} @@ -1235,6 +1224,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey(); const hasReactions = reactions && reactions.length > 0; const highlighted = focusItem?.index === item && focusItem.highlight; + const senderPowerLevel = getPowerLevel(mEvent.getSender()); return ( { @@ -70,10 +70,15 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) { const tombstoneEvent = useStateEvent(room, StateEvent.RoomTombstone); const powerLevels = usePowerLevelsContext(); - const creators = useRoomCreators(room); + const { getPowerLevel, canSendEvent } = usePowerLevelsAPI(powerLevels); + const myUserId = mx.getUserId(); + const canMessage = myUserId + ? canSendEvent(EventType.RoomMessage, getPowerLevel(myUserId)) + : false; - const permissions = useRoomPermissions(creators, powerLevels); - const canMessage = permissions.event(EventType.RoomMessage, mx.getSafeUserId()); + const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); + const theme = useTheme(); + const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags); useKeyDown( window, @@ -104,6 +109,8 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) { eventId={eventId} roomInputRef={roomInputRef} editor={editor} + getPowerLevelTag={getPowerLevelTag} + accessibleTagColors={accessibleTagColors} />
    @@ -124,6 +131,8 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) { roomId={roomId} fileDropContainerRef={roomViewRef} ref={roomInputRef} + getPowerLevelTag={getPowerLevelTag} + accessibleTagColors={accessibleTagColors} /> )} {!canMessage && ( diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 291c21c..d8e2e8b 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -42,7 +42,7 @@ import { getCanonicalAliasOrRoomId, isRoomAlias, mxcUrlToHttp } from '../../util import { _SearchPathSearchParams } from '../../pages/paths'; import * as css from './RoomViewHeader.css'; import { useRoomUnread } from '../../state/hooks/unread'; -import { usePowerLevelsContext } from '../../hooks/usePowerLevels'; +import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels'; import { markAsRead } from '../../../client/action/notifications'; import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { openInviteUser } from '../../../client/action/navigation'; @@ -67,8 +67,6 @@ import { } from '../../hooks/useRoomsNotificationPreferences'; import { JumpToTime } from './jump-to-time'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; -import { useRoomCreators } from '../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../hooks/useRoomPermissions'; type RoomMenuProps = { room: Room; @@ -79,10 +77,8 @@ const RoomMenu = forwardRef(({ room, requestClose const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const powerLevels = usePowerLevelsContext(); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - const canInvite = permissions.action('invite', mx.getSafeUserId()); + const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); + const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const notificationPreferences = useRoomsNotificationPreferencesContext(); const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId); const { navigateRoom } = useRoomNavigate(); diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index fbe3577..e906a02 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -75,10 +75,10 @@ import { getMatrixToRoomEvent } from '../../../plugins/matrix-to'; import { getViaServers } from '../../../plugins/via-servers'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents'; -import { MemberPowerTag, StateEvent } from '../../../../types/matrix/room'; +import { StateEvent } from '../../../../types/matrix/room'; +import { getTagIconSrc, PowerLevelTag } from '../../../hooks/usePowerLevelTags'; import { PowerIcon } from '../../../components/power'; import colorMXID from '../../../../util/colorMXID'; -import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag'; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void; @@ -371,7 +371,7 @@ export const MessagePinItem = as< if (!isPinned && eventId) { pinContent.pinned.push(eventId); } - mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents as any, pinContent); + mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents, pinContent); onClose?.(); }; @@ -679,7 +679,7 @@ export type MessageProps = { reactions?: ReactNode; hideReadReceipts?: boolean; showDeveloperTools?: boolean; - memberPowerTag?: MemberPowerTag; + powerLevelTag?: PowerLevelTag; accessibleTagColors?: Map; legacyUsernameColor?: boolean; hour24Clock: boolean; @@ -710,7 +710,7 @@ export const Message = as<'div', MessageProps>( reactions, hideReadReceipts, showDeveloperTools, - memberPowerTag, + powerLevelTag, accessibleTagColors, legacyUsernameColor, hour24Clock, @@ -733,11 +733,11 @@ export const Message = as<'div', MessageProps>( getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; const senderAvatarMxc = getMemberAvatarMxc(room, senderId); - const tagColor = memberPowerTag?.color - ? accessibleTagColors?.get(memberPowerTag.color) + const tagColor = powerLevelTag?.color + ? accessibleTagColors?.get(powerLevelTag.color) : undefined; - const tagIconSrc = memberPowerTag?.icon - ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon) + const tagIconSrc = powerLevelTag?.icon + ? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon) : undefined; const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor; diff --git a/src/app/features/room/reaction-viewer/ReactionViewer.tsx b/src/app/features/room/reaction-viewer/ReactionViewer.tsx index 6c686bc..d4b3984 100644 --- a/src/app/features/room/reaction-viewer/ReactionViewer.tsx +++ b/src/app/features/room/reaction-viewer/ReactionViewer.tsx @@ -20,14 +20,12 @@ import { getMemberDisplayName } from '../../../utils/room'; import { eventWithShortcode, getMxIdLocalPart } from '../../../utils/matrix'; import * as css from './ReactionViewer.css'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { openProfileViewer } from '../../../../client/action/navigation'; import { useRelations } from '../../../hooks/useRelations'; import { Reaction } from '../../../components/message'; import { getHexcodeForEmoji, getShortcodeFor } from '../../../plugins/emoji'; import { UserAvatar } from '../../../components/user-avatar'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; -import { useOpenUserRoomProfile } from '../../../state/hooks/userRoomProfile'; -import { useSpaceOptionally } from '../../../hooks/useSpace'; -import { getMouseEventCords } from '../../../utils/dom'; export type ReactionViewerProps = { room: Room; @@ -43,8 +41,6 @@ export const ReactionViewer = as<'div', ReactionViewerProps>( relations, useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], []) ); - const space = useSpaceOptionally(); - const openProfile = useOpenUserRoomProfile(); const [selectedKey, setSelectedKey] = useState(() => { if (initialKey) return initialKey; @@ -115,31 +111,24 @@ export const ReactionViewer = as<'div', ReactionViewerProps>( const name = (member ? getName(member) : getMxIdLocalPart(senderId)) ?? senderId; const avatarMxcUrl = member?.getMxcAvatarUrl(); - const avatarUrl = avatarMxcUrl - ? mx.mxcUrlToHttp( - avatarMxcUrl, - 100, - 100, - 'crop', - undefined, - false, - useAuthentication - ) - : undefined; + const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp( + avatarMxcUrl, + 100, + 100, + 'crop', + undefined, + false, + useAuthentication + ) : undefined; return ( { - openProfile( - room.roomId, - space?.roomId, - senderId, - getMouseEventCords(event.nativeEvent), - 'Bottom' - ); + onClick={() => { + requestClose(); + openProfileViewer(senderId, room.roomId); }} before={ diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx index 9986849..8e73e66 100644 --- a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx @@ -69,23 +69,18 @@ import { Image } from '../../../components/media'; import { ImageViewer } from '../../../components/image-viewer'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { VirtualTile } from '../../../components/virtualizer'; -import { usePowerLevelsContext } from '../../../hooks/usePowerLevels'; +import { usePowerLevelsAPI, usePowerLevelsContext } from '../../../hooks/usePowerLevels'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { ContainerColor } from '../../../styles/ContainerColor.css'; -import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags'; +import { + getTagIconSrc, + useAccessibleTagColors, + usePowerLevelTags, +} from '../../../hooks/usePowerLevelTags'; import { useTheme } from '../../../hooks/useTheme'; import { PowerIcon } from '../../../components/power'; import colorMXID from '../../../../util/colorMXID'; import { useIsDirectRoom } from '../../../hooks/useRoom'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; -import { - GetMemberPowerTag, - getPowerTagIconSrc, - useAccessiblePowerTagColors, - useGetMemberPowerTag, -} from '../../../hooks/useMemberPowerTag'; -import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag'; type PinnedMessageProps = { room: Room; @@ -93,27 +88,22 @@ type PinnedMessageProps = { renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>; onOpen: (roomId: string, eventId: string) => void; canPinEvent: boolean; - getMemberPowerTag: GetMemberPowerTag; - accessibleTagColors: Map; - legacyUsernameColor: boolean; - hour24Clock: boolean; - dateFormatString: string; }; -function PinnedMessage({ - room, - eventId, - renderContent, - onOpen, - canPinEvent, - getMemberPowerTag, - accessibleTagColors, - legacyUsernameColor, - hour24Clock, - dateFormatString, -}: PinnedMessageProps) { +function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: PinnedMessageProps) { const pinnedEvent = useRoomEvent(room, eventId); const useAuthentication = useMediaAuthentication(); const mx = useMatrixClient(); + const direct = useIsDirectRoom(); + const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); + + const powerLevels = usePowerLevelsContext(); + const { getPowerLevel } = usePowerLevelsAPI(powerLevels); + const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); + const theme = useTheme(); + const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags); + + const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); + const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); const [unpinState, unpin] = useAsyncCallback( useCallback(() => { @@ -179,15 +169,14 @@ function PinnedMessage({ const senderAvatarMxc = getMemberAvatarMxc(room, sender); const getContent = (() => pinnedEvent.getContent()) as GetContentCallback; - const memberPowerTag = getMemberPowerTag(sender); - const tagColor = memberPowerTag?.color - ? accessibleTagColors?.get(memberPowerTag.color) - : undefined; - const tagIconSrc = memberPowerTag?.icon - ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon) + const senderPowerLevel = getPowerLevel(sender); + const powerLevelTag = getPowerLevelTag(senderPowerLevel); + const tagColor = powerLevelTag?.color ? accessibleTagColors?.get(powerLevelTag.color) : undefined; + const tagIconSrc = powerLevelTag?.icon + ? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon) : undefined; - const usernameColor = legacyUsernameColor ? colorMXID(sender) : tagColor; + const usernameColor = legacyUsernameColor || direct ? colorMXID(sender) : tagColor; return ( @@ -252,34 +242,14 @@ export const RoomPinMenu = forwardRef( const mx = useMatrixClient(); const userId = mx.getUserId()!; const powerLevels = usePowerLevelsContext(); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, userId); - - const creatorsTag = useRoomCreatorsTag(); - const powerLevelTags = usePowerLevelTags(room, powerLevels); - const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels); - - const theme = useTheme(); - const accessibleTagColors = useAccessiblePowerTagColors( - theme.kind, - creatorsTag, - powerLevelTags - ); + const { canSendStateEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels); + const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, getPowerLevel(userId)); const pinnedEvents = useRoomPinnedEvents(room); const sortedPinnedEvent = useMemo(() => Array.from(pinnedEvents).reverse(), [pinnedEvents]); const useAuthentication = useMediaAuthentication(); const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); - - const direct = useIsDirectRoom(); - const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); - - const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); - const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); - const { navigateRoom } = useRoomNavigate(); const scrollRef = useRef(null); @@ -494,11 +464,6 @@ export const RoomPinMenu = forwardRef( renderContent={renderMatrixEvent} onOpen={handleOpen} canPinEvent={canPinEvent} - getMemberPowerTag={getMemberPowerTag} - accessibleTagColors={accessibleTagColors} - legacyUsernameColor={legacyUsernameColor || direct} - hour24Clock={hour24Clock} - dateFormatString={dateFormatString} />
    diff --git a/src/app/features/space-settings/SpaceSettingsRenderer.tsx b/src/app/features/space-settings/SpaceSettingsRenderer.tsx index 79947e0..085c5e2 100644 --- a/src/app/features/space-settings/SpaceSettingsRenderer.tsx +++ b/src/app/features/space-settings/SpaceSettingsRenderer.tsx @@ -16,7 +16,7 @@ function RenderSettings({ state }: RenderSettingsProps) { const allJoinedRooms = useAllJoinedRoomsSet(); const getRoom = useGetRoom(allJoinedRooms); const room = getRoom(roomId); - const space = spaceId && spaceId !== roomId ? getRoom(spaceId) : undefined; + const space = spaceId ? getRoom(spaceId) : undefined; if (!room) return null; diff --git a/src/app/features/space-settings/general/General.tsx b/src/app/features/space-settings/general/General.tsx index 641bfa7..6f4d8d3 100644 --- a/src/app/features/space-settings/general/General.tsx +++ b/src/app/features/space-settings/general/General.tsx @@ -11,8 +11,6 @@ import { RoomPublish, RoomUpgrade, } from '../../common-settings/general'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; type GeneralProps = { requestClose: () => void; @@ -20,8 +18,6 @@ type GeneralProps = { export function General({ requestClose }: GeneralProps) { const room = useRoom(); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - const permissions = useRoomPermissions(creators, powerLevels); return ( @@ -43,20 +39,20 @@ export function General({ requestClose }: GeneralProps) { - + Options - - + + Addresses - - + + Advance Options - + diff --git a/src/app/features/space-settings/permissions/Permissions.tsx b/src/app/features/space-settings/permissions/Permissions.tsx index 7572a71..ae3769b 100644 --- a/src/app/features/space-settings/permissions/Permissions.tsx +++ b/src/app/features/space-settings/permissions/Permissions.tsx @@ -2,13 +2,11 @@ import React, { useState } from 'react'; import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds'; import { Page, PageContent, PageHeader } from '../../../components/page'; import { useRoom } from '../../../hooks/useRoom'; -import { usePowerLevels } from '../../../hooks/usePowerLevels'; +import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { StateEvent } from '../../../../types/matrix/room'; import { usePermissionGroups } from './usePermissionItems'; import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; type PermissionsProps = { requestClose: () => void; @@ -17,12 +15,11 @@ export function Permissions({ requestClose }: PermissionsProps) { const mx = useMatrixClient(); const room = useRoom(); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - - const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId()); - const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId()); + const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); + const canEditPowers = canSendStateEvent( + StateEvent.PowerLevelTags, + getPowerLevel(mx.getSafeUserId()) + ); const permissionGroups = usePermissionGroups(); const [powerEditor, setPowerEditor] = useState(false); @@ -60,11 +57,7 @@ export function Permissions({ requestClose }: PermissionsProps) { onEdit={canEditPowers ? handleEditPowers : undefined} permissionGroups={permissionGroups} /> - + diff --git a/src/app/hooks/useDirectUsers.ts b/src/app/hooks/useDirectUsers.ts deleted file mode 100644 index 3aa1892..0000000 --- a/src/app/hooks/useDirectUsers.ts +++ /dev/null @@ -1,27 +0,0 @@ -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(); - - 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; -}; diff --git a/src/app/hooks/useMemberPowerCompare.ts b/src/app/hooks/useMemberPowerCompare.ts deleted file mode 100644 index 72163ed..0000000 --- a/src/app/hooks/useMemberPowerCompare.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCallback } from 'react'; -import { IPowerLevels, readPowerLevel } from './usePowerLevels'; - -export const useMemberPowerCompare = (creators: Set, 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, - }; -}; diff --git a/src/app/hooks/useMemberPowerTag.ts b/src/app/hooks/useMemberPowerTag.ts deleted file mode 100644 index 31e52aa..0000000 --- a/src/app/hooks/useMemberPowerTag.ts +++ /dev/null @@ -1,87 +0,0 @@ -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, - 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 => { - const accessibleColors: Map = useMemo(() => { - const colors: Map = 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 => { - const PLTagOrRoomMember = useMemo(() => { - let prevTag: MemberPowerTag | undefined; - const tagOrMember: Array = []; - 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; -}; diff --git a/src/app/hooks/useMemberSort.ts b/src/app/hooks/useMemberSort.ts index d8e403c..da95570 100644 --- a/src/app/hooks/useMemberSort.ts +++ b/src/app/hooks/useMemberSort.ts @@ -1,5 +1,5 @@ import { RoomMember } from 'matrix-js-sdk'; -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; export const MemberSort = { Ascending: (a: RoomMember, b: RoomMember) => @@ -46,20 +46,3 @@ export const useMemberSort = (index: number, memberSort: MemberSortItem[]): Memb const item = memberSort[index] ?? memberSort[0]; return item; }; - -export const useMemberPowerSort = (creators: Set): 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; -}; diff --git a/src/app/hooks/useMembership.ts b/src/app/hooks/useMembership.ts deleted file mode 100644 index dbdd527..0000000 --- a/src/app/hooks/useMembership.ts +++ /dev/null @@ -1,28 +0,0 @@ -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( - () => (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; -}; diff --git a/src/app/hooks/useMentionClickHandler.ts b/src/app/hooks/useMentionClickHandler.ts index 9dc81d4..49c291c 100644 --- a/src/app/hooks/useMentionClickHandler.ts +++ b/src/app/hooks/useMentionClickHandler.ts @@ -3,17 +3,14 @@ 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 => { const mx = useMatrixClient(); const { navigateRoom, navigateSpace } = useRoomNavigate(); const navigate = useNavigate(); - const openProfile = useOpenUserRoomProfile(); - const space = useSpaceOptionally(); const handleClick: ReactEventHandler = useCallback( (evt) => { @@ -24,7 +21,7 @@ export const useMentionClickHandler = (roomId: string): ReactEventHandler(path, { viaServers }) : path); }, - [mx, navigate, navigateRoom, navigateSpace, roomId, space, openProfile] + [mx, navigate, navigateRoom, navigateSpace, roomId] ); return handleClick; diff --git a/src/app/hooks/useMutualRooms.ts b/src/app/hooks/useMutualRooms.ts deleted file mode 100644 index a7b3893..0000000 --- a/src/app/hooks/useMutualRooms.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 => { - const mx = useMatrixClient(); - - const supported = useMutualRoomsSupport(); - - const [mutualRoomsState] = useAsyncCallbackValue( - useCallback( - () => (supported ? mx._unstable_getSharedRooms(userId) : Promise.resolve([])), - [mx, userId, supported] - ) - ); - - return mutualRoomsState; -}; diff --git a/src/app/hooks/usePowerLevelTags.ts b/src/app/hooks/usePowerLevelTags.ts index 3933b42..1849265 100644 --- a/src/app/hooks/usePowerLevelTags.ts +++ b/src/app/hooks/usePowerLevelTags.ts @@ -1,24 +1,29 @@ -import { Room } from 'matrix-js-sdk'; -import { useMemo } from 'react'; +import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; +import { useCallback, useMemo } from 'react'; import { IPowerLevels } from './usePowerLevels'; import { useStateEvent } from './useStateEvent'; -import { MemberPowerTag, StateEvent } from '../../types/matrix/room'; +import { StateEvent } from '../../types/matrix/room'; +import { IImageInfo } from '../../types/matrix/common'; +import { ThemeKind } from './useTheme'; +import { accessibleColor } from '../plugins/color'; -export type PowerLevelTags = Record; +export type PowerLevelTagIcon = { + key?: string; + info?: IImageInfo; +}; +export type PowerLevelTag = { + name: string; + color?: string; + icon?: PowerLevelTagIcon; +}; -const powerSortFn = (a: number, b: number) => b - a; -const sortPowers = (powers: number[]): number[] => powers.sort(powerSortFn); +export type PowerLevelTags = Record; + +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) => { - const power = parseInt(p, 10); - if (Number.isNaN(power)) { - return undefined; - } - return power; - }) - .filter((power) => typeof power === 'number'); + const powers: number[] = Object.keys(tags).map((p) => parseInt(p, 10)); return sortPowers(powers); }; @@ -72,14 +77,14 @@ const DEFAULT_TAGS: PowerLevelTags = { }, }; -const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): MemberPowerTag => { +const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): PowerLevelTag => { return { name: `Team ${power}`, }; }; -export type GetPowerLevelTag = (powerLevel: number) => MemberPowerTag; +export type GetPowerLevelTag = (powerLevel: number) => PowerLevelTag; export const usePowerLevelTags = ( room: Room, @@ -101,13 +106,66 @@ export const usePowerLevelTags = ( return powerToTags; }, [powerLevels, tagsEvent]); - return powerLevelTags; + const getTag: GetPowerLevelTag = useCallback( + (power) => { + const tag: PowerLevelTag | undefined = powerLevelTags[power]; + return tag ?? generateFallbackTag(DEFAULT_TAGS, power); + }, + [powerLevelTags] + ); + + return [powerLevelTags, getTag]; }; -export const getPowerLevelTag = ( - powerLevelTags: PowerLevelTags, - powerLevel: number -): MemberPowerTag => { - const tag: MemberPowerTag | undefined = powerLevelTags[powerLevel]; - return tag ?? generateFallbackTag(powerLevelTags, powerLevel); +export const useFlattenPowerLevelTagMembers = ( + members: RoomMember[], + getPowerLevel: (userId: string) => number, + getTag: GetPowerLevelTag +): Array => { + const PLTagOrRoomMember = useMemo(() => { + let prevTag: PowerLevelTag | undefined; + const tagOrMember: Array = []; + 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 => { + const accessibleColors: Map = useMemo(() => { + const colors: Map = 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; }; diff --git a/src/app/hooks/usePowerLevels.ts b/src/app/hooks/usePowerLevels.ts index 0281b23..8bf8b74 100644 --- a/src/app/hooks/usePowerLevels.ts +++ b/src/app/hooks/usePowerLevels.ts @@ -58,11 +58,10 @@ const fillMissingPowers = (powerLevels: IPowerLevels): IPowerLevels => }); const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => { - const plContent = mEvent?.getContent(); + const pl = mEvent?.getContent(); + if (!pl) return DEFAULT_POWER_LEVELS; - const powerLevels = !plContent ? DEFAULT_POWER_LEVELS : fillMissingPowers(plContent); - - return powerLevels; + return fillMissingPowers(pl); }; export function usePowerLevels(room: Room): IPowerLevels { @@ -121,8 +120,33 @@ export const useRoomsPowerLevels = (rooms: Room[]): Map => 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: (powerLevels: IPowerLevels, userId: string | undefined) => number; + user: GetPowerLevel; event: (powerLevels: IPowerLevels, eventType: string | undefined) => number; state: (powerLevels: IPowerLevels, eventType: string | undefined) => number; action: (powerLevels: IPowerLevels, action: PowerLevelActions) => number; @@ -132,7 +156,6 @@ 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]; } @@ -168,13 +191,63 @@ export const readPowerLevel: ReadPowerLevelAPI = { }, }; -export const useGetMemberPowerLevel = (powerLevels: IPowerLevels) => { - const callback = useCallback( - (userId?: string): number => readPowerLevel.user(powerLevels, userId), +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] ); - return callback; + 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, + }; }; /** diff --git a/src/app/hooks/useRoomCreators.ts b/src/app/hooks/useRoomCreators.ts deleted file mode 100644 index 269d11a..0000000 --- a/src/app/hooks/useRoomCreators.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 => { - const createContent = createEvent.getContent(); - - const creators: Set = 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 => { - const createEvent = useStateEvent(room, StateEvent.RoomCreate); - - const creators = useMemo( - () => (createEvent ? getRoomCreators(createEvent) : new Set()), - [createEvent] - ); - - return creators; -}; - -export const getRoomCreatorsForRoomId = (mx: MatrixClient, roomId: string): Set => { - const room = mx.getRoom(roomId); - if (!room) return new Set(); - - const createEvent = getStateEvent(room, StateEvent.RoomCreate); - if (!createEvent) return new Set(); - - return getRoomCreators(createEvent); -}; diff --git a/src/app/hooks/useRoomCreatorsTag.ts b/src/app/hooks/useRoomCreatorsTag.ts deleted file mode 100644 index 2d6db0e..0000000 --- a/src/app/hooks/useRoomCreatorsTag.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MemberPowerTag } from '../../types/matrix/room'; - -const DEFAULT_TAG: MemberPowerTag = { - name: 'Founder', - color: '#0000ff', -}; - -export const useRoomCreatorsTag = (): MemberPowerTag => DEFAULT_TAG; diff --git a/src/app/hooks/useRoomPermissions.ts b/src/app/hooks/useRoomPermissions.ts deleted file mode 100644 index cb6f69a..0000000 --- a/src/app/hooks/useRoomPermissions.ts +++ /dev/null @@ -1,60 +0,0 @@ -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, - 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, - powerLevels: IPowerLevels -): RoomPermissionsAPI => { - const api: RoomPermissionsAPI = useMemo( - () => getRoomPermissionsAPI(creators, powerLevels), - [creators, powerLevels] - ); - - return api; -}; diff --git a/src/app/hooks/useUserPresence.ts b/src/app/hooks/useUserPresence.ts deleted file mode 100644 index 5137950..0000000 --- a/src/app/hooks/useUserPresence.ts +++ /dev/null @@ -1,58 +0,0 @@ -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 => - useMemo( - () => ({ - [Presence.Online]: 'Active', - [Presence.Unavailable]: 'Busy', - [Presence.Offline]: 'Away', - }), - [] - ); diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index 247885c..d6d93aa 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -62,7 +62,6 @@ import { AutoRestoreBackupOnVerification } from '../components/BackupRestore'; import { RoomSettingsRenderer } from '../features/room-settings'; import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences'; import { SpaceSettingsRenderer } from '../features/space-settings'; -import { UserRoomProfileRenderer } from '../components/UserRoomProfileRenderer'; import { CreateRoomModalRenderer } from '../features/create-room'; import { HomeCreateRoom } from './client/home/CreateRoom'; import { Create } from './client/create'; @@ -131,7 +130,6 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) > - diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx index 2597bb7..d233391 100644 --- a/src/app/pages/client/home/Home.tsx +++ b/src/app/pages/client/home/Home.tsx @@ -30,12 +30,10 @@ import { NavLink, } from '../../../components/nav'; import { - encodeSearchParamValueArray, getExplorePath, getHomeCreatePath, getHomeRoomPath, getHomeSearchPath, - withSearchParam, } from '../../pathUtils'; import { getCanonicalAliasOrRoomId } from '../../../utils/matrix'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; @@ -51,6 +49,7 @@ import { makeNavCategoryId } from '../../../state/closedNavCategories'; import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; import { useCategoryHandler } from '../../../hooks/useCategoryHandler'; import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper'; +import { openJoinAlias } from '../../../../client/action/navigation'; import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page'; import { useRoomsUnread } from '../../../state/hooks/unread'; import { markAsRead } from '../../../../client/action/notifications'; @@ -62,9 +61,6 @@ import { getRoomNotificationMode, useRoomsNotificationPreferencesContext, } from '../../../hooks/useRoomsNotificationPreferences'; -import { UseStateProvider } from '../../../components/UseStateProvider'; -import { JoinAddressPrompt } from '../../../components/join-address-prompt'; -import { _RoomSearchParams } from '../../paths'; type HomeMenuProps = { requestClose: () => void; @@ -81,6 +77,11 @@ const HomeMenu = forwardRef(({ requestClose }, re requestClose(); }; + const handleJoinAddress = () => { + openJoinAlias(); + requestClose(); + }; + return ( @@ -95,6 +96,16 @@ const HomeMenu = forwardRef(({ requestClose }, re Mark as Read + } + > + + Join with Address + + ); @@ -257,44 +268,22 @@ export function Home() { - - {(open, setOpen) => ( - <> - - setOpen(true)}> - - - - - - - - Join with Address - - - - - - - {open && ( - setOpen(false)} - onOpen={(roomIdOrAlias, viaServers, eventId) => { - setOpen(false); - const path = getHomeRoomPath(roomIdOrAlias, eventId); - navigate( - viaServers - ? withSearchParam<_RoomSearchParams>(path, { - viaServers: encodeSearchParamValueArray(viaServers), - }) - : path - ); - }} - /> - )} - - )} - + + openJoinAlias()}> + + + + + + + + Join with Address + + + + + + diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx index afdfec6..a495774 100644 --- a/src/app/pages/client/inbox/Notifications.tsx +++ b/src/app/pages/client/inbox/Notifications.tsx @@ -84,19 +84,16 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { allRoomsAtom } from '../../../state/room-list/roomList'; -import { usePowerLevels } from '../../../hooks/usePowerLevels'; -import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags'; +import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; +import { + getTagIconSrc, + useAccessibleTagColors, + usePowerLevelTags, +} from '../../../hooks/usePowerLevelTags'; import { useTheme } from '../../../hooks/useTheme'; import { PowerIcon } from '../../../components/power'; import colorMXID from '../../../../util/colorMXID'; import { mDirectAtom } from '../../../state/mDirectList'; -import { - getPowerTagIconSrc, - useAccessiblePowerTagColors, - useGetMemberPowerTag, -} from '../../../hooks/useMemberPowerTag'; -import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; type RoomNotificationsGroup = { roomId: string; @@ -227,14 +224,10 @@ function RoomNotificationsGroupComp({ const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const creatorsTag = useRoomCreatorsTag(); - const powerLevelTags = usePowerLevelTags(room, powerLevels); - const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels); - + const { getPowerLevel } = usePowerLevelsAPI(powerLevels); + const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); const theme = useTheme(); - const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags); + const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags); const mentionClickHandler = useMentionClickHandler(room.roomId); const spoilerClickHandler = useSpoilerClickHandler(); @@ -454,12 +447,13 @@ function RoomNotificationsGroupComp({ const threadRootId = relation?.rel_type === RelationType.Thread ? relation.event_id : undefined; - const memberPowerTag = getMemberPowerTag(event.sender); - const tagColor = memberPowerTag?.color - ? accessibleTagColors?.get(memberPowerTag.color) + const senderPowerLevel = getPowerLevel(event.sender); + const powerLevelTag = getPowerLevelTag(senderPowerLevel); + const tagColor = powerLevelTag?.color + ? accessibleTagColors?.get(powerLevelTag.color) : undefined; - const tagIconSrc = memberPowerTag?.icon - ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon) + const tagIconSrc = powerLevelTag?.icon + ? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon) : undefined; const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor; @@ -529,7 +523,8 @@ function RoomNotificationsGroupComp({ replyEventId={replyEventId} threadRootId={threadRootId} onClick={handleOpenClick} - getMemberPowerTag={getMemberPowerTag} + getPowerLevel={getPowerLevel} + getPowerLevelTag={getPowerLevelTag} accessibleTagColors={accessibleTagColors} legacyUsernameColor={legacyUsernameColor} /> diff --git a/src/app/pages/client/sidebar/CreateTab.tsx b/src/app/pages/client/sidebar/CreateTab.tsx index e6575cb..a7f9350 100644 --- a/src/app/pages/client/sidebar/CreateTab.tsx +++ b/src/app/pages/client/sidebar/CreateTab.tsx @@ -7,22 +7,15 @@ import { stopPropagation } from '../../../utils/keyboard'; import { SequenceCard } from '../../../components/sequence-card'; import { SettingTile } from '../../../components/setting-tile'; import { ContainerColor } from '../../../styles/ContainerColor.css'; -import { - encodeSearchParamValueArray, - getCreatePath, - getSpacePath, - withSearchParam, -} from '../../pathUtils'; +import { openJoinAlias } from '../../../../client/action/navigation'; +import { getCreatePath } from '../../pathUtils'; import { useCreateSelected } from '../../../hooks/router/useCreateSelected'; -import { JoinAddressPrompt } from '../../../components/join-address-prompt'; -import { _RoomSearchParams } from '../../paths'; export function CreateTab() { const createSelected = useCreateSelected(); const navigate = useNavigate(); const [menuCords, setMenuCords] = useState(); - const [joinAddress, setJoinAddress] = useState(false); const handleMenu: MouseEventHandler = (evt) => { setMenuCords(menuCords ? undefined : evt.currentTarget.getBoundingClientRect()); @@ -34,7 +27,7 @@ export function CreateTab() { }; const handleJoinWithAddress = () => { - setJoinAddress(true); + openJoinAlias(); setMenuCords(undefined); }; @@ -110,22 +103,6 @@ export function CreateTab() { > - {joinAddress && ( - setJoinAddress(false)} - onOpen={(roomIdOrAlias, viaServers) => { - setJoinAddress(false); - const path = getSpacePath(roomIdOrAlias); - navigate( - viaServers - ? withSearchParam<_RoomSearchParams>(path, { - viaServers: encodeSearchParamValueArray(viaServers), - }) - : path - ); - }} - /> - )} )} diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 3ee6c72..011741e 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -77,7 +77,7 @@ import { AccountDataEvent } from '../../../../types/matrix/accountData'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath'; import { useOpenedSidebarFolderAtom } from '../../../state/hooks/openedSidebarFolder'; -import { usePowerLevels } from '../../../hooks/usePowerLevels'; +import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { useRoomsUnread } from '../../../state/hooks/unread'; import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; import { markAsRead } from '../../../../client/action/notifications'; @@ -91,8 +91,6 @@ import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; type SpaceMenuProps = { room: Room; @@ -105,10 +103,8 @@ const SpaceMenu = forwardRef( const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const roomToParents = useAtomValue(roomToParentsAtom); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - const canInvite = permissions.action('invite', mx.getSafeUserId()); + const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); + const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const openSpaceSettings = useOpenSpaceSettings(); const allChild = useSpaceChildren( diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index a335a0a..d100946 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -10,7 +10,6 @@ import { useAtom, useAtomValue } from 'jotai'; import { Avatar, Box, - Button, Icon, IconButton, Icons, @@ -19,9 +18,7 @@ import { MenuItem, PopOut, RectCords, - Spinner, Text, - color, config, toRem, } from 'folds'; @@ -56,7 +53,7 @@ import { useRoomName } from '../../../hooks/useRoomMeta'; import { useSpaceJoinedHierarchy } from '../../../hooks/useSpaceHierarchy'; import { allRoomsAtom } from '../../../state/room-list/roomList'; import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page'; -import { usePowerLevels } from '../../../hooks/usePowerLevels'; +import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { openInviteUser } from '../../../../client/action/navigation'; import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList'; import { roomToParentsAtom } from '../../../state/room/roomToParents'; @@ -67,7 +64,7 @@ import { LeaveSpacePrompt } from '../../../components/leave-space-prompt'; import { copyToClipboard } from '../../../utils/dom'; import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories'; import { useStateEvent } from '../../../hooks/useStateEvent'; -import { Membership, StateEvent } from '../../../../types/matrix/room'; +import { StateEvent } from '../../../../types/matrix/room'; import { stopPropagation } from '../../../utils/keyboard'; import { getMatrixToRoom } from '../../../plugins/matrix-to'; import { getViaServers } from '../../../plugins/via-servers'; @@ -79,11 +76,6 @@ import { } from '../../../hooks/useRoomsNotificationPreferences'; import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; -import { useRoomCreators } from '../../../hooks/useRoomCreators'; -import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; -import { ContainerColor } from '../../../styles/ContainerColor.css'; -import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; -import { BreakWord } from '../../../styles/Text.css'; type SpaceMenuProps = { room: Room; @@ -95,10 +87,8 @@ const SpaceMenu = forwardRef(({ room, requestClo const [developerTools] = useSetting(settingsAtom, 'developerTools'); const roomToParents = useAtomValue(roomToParentsAtom); const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - - const permissions = useRoomPermissions(creators, powerLevels); - const canInvite = permissions.action('invite', mx.getSafeUserId()); + const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); + const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const openSpaceSettings = useOpenSpaceSettings(); const { navigateRoom } = useRoomNavigate(); @@ -294,75 +284,6 @@ function SpaceHeader() { ); } -type SpaceTombstoneProps = { roomId: string; replacementRoomId: string }; -export function SpaceTombstone({ roomId, replacementRoomId }: SpaceTombstoneProps) { - const mx = useMatrixClient(); - const { navigateSpace } = useRoomNavigate(); - - const [joinState, handleJoin] = useAsyncCallback( - useCallback(() => { - const currentRoom = mx.getRoom(roomId); - const via = currentRoom ? getViaServers(currentRoom) : []; - return mx.joinRoom(replacementRoomId, { - viaServers: via, - }); - }, [mx, roomId, replacementRoomId]) - ); - const replacementRoom = mx.getRoom(replacementRoomId); - - const handleOpen = () => { - if (replacementRoom) navigateSpace(replacementRoom.roomId); - if (joinState.status === AsyncStatus.Success) navigateSpace(joinState.data.roomId); - }; - - return ( - - - Space Upgraded - This space has been replaced and is no longer active. - {joinState.status === AsyncStatus.Error && ( - - {(joinState.error as any)?.message ?? 'Failed to join replacement space!'} - - )} - - - {replacementRoom?.getMyMembership() === Membership.Join || - joinState.status === AsyncStatus.Success ? ( - - ) : ( - - )} - - - ); -} - export function Space() { const mx = useMatrixClient(); const space = useSpace(); @@ -375,8 +296,6 @@ export function Space() { const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); const notificationPreferences = useRoomsNotificationPreferencesContext(); - const tombstoneEvent = useStateEvent(space, StateEvent.RoomTombstone); - const selectedRoomId = useSelectedRoom(); const lobbySelected = useSpaceLobbySelected(spaceIdOrAlias); const searchSelected = useSpaceSearchSelected(spaceIdOrAlias); @@ -432,12 +351,6 @@ export function Space() { - {tombstoneEvent && ( - - )} diff --git a/src/app/plugins/matrix-to.ts b/src/app/plugins/matrix-to.ts index feeafe0..c9df0a8 100644 --- a/src/app/plugins/matrix-to.ts +++ b/src/app/plugins/matrix-to.ts @@ -42,9 +42,9 @@ const MATRIX_TO = /^https?:\/\/matrix\.to\S*$/; export const testMatrixTo = (href: string): boolean => MATRIX_TO.test(href); const MATRIX_TO_USER = /^https?:\/\/matrix\.to\/#\/(@[^:\s]+:[^?/\s]+)\/?$/; -const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^?/\s]+)\/?(\?[\S]*)?$/; +const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/?(\?[\S]*)?$/; const MATRIX_TO_ROOM_EVENT = - /^https?:\/\/matrix\.to\/#\/([#!][^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/; + /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/; export const parseMatrixToUser = (href: string): string | undefined => { const match = href.match(MATRIX_TO_USER); diff --git a/src/app/plugins/via-servers.ts b/src/app/plugins/via-servers.ts index d825a1f..7547099 100644 --- a/src/app/plugins/via-servers.ts +++ b/src/app/plugins/via-servers.ts @@ -1,19 +1,11 @@ import { Room } from 'matrix-js-sdk'; import { IPowerLevels } from '../hooks/usePowerLevels'; -import { creatorsSupported, getMxIdServer } from '../utils/matrix'; -import { IRoomCreateContent, StateEvent } from '../../types/matrix/room'; +import { getMxIdServer } from '../utils/matrix'; +import { StateEvent } from '../../types/matrix/room'; import { getStateEvent } from '../utils/room'; export const getViaServers = (room: Room): string[] => { const getHighestPowerUserId = (): string | undefined => { - const creatorEvent = getStateEvent(room, StateEvent.RoomCreate); - if ( - creatorEvent && - creatorsSupported(creatorEvent.getContent().room_version) - ) { - return creatorEvent.getSender(); - } - const powerLevels = getStateEvent(room, StateEvent.RoomPowerLevels)?.getContent(); if (!powerLevels) return undefined; diff --git a/src/app/state/hooks/userRoomProfile.ts b/src/app/state/hooks/userRoomProfile.ts deleted file mode 100644 index 7fed0c0..0000000 --- a/src/app/state/hooks/userRoomProfile.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useCallback } from 'react'; -import { useAtomValue, useSetAtom } from 'jotai'; -import { Position, RectCords } from 'folds'; -import { userRoomProfileAtom, UserRoomProfileState } from '../userRoomProfile'; - -export const useUserRoomProfileState = (): UserRoomProfileState | undefined => { - const data = useAtomValue(userRoomProfileAtom); - - return data; -}; - -type CloseCallback = () => void; -export const useCloseUserRoomProfile = (): CloseCallback => { - const setUserRoomProfile = useSetAtom(userRoomProfileAtom); - - const close: CloseCallback = useCallback(() => { - setUserRoomProfile(undefined); - }, [setUserRoomProfile]); - - return close; -}; - -type OpenCallback = ( - roomId: string, - spaceId: string | undefined, - userId: string, - cords: RectCords, - position?: Position -) => void; -export const useOpenUserRoomProfile = (): OpenCallback => { - const setUserRoomProfile = useSetAtom(userRoomProfileAtom); - - const open: OpenCallback = useCallback( - (roomId, spaceId, userId, cords, position) => { - setUserRoomProfile({ roomId, spaceId, userId, cords, position }); - }, - [setUserRoomProfile] - ); - - return open; -}; diff --git a/src/app/state/userRoomProfile.ts b/src/app/state/userRoomProfile.ts deleted file mode 100644 index cf4e403..0000000 --- a/src/app/state/userRoomProfile.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Position, RectCords } from 'folds'; -import { atom } from 'jotai'; - -export type UserRoomProfileState = { - userId: string; - roomId: string; - spaceId?: string; - cords: RectCords; - position?: Position; -}; - -export const userRoomProfileAtom = atom(undefined); diff --git a/src/app/utils/dom.ts b/src/app/utils/dom.ts index 80db3ae..f4c3f71 100644 --- a/src/app/utils/dom.ts +++ b/src/app/utils/dom.ts @@ -43,17 +43,6 @@ export const canFitInScrollView = ( export type FilesOrFile = T extends true ? File[] : File; -export const getFilesFromFileList = (fileList: FileList): File[] => { - const files: File[] = []; - - for (let i = 0; i < fileList.length; i += 1) { - const file: File | undefined = fileList[i]; - if (file instanceof File) files.push(file); - } - - return files; -}; - export const selectFile = ( accept: string, multiple?: M @@ -69,7 +58,7 @@ export const selectFile = ( if (!fileList) { resolve(undefined); } else { - const files: File[] = getFilesFromFileList(fileList); + const files: File[] = [...fileList].filter((file) => file); resolve((multiple ? files : files[0]) as FilesOrFile); } input.removeEventListener('change', changeHandler); @@ -81,7 +70,7 @@ export const selectFile = ( export const getDataTransferFiles = (dataTransfer: DataTransfer): File[] | undefined => { const fileList = dataTransfer.files; - const files: File[] = getFilesFromFileList(fileList); + const files = [...fileList].filter((file) => file); if (files.length === 0) return undefined; return files; }; @@ -235,10 +224,3 @@ export const notificationPermission = (permission: NotificationPermission) => { } return false; }; - -export const getMouseEventCords = (event: MouseEvent) => ({ - x: event.clientX, - y: event.clientY, - width: 0, - height: 0, -}); diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index a803120..b31677a 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -17,13 +17,13 @@ import to from 'await-to-js'; import { IImageInfo, IThumbnailContent, IVideoInfo } from '../../types/matrix/common'; import { AccountDataEvent } from '../../types/matrix/accountData'; import { getStateEvent } from './room'; -import { Membership, StateEvent } from '../../types/matrix/room'; +import { StateEvent } from '../../types/matrix/room'; const DOMAIN_REGEX = /\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b/; export const isServerName = (serverName: string): boolean => DOMAIN_REGEX.test(serverName); -const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@$+#])([^\s:]+):(\S+)$/); +const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@$+#])(.+):(\S+)$/); const validMxId = (id: string): boolean => !!matchMxId(id); @@ -182,12 +182,7 @@ export const eventWithShortcode = (ev: MatrixEvent) => export const getDMRoomFor = (mx: MatrixClient, userId: string): Room | undefined => { const dmLikeRooms = mx .getRooms() - .filter( - (room) => - room.getMyMembership() === Membership.Join && - room.hasEncryptionStateEvent() && - room.getMembers().length <= 2 - ); + .filter((room) => room.hasEncryptionStateEvent() && room.getMembers().length <= 2); return dmLikeRooms.find((room) => room.getMember(userId)); }; @@ -362,7 +357,3 @@ export const knockRestrictedSupported = (version: string): boolean => { const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; return !unsupportedVersion.includes(version); }; -export const creatorsSupported = (version: string): boolean => { - const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; - return !unsupportedVersion.includes(version); -}; diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index b4bba2a..a962c45 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -20,7 +20,6 @@ import { import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; import { AccountDataEvent } from '../../types/matrix/accountData'; import { - IRoomCreateContent, Membership, MessageEvent, NotificationType, @@ -44,7 +43,7 @@ export const getStateEvents = (room: Room, eventType: StateEvent): MatrixEvent[] export const getAccountData = ( mx: MatrixClient, eventType: AccountDataEvent -): MatrixEvent | undefined => mx.getAccountData(eventType as any); +): MatrixEvent | undefined => mx.getAccountData(eventType); export const getMDirects = (mDirectEvent: MatrixEvent): Set => { const roomIds = new Set(); @@ -481,23 +480,6 @@ export const bannedInRooms = (mx: MatrixClient, rooms: string[], otherUserId: st return banned; }); -export const getAllVersionsRoomCreator = (room: Room): Set => { - const creators = new Set(); - - const createEvent = getStateEvent(room, StateEvent.RoomCreate); - const createContent = createEvent?.getContent(); - const creator = createEvent?.getSender(); - if (typeof creator === 'string') creators.add(creator); - - if (createContent && Array.isArray(createContent.additional_creators)) { - createContent.additional_creators.forEach((c) => { - if (typeof c === 'string') creators.add(c); - }); - } - - return creators; -}; - export const guessPerfectParent = ( mx: MatrixClient, roomId: string, @@ -508,29 +490,15 @@ export const guessPerfectParent = ( } const getSpecialUsers = (rId: string): string[] => { - const specialUsers: Set = new Set(); - const r = mx.getRoom(rId); - if (!r) return []; - - getAllVersionsRoomCreator(r).forEach((c) => specialUsers.add(c)); - - const powerLevels = getStateEvent( - r, - StateEvent.RoomPowerLevels - )?.getContent(); + const powerLevels = + r && getStateEvent(r, StateEvent.RoomPowerLevels)?.getContent(); const { users_default: usersDefault, users } = powerLevels ?? {}; + if (typeof users !== 'object') return []; + const defaultPower = typeof usersDefault === 'number' ? usersDefault : 0; - - if (typeof users === 'object') - Object.keys(users).forEach((userId) => { - if (users[userId] > defaultPower) { - specialUsers.add(userId); - } - }); - - return Array.from(specialUsers); + return Object.keys(users).filter((userId) => users[userId] > defaultPower); }; let perfectParent: string | undefined; diff --git a/src/types/matrix/accountData.ts b/src/types/matrix/accountData.ts index 9871599..20ce941 100644 --- a/src/types/matrix/accountData.ts +++ b/src/types/matrix/accountData.ts @@ -18,8 +18,6 @@ export enum AccountDataEvent { MegolmBackupV1 = 'm.megolm_backup.v1', } -export type MDirectContent = Record; - export type SecretStorageDefaultKeyContent = { key: string; }; diff --git a/src/types/matrix/room.ts b/src/types/matrix/room.ts index b866fd7..65dc35f 100644 --- a/src/types/matrix/room.ts +++ b/src/types/matrix/room.ts @@ -1,5 +1,3 @@ -import { IImageInfo } from './common'; - export enum Membership { Invite = 'invite', Knock = 'knock', @@ -70,9 +68,8 @@ export type IRoomCreateContent = { ['m.federate']?: boolean; room_version: string; type?: string; - additional_creators?: string[]; predecessor?: { - event_id?: string; + event_id: string; room_id: string; }; }; @@ -96,13 +93,3 @@ export type MuteChanges = { added: string[]; removed: string[]; }; - -export type MemberPowerTagIcon = { - key?: string; - info?: IImageInfo; -}; -export type MemberPowerTag = { - name: string; - color?: string; - icon?: MemberPowerTagIcon; -};