move room user profile to new sdk

This commit is contained in:
Ajay Bura 2026-07-21 15:33:11 +05:30
commit 23537b7a76
12 changed files with 360 additions and 284 deletions

View file

@ -4,22 +4,15 @@ 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 (
<PopOut
anchor={cords}
@ -35,8 +28,8 @@ function UserRoomProfileContextMenu({ state }: { state: UserRoomProfileState })
}}
>
<Menu style={{ width: toRem(340) }}>
<SpaceProvider value={space ?? null}>
<RoomProvider value={room}>
<SpaceProvider value={spaceId ?? null}>
<RoomProvider value={roomId}>
<UserRoomProfile userId={userId} />
</RoomProvider>
</SpaceProvider>

View file

@ -11,13 +11,14 @@ import {
toRem,
} from 'folds';
import React, { ReactNode, useId } from 'react';
import { Presence } from 'neurone/lib/api/types';
import * as css from './styles.css';
import { Presence, usePresenceLabel } from '../../hooks/useUserPresence';
import { usePresenceLabel } from '../../hooks/useUserPresence';
const PresenceToColor: Record<Presence, MainColor> = {
[Presence.Online]: 'Success',
[Presence.Unavailable]: 'Warning',
[Presence.Offline]: 'Secondary',
online: 'Success',
unavailable: 'Warning',
offline: 'Secondary',
};
type PresenceBadgeProps = {
@ -51,7 +52,7 @@ export function PresenceBadge({ presence, status, size }: PresenceBadgeProps) {
ref={triggerRef}
size={size}
variant={PresenceToColor[presence]}
fill={presence === Presence.Offline ? 'Soft' : 'Solid'}
fill={presence === 'offline' ? 'Soft' : 'Solid'}
radii="Pill"
/>
)}

View file

