diff --git a/src/app/components/create-room/utils.ts b/src/app/components/create-room/utils.ts index a0ca7488..f10adf21 100644 --- a/src/app/components/create-room/utils.ts +++ b/src/app/components/create-room/utils.ts @@ -11,6 +11,7 @@ import { CreateRoomKind } from './CreateRoomKindSelector'; import { RoomType, StateEvent } from '../../../types/matrix/room'; import { getViaServers } from '../../plugins/via-servers'; import { getMxIdServer } from '../../utils/matrix'; +import { IPowerLevels } from '../../hooks/usePowerLevels'; export const createRoomCreationContent = ( type: RoomType | undefined, @@ -82,6 +83,44 @@ export const createRoomEncryptionState = () => ({ }, }); +export const createRoomCallState = () => ({ + type: 'org.matrix.msc3401.call', + state_key: '', + content: {}, +}); + +export const createPowerLevelContentOverrides = ( + base: IPowerLevels, + overrides: Partial +): IPowerLevels => ({ + ...base, + ...overrides, + ...(base.events || overrides.events + ? { + events: { + ...base.events, + ...overrides.events, + }, + } + : {}), + ...(base.users || overrides.users + ? { + users: { + ...base.users, + ...overrides.users, + }, + } + : {}), + ...(base.notifications || overrides.notifications + ? { + notifications: { + ...base.notifications, + ...overrides.notifications, + }, + } + : {}), +}); + export type CreateRoomData = { version: string; type?: RoomType; @@ -94,6 +133,7 @@ export type CreateRoomData = { knock: boolean; allowFederation: boolean; additionalCreators?: string[]; + powerLevelContentOverrides?: IPowerLevels; }; export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise => { const initialState: ICreateRoomStateEvent[] = []; @@ -106,6 +146,10 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis initialState.push(createRoomParentState(data.parent)); } + if (data.type === RoomType.Call) { + initialState.push(createRoomCallState()); + } + initialState.push(createRoomJoinRulesState(data.kind, data.parent, data.knock)); const options: ICreateRoomOpts = { @@ -136,5 +180,15 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis ); } + if (data.powerLevelContentOverrides) { + const roomPowers = await mx.getStateEvent(result.room_id, StateEvent.RoomPowerLevels, ''); + const updatedPowers = createPowerLevelContentOverrides( + roomPowers, + data.powerLevelContentOverrides + ); + + await mx.sendStateEvent(result.room_id, StateEvent.RoomPowerLevels as any, updatedPowers, ''); + } + return result.room_id; }; diff --git a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx index b0c64f60..a9e7f51f 100644 --- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx @@ -174,7 +174,7 @@ export function RoomMentionAutocomplete({ )} /> ) : ( - + )} } diff --git a/src/app/components/room-avatar/RoomAvatar.tsx b/src/app/components/room-avatar/RoomAvatar.tsx index 60de8346..1ee72d4a 100644 --- a/src/app/components/room-avatar/RoomAvatar.tsx +++ b/src/app/components/room-avatar/RoomAvatar.tsx @@ -2,7 +2,7 @@ import { JoinRule } from 'matrix-js-sdk'; import { AvatarFallback, AvatarImage, Icon, Icons, color } from 'folds'; import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useState } from 'react'; import * as css from './RoomAvatar.css'; -import { joinRuleToIconSrc } from '../../utils/room'; +import { getRoomIconSrc } from '../../utils/room'; import colorMXID from '../../../util/colorMXID'; type RoomAvatarProps = { @@ -44,14 +44,10 @@ export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps export const RoomIcon = forwardRef< SVGSVGElement, Omit, 'src'> & { - joinRule: JoinRule; - space?: boolean; - call?: boolean; + joinRule?: JoinRule; + roomType?: string; + locked?: boolean; } ->(({ joinRule, space, call, ...props }, ref) => ( - +>(({ joinRule, roomType, locked, ...props }, ref) => ( + )); diff --git a/src/app/features/call/CallView.tsx b/src/app/features/call/CallView.tsx index 060a9cf7..300e18d8 100644 --- a/src/app/features/call/CallView.tsx +++ b/src/app/features/call/CallView.tsx @@ -1,4 +1,4 @@ -import { Room } from 'matrix-js-sdk'; +import { EventType, Room } from 'matrix-js-sdk'; import React, { useContext, useCallback, @@ -21,6 +21,10 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { getMemberDisplayName } from '../../utils/room'; import { getMxIdLocalPart } from '../../utils/matrix'; import * as css from './CallView.css'; +import { useRoomPermissions } from '../../hooks/useRoomPermissions'; +import { useRoomCreators } from '../../hooks/useRoomCreators'; +import { usePowerLevelsContext } from '../../hooks/usePowerLevels'; +import { useRoomName } from '../../hooks/useRoomMeta'; type OriginalStyles = { position?: string; @@ -57,6 +61,13 @@ export function CallView({ room }: { room: Room }) { const [visibleCallNames, setVisibleCallNames] = useState(''); + const powerLevels = usePowerLevelsContext(); + const creators = useRoomCreators(room); + + const roomName = useRoomName(room); + const permissions = useRoomPermissions(creators, powerLevels); + const canJoin = permissions.event(EventType.GroupCallMemberPrefix, mx.getSafeUserId()); + const { isActiveCallReady, activeCallRoomId, @@ -160,6 +171,8 @@ export function CallView({ room }: { room: Room }) { ]); const handleJoinVCClick: MouseEventHandler = (evt) => { + if (!canJoin) return; + if (isMobile) { evt.stopPropagation(); setViewedCallRoomId(room.roomId); @@ -210,11 +223,7 @@ export function CallView({ room }: { room: Room }) { > {callMembers.slice(0, 6).map((callMember) => ( - + ))} @@ -231,21 +240,25 @@ export function CallView({ room }: { room: Room }) { paddingBottom: config.space.S300, }} > - {room.name} + {roomName} {visibleCallNames !== '' ? visibleCallNames : 'No one'}{' '} {memberDisplayNames.length > 1 ? 'are' : 'is'} currently in voice - diff --git a/src/app/features/common-settings/general/RoomProfile.tsx b/src/app/features/common-settings/general/RoomProfile.tsx index 0f515c39..0a67f9e0 100644 --- a/src/app/features/common-settings/general/RoomProfile.tsx +++ b/src/app/features/common-settings/general/RoomProfile.tsx @@ -199,7 +199,7 @@ export function RoomProfileEdit({ alt={name} renderFallback={() => ( ( { if (kind === CreateRoomKind.Private) return Icons.HashLock; @@ -118,9 +119,18 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP roomKnock = knock; } + let roomType; + const powerOverrides: IPowerLevels = { + events: {}, + }; + if (callRoom) { + roomType = RoomType.Call; + powerOverrides.events![StateEvent.GroupCallMemberPrefix] = 0; + } + create({ version: selectedRoomVersion, - type: callRoom ? RoomType.Call : undefined, + type: roomType, parent: space, kind, name: roomName, @@ -130,6 +140,7 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP knock: roomKnock, allowFederation: federation, additionalCreators: allowAdditionalCreators ? additionalCreators : undefined, + powerLevelContentOverrides: powerOverrides, }).then((roomId) => { if (alive()) { onCreate?.(roomId); @@ -183,12 +194,7 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP title="Call Room" description="Enable this to create a room optimized for voice calls." after={ - + } /> diff --git a/src/app/features/lobby/RoomItem.tsx b/src/app/features/lobby/RoomItem.tsx index 994cda05..f5e17bda 100644 --- a/src/app/features/lobby/RoomItem.tsx +++ b/src/app/features/lobby/RoomItem.tsx @@ -30,7 +30,7 @@ import { LocalRoomSummaryLoader } from '../../components/RoomSummaryLoader'; import { UseStateProvider } from '../../components/UseStateProvider'; import { RoomTopicViewer } from '../../components/room-topic-viewer'; import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard'; -import { Membership } from '../../../types/matrix/room'; +import { Membership, RoomType } from '../../../types/matrix/room'; import * as css from './RoomItem.css'; import * as styleCss from './style.css'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; @@ -175,6 +175,7 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf type RoomProfileProps = { roomId: string; + roomType?: string; name: string; topic?: string; avatarUrl?: string; @@ -185,6 +186,7 @@ type RoomProfileProps = { }; function RoomProfile({ roomId, + roomType, name, topic, avatarUrl, @@ -200,9 +202,7 @@ function RoomProfile({ roomId={roomId} src={avatarUrl} alt={name} - renderFallback={() => ( - - )} + renderFallback={() => } /> @@ -338,6 +338,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>( {(localSummary) => ( ( {summary && ( } > @@ -392,10 +390,7 @@ export function SearchFilters({ onClick={() => onSelectedRoomsChange(selectedRooms.filter((rId) => rId !== roomId))} radii="Pill" before={ - + } after={} > diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index 3097de2f..03c2d169 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -1,5 +1,5 @@ import React, { MouseEventHandler, forwardRef, useState, MouseEvent } from 'react'; -import { Room } from 'matrix-js-sdk'; +import { EventType, Room } from 'matrix-js-sdk'; import { Avatar, Box, @@ -21,7 +21,7 @@ import { } from 'folds'; import { useFocusWithin, useHover } from 'react-aria'; import FocusTrap from 'focus-trap-react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { NavButton, NavItem, NavItemContent, NavItemOptions } from '../../components/nav'; import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; @@ -59,6 +59,7 @@ import { useCallMembers } from '../../hooks/useCallMemberships'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { RoomNavUser } from './RoomNavUser'; +import { useRoomName } from '../../hooks/useRoomMeta'; type RoomNavItemMenuProps = { room: Room; @@ -241,6 +242,10 @@ export function RoomNavItem({ const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); const [menuAnchor, setMenuAnchor] = useState(); const unread = useRoomUnread(room.roomId, roomToUnreadAtom); + const typingMember = useRoomTypingMember(room.roomId).filter( + (receipt) => receipt.userId !== mx.getUserId() + ); + const { isActiveCallReady, activeCallRoomId, @@ -250,14 +255,20 @@ export function RoomNavItem({ toggleChat, hangUp, } = useCallState(); - const typingMember = useRoomTypingMember(room.roomId).filter( - (receipt) => receipt.userId !== mx.getUserId() - ); + const isActiveCall = isActiveCallReady && activeCallRoomId === room.roomId; const callMemberships = useCallMembers(mx, room.roomId); + + const powerLevels = usePowerLevels(room); + const creators = useRoomCreators(room); + const roomName = useRoomName(room); + + const permissions = useRoomPermissions(creators, powerLevels); + const canJoinCall = permissions.event(EventType.GroupCallMemberPrefix, mx.getSafeUserId()); + const { navigateRoom } = useRoomNavigate(); const navigate = useNavigate(); - const { roomIdOrAlias: viewedRoomId } = useParams(); + const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; @@ -278,10 +289,7 @@ export function RoomNavItem({ const handleNavItemClick: MouseEventHandler = (evt) => { if (room.isCallRoom()) { if (!isMobile) { - if (!isActiveCall) { - if (mx.getRoom(viewedRoomId)?.isCallRoom()) { - navigateRoom(room.roomId); - } + if (!isActiveCall && canJoinCall) { hangUp(); setActiveCallRoomId(room.roomId); } else { @@ -307,7 +315,7 @@ export function RoomNavItem({ const optionsVisible = hover || !!menuAnchor; const ariaLabel = [ - room.name, + roomName, room.isCallRoom() ? [ 'Call Room', @@ -345,10 +353,10 @@ export function RoomNavItem({ ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication) } - alt={room.name} + alt={roomName} renderFallback={() => ( - {nameInitials(room.name)} + {nameInitials(roomName)} )} /> @@ -360,7 +368,8 @@ export function RoomNavItem({ filled={selected || isActiveCall} size="100" joinRule={room.getJoinRule()} - call={room.isCallRoom()} + roomType={room.getType()} + locked={room.isCallRoom() && !canJoinCall} /> )} @@ -371,7 +380,7 @@ export function RoomNavItem({ size="Inherit" truncate > - {room.name} + {roomName} {!optionsVisible && !unread && !selected && typingMember.length > 0 && ( diff --git a/src/app/features/room-settings/permissions/usePermissionItems.ts b/src/app/features/room-settings/permissions/usePermissionItems.ts index 9f3fd104..cf77a277 100644 --- a/src/app/features/room-settings/permissions/usePermissionItems.ts +++ b/src/app/features/room-settings/permissions/usePermissionItems.ts @@ -49,13 +49,6 @@ export const usePermissionGroups = (): PermissionGroup[] => { const callSettingsGroup: PermissionGroup = { name: 'Calls', items: [ - { - location: { - state: true, - key: StateEvent.GroupCallPrefix, - }, - name: 'Start Call', - }, { location: { state: true, diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 058063c0..5c430268 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -23,7 +23,7 @@ import { Spinner, } from 'folds'; import { useNavigate } from 'react-router-dom'; -import { JoinRule, Room } from 'matrix-js-sdk'; +import { Room } from 'matrix-js-sdk'; import { useStateEvent } from '../../hooks/useStateEvent'; import { PageHeader } from '../../components/page'; @@ -321,11 +321,7 @@ export function RoomViewHeader() { src={avatarUrl} alt={name} renderFallback={() => ( - + )} /> diff --git a/src/app/features/search/Search.tsx b/src/app/features/search/Search.tsx index fcd6233a..6027f322 100644 --- a/src/app/features/search/Search.tsx +++ b/src/app/features/search/Search.tsx @@ -373,7 +373,7 @@ export function Search({ requestClose }: SearchProps) { )} diff --git a/src/app/features/space-settings/SpaceSettings.tsx b/src/app/features/space-settings/SpaceSettings.tsx index e565fb92..b5fefc93 100644 --- a/src/app/features/space-settings/SpaceSettings.tsx +++ b/src/app/features/space-settings/SpaceSettings.tsx @@ -103,7 +103,7 @@ export function SpaceSettings({ initialPage, requestClose }: SpaceSettingsProps) alt={roomName} renderFallback={() => ( { if (mx?.getUserId()) { - if ( - (activeCallRoomId !== viewedCallRoomId && isActiveCallReady) || - (activeCallRoomId && !isActiveCallReady) || - (!activeCallRoomId && viewedCallRoomId && !isActiveCallReady) - ) { - const roomIdToSet = (skipLobby ? activeCallRoomId : viewedCallRoomId) ?? ''; - - if (roomIdToSet === '') { - return; - } + if (activeCallRoomId && !isActiveCallReady) { + const roomIdToSet = activeCallRoomId; const widgetId = `element-call-${roomIdToSet}-${Date.now()}`; const newUrl = getWidgetUrl( @@ -125,7 +117,6 @@ export function PersistentCallContainer({ children }: PersistentCallContainerPro [ mx, activeCallRoomId, - viewedCallRoomId, isActiveCallReady, clientConfig.elementCallUrl, activeClientWidget, diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index c084c00e..02186a4c 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -257,25 +257,60 @@ export const getUnreadInfos = (mx: MatrixClient): UnreadInfo[] => { return unreadInfos; }; -export const joinRuleToIconSrc = ( +export const getRoomIconSrc = ( icons: Record, - joinRule: JoinRule, - space: boolean, - call: boolean -): IconSrc | undefined => { - if (joinRule === JoinRule.Restricted) { - return space ? icons.Space : call ? icons.VolumeHigh : icons.Hash; + roomType?: string, + joinRule?: JoinRule, + locked?: boolean +): IconSrc => { + type RoomIcons = { + base: IconSrc; + locked: IconSrc; + public: IconSrc; + }; + + const roomTypeIcons: Record = { + [RoomType.Call]: { + base: icons.VolumeHigh, + locked: icons.Lock, + public: icons.VolumeHigh, + }, + [RoomType.Space]: { + base: icons.Space, + locked: icons.SpaceLock, + public: icons.SpaceGlobe, + }, + default: { + base: icons.Hash, + locked: icons.HashLock, + public: icons.HashGlobe, + }, + }; + + const roomIcons = roomTypeIcons[roomType ?? 'default'] ?? roomTypeIcons.default; + + let roomIcon = roomIcons.base; + + if (locked) { + roomIcon = roomIcons.locked; + } else { + switch (joinRule) { + case JoinRule.Invite: + case JoinRule.Knock: + roomIcon = roomIcons.locked; + break; + case JoinRule.Restricted: + roomIcon = roomIcons.base; + break; + case JoinRule.Public: + roomIcon = roomIcons.public; + break; + default: + break; + } } - if (joinRule === JoinRule.Knock) { - return space ? icons.SpaceLock : call ? icons.VolumeHigh : icons.HashLock; - } - if (joinRule === JoinRule.Invite) { - return space ? icons.SpaceLock : call ? icons.VolumeHigh : icons.HashLock; - } - if (joinRule === JoinRule.Public) { - return space ? icons.SpaceGlobe : call ? icons.VolumeHigh : icons.HashGlobe; - } - return undefined; + + return roomIcon; }; export const getRoomAvatarUrl = (