@ -5,7 +5,6 @@ import { isKeyHotkey } from 'is-hotkey';
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
import { PowerColorBadge, PowerIcon } from '../power';
import { getPowerTagIconSrc } from '../../hooks/useMemberPowerTag';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { stopPropagation } from '../../utils/keyboard';
import { useRoom } from '../../hooks/useRoom';
@ -14,18 +13,19 @@ import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
import { SpaceSettingsPage } from '../../state/spaceSettings';
import { RoomSettingsPage } from '../../state/roomSettings';
import { useClient } from '../../hooks/useClient';
export function CreatorChip() {
const mx = useMatrixClient();
const ct = useClient();
const useAuthentication = useMediaAuthentication();
const room = useRoom();
const space = useSpaceOptionally();
const roomId = useRoom();
const spaceId = useSpaceOptionally();
const openRoomSettings = useOpenRoomSettings();
const openSpaceSettings = useOpenSpaceSettings();
const [cords, setCords] = useState<RectCords>();
const tag = useRoomCreatorsTag();
const tagIconSrc = tag.icon && getPowerTagIconSrc(mx, useAuthentication, tag.icon);
const tagIconSrc = tag.icon && getPowerTagIconSrc(ct, useAuthentication, tag.icon);
const open: MouseEventHandler<HTMLButtonElement> = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect());
@ -58,14 +58,10 @@ export function CreatorChip() {
size="300"
radii="300"
onClick={() => {
if (room.isSpaceRoom()) {
openSpaceSettings(
room.roomId,
space?.roomId,
SpaceSettingsPage.PermissionsPage
);
if (ct.spaceRooms.read.has(roomId)) {
openSpaceSettings(roomId, spaceId, SpaceSettingsPage.PermissionsPage);
} else {
openRoomSettings(room.roomId, space?.roomId, RoomSettingsPage.PermissionsPage);
openRoomSettings(roomId, spaceId, RoomSettingsPage.PermissionsPage);
}
close();
}}

View file

@ -23,13 +23,20 @@ import {
import React, { MouseEventHandler, useCallback, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import { isKeyHotkey } from 'is-hotkey';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import produce from 'immer';
import { putStateEvent } from 'neurone/lib/api/matrix/client/v3/rooms';
import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { PowerColorBadge, PowerIcon } from '../power';
import { useGetMemberPowerLevel, usePowerLevels } from '../../hooks/usePowerLevels';
import { getPowers, usePowerLevelTags } from '../../hooks/usePowerLevelTags';
import {
applyPermissionPower,
PermissionLocation,
useGetMemberPowerLevel,
usePowerLevelsContext,
} from '../../hooks/usePowerLevels';
import { getPowers, usePowerLevelTagsContext } from '../../hooks/usePowerLevelTags';
import { stopPropagation } from '../../utils/keyboard';
import { StateEvent } from '../../../types/matrix/room';
import { StateEvent, StateEventType } from '../../../types/matrix/room';
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
import { RoomSettingsPage } from '../../state/roomSettings';
import { useRoom } from '../../hooks/useRoom';
@ -40,9 +47,10 @@ import { SpaceSettingsPage } from '../../state/spaceSettings';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { BreakWord } from '../../styles/Text.css';
import { getPowerTagIconSrc, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useMemberPowerCompare } from '../../hooks/useMemberPowerCompare';
import { useClient } from '../../hooks/useClient';
import { useRoomCreators } from '../../hooks/useRoomCreators';
type SelfDemoteAlertProps = {
power: number;
@ -145,30 +153,29 @@ function SharedPowerAlert({ power, onCancel, onChange }: SharedPowerAlertProps)
}
export function PowerChip({ userId }: { userId: string }) {
const mx = useMatrixClient();
const room = useRoom();
const space = useSpaceOptionally();
const ct = useClient();
const roomId = useRoom();
const spaceId = useSpaceOptionally();
const useAuthentication = useMediaAuthentication();
const openRoomSettings = useOpenRoomSettings();
const openSpaceSettings = useOpenSpaceSettings();
const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const creators = useRoomCreators(roomId);
const powerLevels = usePowerLevelsContext();
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 powerLevelTags = usePowerLevelTagsContext();
const getMemberPowerTag = useGetMemberPowerTag(creators, powerLevels);
const myUserId = mx.getSafeUserId();
const myUserId = ct.session.userId;
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 tagIconSrc = tag.icon && getPowerTagIconSrc(ct, useAuthentication, tag.icon);
const [cords, setCords] = useState<RectCords>();
@ -178,12 +185,28 @@ export function PowerChip({ userId }: { userId: string }) {
const close = () => setCords(undefined);
const [powerState, changePower] = useAsyncCallback<undefined, Error, [number]>(
const [powerState, changePower] = useAsyncCallback<void, Error, [number]>(
useCallback(
async (power: number) => {
await mx.setPowerLevel(room.roomId, userId, power);
const editedPowerLevels = produce(powerLevels, (draftPowerLevels) => {
const location: PermissionLocation = {
user: true,
key: userId,
};
applyPermissionPower(draftPowerLevels, location, power);
return draftPowerLevels;
});
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId, eventType: StateEventType.RoomPowerLevels, stateKey: '' },
body: editedPowerLevels,
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(printMatrixError(err));
},
[mx, userId, room]
[ct, userId, roomId, powerLevels]
)
);
const changing = powerState.status === AsyncStatus.Loading;
@ -196,11 +219,11 @@ export function PowerChip({ userId }: { userId: string }) {
if (!canChangePowers) return;
if (power === getMemberPowerLevel(userId)) return;
if (userId === mx.getSafeUserId()) {
if (userId === myUserId) {
setSelfDemote(power);
return;
}
if (!creators.has(myUserId) && power === getMemberPowerLevel(myUserId)) {
if (!creators?.has(myUserId) && power === getMemberPowerLevel(myUserId)) {
setSharedPower(power);
return;
}
@ -252,10 +275,10 @@ export function PowerChip({ userId }: { userId: string }) {
{getPowers(powerLevelTags).map((power) => {
const powerTag = powerLevelTags[power];
const powerTagIconSrc =
powerTag.icon && getPowerTagIconSrc(mx, useAuthentication, powerTag.icon);
powerTag.icon && getPowerTagIconSrc(ct, useAuthentication, powerTag.icon);
const selected = getMemberPowerLevel(userId) === power;
const canAssignPower = creators.has(myUserId)
const canAssignPower = creators?.has(myUserId)
? true
: power <= getMemberPowerLevel(myUserId);
@ -293,18 +316,10 @@ export function PowerChip({ userId }: { userId: string }) {
size="300"
radii="300"
onClick={() => {
if (room.isSpaceRoom()) {
openSpaceSettings(
room.roomId,
space?.roomId,
SpaceSettingsPage.PermissionsPage
);
if (ct.spaceRooms.read.has(roomId)) {
openSpaceSettings(roomId, spaceId, SpaceSettingsPage.PermissionsPage);
} else {
openRoomSettings(
room.roomId,
space?.roomId,
RoomSettingsPage.PermissionsPage
);
openRoomSettings(roomId, spaceId, RoomSettingsPage.PermissionsPage);
}
close();
}}

View file

@ -2,7 +2,6 @@ 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,
@ -20,7 +19,7 @@ import {
Scroll,
Avatar,
} from 'folds';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { putAccountData } from 'neurone/lib/api/matrix/client/v3/user';
import { getMxIdServer } from '../../utils/matrix';
import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { stopPropagation } from '../../utils/keyboard';
@ -28,27 +27,27 @@ import { copyToClipboard } from '../../utils/dom';
import { getExploreServerPath } from '../../pages/pathUtils';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { factoryRoomIdByAtoZ } from '../../utils/sort';
import {
useMutualRooms,
useMutualRoomsSupport,
useUnstableMutualRoomsSupport,
} from '../../hooks/useMutualRooms';
import { useMutualRooms } 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 { getIgnoredUsersContent, useIgnoredUsers } from '../../hooks/useIgnoredUsers';
import { CutoutCard } from '../cutout-card';
import { SettingTile } from '../setting-tile';
import { useClient } from '../../hooks/useClient';
import { AccountDataType } from '../../../types/matrix/accountData';
import { useRoomHeroes } from '../../hooks/storage/useRoomHeros';
import { useGetRoomName } from '../../hooks/storage/useGetRoomName';
import { useGetRoomAvatar } from '../../hooks/storage/useGetRoomAvatar';
import { mxcToHttp } from '../../../client/mxcToHttp';
export function ServerChip({ server }: { server: string }) {
const mx = useMatrixClient();
const myServer = getMxIdServer(mx.getSafeUserId());
const ct = useClient();
const myServer = getMxIdServer(ct.session.userId);
const navigate = useNavigate();
const closeProfile = useCloseUserRoomProfile();
const [copied, setCopied] = useTimeoutToggle();
@ -229,24 +228,26 @@ export function ShareChip({ userId }: { userId: string }) {
}
type MutualRoomsData = {
rooms: Room[];
spaces: Room[];
directs: Room[];
rooms: string[];
spaces: string[];
directs: string[];
};
const EMPTY_LIST: string[] = [];
export function MutualRoomsChip({ userId }: { userId: string }) {
const mx = useMatrixClient();
const mutualRoomSupported = useMutualRoomsSupport();
const mutualRoomUnstable = useUnstableMutualRoomsSupport();
const ct = useClient();
const mutualRoomsState = useMutualRooms(userId);
console.log(mutualRoomSupported, mutualRoomsState);
const { navigateRoom, navigateSpace } = useRoomNavigate();
const closeUserRoomProfile = useCloseUserRoomProfile();
const directs = useDirectRooms();
const useAuthentication = useMediaAuthentication();
const allJoinedRooms = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allJoinedRooms);
const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(
mutualRoomsState.status === AsyncStatus.Success ? mutualRoomsState.data : EMPTY_LIST
);
const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
const [cords, setCords] = useState<RectCords>();
@ -264,36 +265,33 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
};
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);
const mutualRooms = mutualRoomsState.data.sort(factoryRoomIdByAtoZ(getRoomName));
mutualRooms.forEach((roomId) => {
if (ct.spaceRooms.read.has(roomId)) {
data.spaces.push(roomId);
return;
}
if (directs.includes(room.roomId)) {
data.directs.push(room);
if (directs.includes(roomId)) {
data.directs.push(roomId);
return;
}
data.rooms.push(room);
data.rooms.push(roomId);
});
}
return data;
}, [mutualRoomsState, getRoom, directs, mx]);
}, [mutualRoomsState, getRoomName, directs, ct]);
if (
userId === mx.getSafeUserId() ||
(!mutualRoomSupported && !mutualRoomUnstable) ||
mutualRoomsState.status === AsyncStatus.Error
) {
if (userId === ct.session.userId || mutualRoomsState.status === AsyncStatus.Error) {
return null;
}
const renderItem = (room: Room) => {
const { roomId } = room;
const renderItem = (roomId: string) => {
const dm = directs.includes(roomId);
const space = ct.spaceRooms.read.has(roomId);
const avatarUrl = getRoomAvatar(roomId);
const roomName = getRoomName(roomId) ?? 'Unknown';
return (
<MenuItem
@ -304,7 +302,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
radii="300"
style={{ paddingLeft: config.space.S100 }}
onClick={() => {
if (room.isSpaceRoom()) {
if (space) {
navigateSpace(roomId);
} else {
navigateRoom(roomId);
@ -313,29 +311,37 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
}}
before={
<Avatar size="200" radii={dm ? '400' : '300'}>
{dm || room.isSpaceRoom() ? (
{dm || space ? (
<RoomAvatar
roomId={room.roomId}
roomId={roomId}
src={
dm
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
: getRoomAvatarUrl(mx, room, 96, useAuthentication)
avatarUrl
? mxcToHttp(ct.session.baseUrl, avatarUrl, useAuthentication).thumbnail({
width: 96,
height: 96,
method: 'crop',
})
: undefined
}
alt={room.name}
alt={roomName}
renderFallback={() => (
<Text as="span" size="H6">
{nameInitials(room.name)}
{nameInitials(roomName)}
</Text>
)}
/>
) : (
<RoomIcon size="100" joinRule={room.getJoinRule()} roomType={room.getType()} />
<RoomIcon
size="100"
joinRule={ct.roomToJoinRule.read.get(roomId)}
roomType={ct.roomToType.read.get(roomId)}
/>
)}
</Avatar>
}
>
<Text size="B300" truncate>
{room.name}
{roomName}
</Text>
</MenuItem>
);
@ -443,7 +449,7 @@ export function IgnoredUserAlert() {
}
export function OptionsChip({ userId }: { userId: string }) {
const mx = useMatrixClient();
const ct = useClient();
const [cords, setCords] = useState<RectCords>();
const open: MouseEventHandler<HTMLButtonElement> = (evt) => {
@ -459,8 +465,14 @@ export function OptionsChip({ userId }: { userId: string }) {
useCallback(async () => {
const users = ignoredUsers.filter((u) => u !== userId);
if (!ignored) users.push(userId);
await mx.setIgnoredUsers(users);
}, [mx, ignoredUsers, userId, ignored])
return ct.requestAuthJSON(
putAccountData({
pathParams: { userId: ct.session.userId, type: AccountDataType.IgnoredUserList },
body: getIgnoredUsersContent(users),
})
);
}, [ct, ignoredUsers, userId, ignored])
);
const ignoring = ignoreState.status === AsyncStatus.Loading;

View file

@ -1,14 +1,16 @@
import { Box, Button, color, config, Icon, Icons, Spinner, Text, Input } from 'folds';
import React, { useCallback, useRef } from 'react';
import { postBan, postInvite, postKick, postUnban } from 'neurone/lib/api/matrix/client/v3/rooms';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
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';
import { useClient } from '../../hooks/useClient';
type UserKickAlertProps = {
reason?: string;
@ -64,18 +66,28 @@ type UserBanAlertProps = {
ts?: number;
};
export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBanAlertProps) {
const mx = useMatrixClient();
const room = useRoom();
const ct = useClient();
const roomId = 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<undefined, Error, []>(
const [unbanState, unban] = useAsyncCallback<void, Error, []>(
useCallback(async () => {
await mx.unban(room.roomId, userId);
}, [mx, room, userId])
const resp = await ct.requestAuthJSON(
postUnban({
pathParams: { roomId },
body: { user_id: userId },
})
);
const [err] = await parseMatrixResp(resp);
if (err) {
throw new Error(err.data.error ?? 'Unknown error.');
}
}, [ct, roomId, userId])
);
const banning = unbanState.status === AsyncStatus.Loading;
const error = unbanState.status === AsyncStatus.Error;
@ -139,18 +151,25 @@ type UserInviteAlertProps = {
ts?: number;
};
export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: UserInviteAlertProps) {
const mx = useMatrixClient();
const room = useRoom();
const ct = useClient();
const roomId = 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<undefined, Error, []>(
const [kickState, kick] = useAsyncCallback<void, Error, []>(
useCallback(async () => {
await mx.kick(room.roomId, userId);
}, [mx, room, userId])
const resp = await ct.requestAuthJSON(
postKick({
pathParams: { roomId },
body: { user_id: userId },
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error);
}, [ct, roomId, userId])
);
const kicking = kickState.status === AsyncStatus.Loading;
const error = kickState.status === AsyncStatus.Error;
@ -215,8 +234,8 @@ type UserModerationProps = {
canInvite: boolean;
};
export function UserModeration({ userId, canKick, canBan, canInvite }: UserModerationProps) {
const mx = useMatrixClient();
const room = useRoom();
const ct = useClient();
const roomId = useRoom();
const reasonInputRef = useRef<HTMLInputElement>(null);
const getReason = useCallback((): string | undefined => {
@ -227,22 +246,43 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer
return reason;
}, []);
const [kickState, kick] = useAsyncCallback<undefined, Error, []>(
const [kickState, kick] = useAsyncCallback<void, Error, []>(
useCallback(async () => {
await mx.kick(room.roomId, userId, getReason());
}, [mx, room, userId, getReason])
const resp = await ct.requestAuthJSON(
postKick({
pathParams: { roomId },
body: { user_id: userId, reason: getReason() },
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error);
}, [ct, roomId, userId, getReason])
);
const [banState, ban] = useAsyncCallback<undefined, Error, []>(
const [banState, ban] = useAsyncCallback<void, Error, []>(
useCallback(async () => {
await mx.ban(room.roomId, userId, getReason());
}, [mx, room, userId, getReason])
const resp = await ct.requestAuthJSON(
postBan({
pathParams: { roomId },
body: { user_id: userId, reason: getReason() },
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error);
}, [ct, roomId, userId, getReason])
);
const [inviteState, invite] = useAsyncCallback<undefined, Error, []>(
const [inviteState, invite] = useAsyncCallback<void, Error, []>(
useCallback(async () => {
await mx.invite(room.roomId, userId, getReason());
}, [mx, room, userId, getReason])
const resp = await ct.requestAuthJSON(
postInvite({
pathParams: { roomId },
body: { user_id: userId, reason: getReason() },
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error);
}, [ct, roomId, userId, getReason])
);
const disabled =

View file

@ -1,12 +1,15 @@
import { Box, Button, config, Icon, Icons, Text } from 'folds';
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ObjectReader } from 'neurone/lib/utils';
import { UserHero, UserHeroName } from './UserHero';
import { getMxIdServer, mxcUrlToHttp } from '../../utils/matrix';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getMxIdServer } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { usePowerLevels } from '../../hooks/usePowerLevels';
import {
PowerLevelsContextProvider,
usePowerLevels,
usePowerLevelsState,
} from '../../hooks/usePowerLevels';
import { useRoom } from '../../hooks/useRoom';
import { useUserPresence } from '../../hooks/useUserPresence';
import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips';
@ -14,7 +17,6 @@ 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';
@ -22,40 +24,54 @@ import { useMemberPowerCompare } from '../../hooks/useMemberPowerCompare';
import { CreatorChip } from './CreatorChip';
import { getDirectCreatePath, withSearchParam } from '../../pages/pathUtils';
import { DirectCreateSearchParams } from '../../pages/paths';
import { useRoomMember } from '../../hooks/useRoomMember';
import { mxcToHttp } from '../../../client/mxcToHttp';
import { useClient } from '../../hooks/useClient';
import {
PowerLevelTagsContextProvider,
usePowerLevelTags,
usePowerLevelTagsState,
} from '../../hooks/usePowerLevelTags';
type UserRoomProfileProps = {
userId: string;
};
export function UserRoomProfile({ userId }: UserRoomProfileProps) {
const mx = useMatrixClient();
const ct = useClient();
const useAuthentication = useMediaAuthentication();
const navigate = useNavigate();
const closeUserRoomProfile = useCloseUserRoomProfile();
const ignoredUsers = useIgnoredUsers();
const ignored = ignoredUsers.includes(userId);
const room = useRoom();
const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const roomId = useRoom();
const stateObjResult = usePowerLevelsState(roomId);
const powerLevels = usePowerLevels(stateObjResult.value);
const creators = useRoomCreators(roomId);
const powerLevelTagsResult = usePowerLevelTagsState(roomId);
const powerLevelTags = usePowerLevelTags(powerLevels, powerLevelTagsResult.value);
const permissions = useRoomPermissions(creators, powerLevels);
const { hasMorePower } = useMemberPowerCompare(creators, powerLevels);
const myUserId = mx.getSafeUserId();
const creator = creators.has(userId);
const myUserId = ct.session.userId;
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 { memberEventResult, getMembership, getName, getAvatar } = useRoomMember(roomId, userId);
const membership = getMembership();
const server = getMxIdServer(userId);
const displayName = getMemberDisplayName(room, userId);
const avatarMxc = getMemberAvatarMxc(room, userId);
const avatarUrl = (avatarMxc && mxcUrlToHttp(mx, avatarMxc, useAuthentication)) ?? undefined;
const displayName = getName();
const avatarMxc = getAvatar();
const avatarUrl = avatarMxc
? mxcToHttp(ct.session.baseUrl, avatarMxc, useAuthentication).url()
: undefined;
const presence = useUserPresence(userId);
@ -68,75 +84,78 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
};
return (
<Box direction="Column">
<UserHero
userId={userId}
avatarUrl={avatarUrl}
presence={presence && presence.lastActiveTs !== 0 ? presence : undefined}
/>
<Box direction="Column" gap="500" style={{ padding: config.space.S400 }}>
<Box direction="Column" gap="400">
<Box gap="400" alignItems="Start">
<UserHeroName displayName={displayName} userId={userId} />
{userId !== myUserId && (
<Box shrink="No">
<Button
size="300"
variant="Primary"
fill="Solid"
radii="300"
before={<Icon size="50" src={Icons.Message} filled />}
onClick={handleMessage}
>
<Text size="B300">Message</Text>
</Button>
<PowerLevelsContextProvider value={powerLevels}>
<PowerLevelTagsContextProvider value={powerLevelTags}>
<Box direction="Column">
<UserHero
userId={userId}
avatarUrl={avatarUrl}
presence={presence && presence.lastActiveTs !== 0 ? presence : undefined}
/>
<Box direction="Column" gap="500" style={{ padding: config.space.S400 }}>
<Box direction="Column" gap="400">
<Box gap="400" alignItems="Start">
<UserHeroName displayName={displayName} userId={userId} />
{userId !== myUserId && (
<Box shrink="No">
<Button
size="300"
variant="Primary"
fill="Solid"
radii="300"
before={<Icon size="50" src={Icons.Message} filled />}
onClick={handleMessage}
>
<Text size="B300">Message</Text>
</Button>
</Box>
)}
</Box>
<Box alignItems="Center" gap="200" wrap="Wrap">
{server && <ServerChip server={server} />}
<ShareChip userId={userId} />
{creator ? <CreatorChip /> : <PowerChip userId={userId} />}
{userId !== myUserId && <MutualRoomsChip userId={userId} />}
{userId !== myUserId && <OptionsChip userId={userId} />}
</Box>
</Box>
{ignored && <IgnoredUserAlert />}
{memberEventResult.value && membership === Membership.Ban && (
<UserBanAlert
userId={userId}
reason={ObjectReader.string(memberEventResult.value.event.content, 'reason')}
canUnban={canUnban}
bannedBy={memberEventResult.value.event.sender}
ts={memberEventResult.value.event.origin_server_ts}
/>
)}
</Box>
<Box alignItems="Center" gap="200" wrap="Wrap">
{server && <ServerChip server={server} />}
<ShareChip userId={userId} />
{creator ? <CreatorChip /> : <PowerChip userId={userId} />}
{userId !== myUserId && <MutualRoomsChip userId={userId} />}
{userId !== myUserId && <OptionsChip userId={userId} />}
{memberEventResult.value &&
membership === Membership.Leave &&
memberEventResult.value.event.sender !== userId && (
<UserKickAlert
reason={ObjectReader.string(memberEventResult.value.event.content, 'reason')}
kickedBy={memberEventResult.value.event.sender}
ts={memberEventResult.value.event.origin_server_ts}
/>
)}
{memberEventResult.value && membership === Membership.Invite && (
<UserInviteAlert
userId={userId}
reason={ObjectReader.string(memberEventResult.value.event.content, 'reason')}
canKick={canKickUser}
invitedBy={memberEventResult.value.event.sender}
ts={memberEventResult.value.event.origin_server_ts}
/>
)}
<UserModeration
userId={userId}
canInvite={canInvite && membership === Membership.Leave}
canKick={canKickUser && membership === Membership.Join}
canBan={canBanUser && membership !== Membership.Ban}
/>
</Box>
</Box>
{ignored && <IgnoredUserAlert />}
{member && membership === Membership.Ban && (
<UserBanAlert
userId={userId}
reason={member.events.member?.getContent().reason}
canUnban={canUnban}
bannedBy={member.events.member?.getSender()}
ts={member.events.member?.getTs()}
/>
)}
{member &&
membership === Membership.Leave &&
member.events.member &&
member.events.member.getSender() !== userId && (
<UserKickAlert
reason={member.events.member?.getContent().reason}
kickedBy={member.events.member?.getSender()}
ts={member.events.member?.getTs()}
/>
)}
{member && membership === Membership.Invite && (
<UserInviteAlert
userId={userId}
reason={member.events.member?.getContent().reason}
canKick={canKickUser}
invitedBy={member.events.member?.getSender()}
ts={member.events.member?.getTs()}
/>
)}
<UserModeration
userId={userId}
canInvite={canInvite && membership === Membership.Leave}
canKick={canKickUser && membership === Membership.Join}
canBan={canBanUser && membership !== Membership.Ban}
/>
</Box>
</Box>
</PowerLevelTagsContextProvider>
</PowerLevelsContextProvider>
);
}

View file

@ -1,15 +1,18 @@
import { useCallback } from 'react';
import { IPowerLevels, readPowerLevel } from './usePowerLevels';
export const useMemberPowerCompare = (creators: Set<string>, powerLevels: IPowerLevels) => {
export const useMemberPowerCompare = (
creators: Set<string> | undefined,
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);
const aIsCreator = creators?.has(userIdA);
const bIsCreator = creators?.has(userIdB);
if (aIsCreator && bIsCreator) return false;
if (aIsCreator) return true;
if (bIsCreator) return false;

View file

@ -1,16 +1,15 @@
import { useCallback } from 'react';
import { MatrixClient, Method } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient';
import { withSearchParams } from 'neurone/lib/api/utils';
import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
import { ClientContext } from '../../client/context';
import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback';
import { useUnstableFeatureSupport, useSpecVersions } from './useSpecVersions';
import { useClient } from './useClient';
export const useUnstableMutualRoomsSupport = (): boolean => {
const checkSupport = useUnstableFeatureSupport();
const supported =
checkSupport('uk.half-shot.msc2666') ||
checkSupport('uk.half-shot.msc2666.mutual_rooms') ||
checkSupport('uk.half-shot.msc2666.query_mutual_rooms');
const supported = checkSupport('uk.half-shot.msc2666.query_mutual_rooms');
return !!supported;
};
@ -31,24 +30,33 @@ type MutualRoomsOK = {
count: number;
};
const fetchAllMutualRooms = async (mx: MatrixClient, userId: string): Promise<string[]> => {
const fetchAllMutualRooms = async (
ct: ClientContext,
userId: string,
unstable?: boolean
): Promise<string[]> => {
const mutualRooms: Set<string> = new Set();
let nextBatch: string | undefined;
do {
// eslint-disable-next-line no-await-in-loop
const result = await mx.http.authedRequest<MutualRoomsOK>(
Method.Get,
'/mutual_rooms',
{
user_id: userId,
from: nextBatch,
},
undefined,
{
prefix: '/_matrix/client/v1',
}
);
const resp = await ct.requestAuthJSON({
method: 'GET',
path: withSearchParams(
unstable
? '/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms'
: '/_matrix/client/v1/mutual_rooms',
{
user_id: userId,
from: nextBatch,
}
),
requireAuthentication: true,
});
// eslint-disable-next-line no-await-in-loop
const [err, result] = await parseMatrixResp<MutualRoomsOK>(resp);
if (err) throw new Error(printMatrixError(err));
result.joined.forEach((r) => mutualRooms.add(r));
nextBatch = result.next_batch;
} while (typeof nextBatch === 'string');
@ -57,17 +65,19 @@ const fetchAllMutualRooms = async (mx: MatrixClient, userId: string): Promise<st
};
export const useMutualRooms = (userId: string): AsyncState<string[], unknown> => {
const mx = useMatrixClient();
const ct = useClient();
const unstableSupport = useUnstableMutualRoomsSupport();
const support = useMutualRoomsSupport();
const [mutualRoomsState] = useAsyncCallbackValue(
useCallback(() => {
if (support) return fetchAllMutualRooms(mx, userId);
if (unstableSupport) return mx._unstable_getSharedRooms(userId);
return Promise.resolve([]);
}, [mx, userId, unstableSupport, support])
useCallback(async () => {
if (support) return fetchAllMutualRooms(ct, userId);
if (unstableSupport) return fetchAllMutualRooms(ct, userId, true);
const memberships = await ct.storage.joinRoomMember.read.queryIndex('userId', userId);
return memberships.map((membership) => membership.roomId);
}, [ct, userId, unstableSupport, support])
);
return mutualRoomsState;

View file

@ -6,7 +6,6 @@ import { StateEvent, StateEventType } from '../../types/matrix/room';
import { getJoinRoomStateId, JoinRoomStateObject } from '../../client/storage/collections';
import { useClient } from './useClient';
import { useObservableValuePromise } from './useObservableValue';
import { ClientContext } from '../../client/context';
export type PowerLevelActions = 'invite' | 'redact' | 'kick' | 'ban' | 'historical';
export type PowerLevelNotificationsAction = 'room';
@ -243,8 +242,13 @@ export const applyPermissionPower = (
if (typeof location.key === 'string') {
const users = powerLevels.users ?? {};
users[location.key] = power;
if (powerLevels.users_default === power) {
delete users[location.key];
}
// eslint-disable-next-line no-param-reassign
powerLevels.users = users;
return powerLevels;
}
// eslint-disable-next-line no-param-reassign

View file

@ -1,12 +1,8 @@
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',
}
import { useMemo } from 'react';
import { Presence } from 'neurone/lib/api/types';
import { useClient } from './useClient';
import { useObservableValuePromise } from './useObservableValue';
import { PresenceObject } from '../../client/storage/collections';
export type UserPresence = {
presence: Presence;
@ -15,44 +11,31 @@ export type UserPresence = {
lastActiveTs?: number;
};
const getUserPresence = (user: User): UserPresence => ({
presence: user.presence as Presence,
status: user.presenceStatusMsg,
active: user.currentlyActive,
lastActiveTs: user.getLastActiveTs(),
const getUserPresence = (user?: PresenceObject): UserPresence => ({
presence: user?.presence ?? 'offline',
status: user?.content.status_msg ?? undefined,
active: user?.content.currently_active ?? false,
lastActiveTs: user?.content.last_active_ago,
});
export const useUserPresence = (userId: string): UserPresence | undefined => {
const mx = useMatrixClient();
const user = mx.getUser(userId);
const ct = useClient();
const presencePromise = useMemo(
() => ct.storage.presence.observableRead.get(userId),
[ct, userId]
);
const presenceResult = useObservableValuePromise(presencePromise);
const presenceObj = presenceResult.value;
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;
return getUserPresence(presenceObj);
};
export const usePresenceLabel = (): Record<Presence, string> =>
useMemo(
() => ({
[Presence.Online]: 'Active',
[Presence.Unavailable]: 'Busy',
[Presence.Offline]: 'Away',
online: 'Active',
unavailable: 'Busy',
offline: 'Away',
}),
[]
);

View file

@ -135,8 +135,8 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
>
<Outlet />
</ClientLayout>
{/*<SearchModalRenderer />
<UserRoomProfileRenderer />*/}
{/*<SearchModalRenderer />*/}
<UserRoomProfileRenderer />
<CreateRoomModalRenderer />
<CreateSpaceModalRenderer />
{/*<RoomSettingsRenderer />