Support room version 12 (#2399)

* WIP - support room version 12

* add room creators hook

* revert changes from powerlevels

* improve use room creators hook

* add hook to get dm users

* add options to add creators in create room/space

* add member item component in member drawer

* remove unused import

* extract member drawer header component

* get room creators as set only if room version support them

* add room permissions hook

* support room v12 creators power

* make predecessor event id optional

* add info about founders in permissions

* allow to create infinite powers to room creators

* allow everyone with permission to create infinite power

* handle additional creators in room upgrade

* add option to follow space tombstone
This commit is contained in:
Ajay Bura
2025-08-12 19:42:30 +05:30
committed by GitHub
parent 4d1ae4eafd
commit f82cfead46
58 changed files with 1717 additions and 783 deletions

View File

@@ -1,10 +1,8 @@
import { keyframes, style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds';
import { config, toRem } from 'folds';
export const MembersDrawer = style({
width: toRem(266),
backgroundColor: color.Background.Container,
color: color.Background.OnContainer,
});
export const MembersDrawerHeader = style({

View File

@@ -26,7 +26,7 @@ import {
TooltipProvider,
config,
} from 'folds';
import { Room, RoomMember } from 'matrix-js-sdk';
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
import { useVirtualizer } from '@tanstack/react-virtual';
import classNames from 'classnames';
@@ -39,7 +39,6 @@ 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';
@@ -51,12 +50,116 @@ import { UserAvatar } from '../../components/user-avatar';
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useMembershipFilter, useMembershipFilterMenu } from '../../hooks/useMemberFilter';
import { useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort';
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort';
import { 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 (
<Header className={css.MembersDrawerHeader} variant="Background" size="600">
<Box grow="Yes" alignItems="Center" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text title={`${room.getJoinedMemberCount()} Members`} size="H5" truncate>
{`${millify(room.getJoinedMemberCount())} Members`}
</Text>
</Box>
<Box shrink="No" alignItems="Center">
<TooltipProvider
position="Bottom"
align="End"
offset={4}
tooltip={
<Tooltip>
<Text>Close</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton
ref={triggerRef}
variant="Background"
onClick={() => setPeopleDrawer(false)}
>
<Icon src={Icons.Cross} />
</IconButton>
)}
</TooltipProvider>
</Box>
</Box>
</Header>
);
}
type MemberItemProps = {
mx: MatrixClient;
useAuthentication: boolean;
room: Room;
member: RoomMember;
onClick: MouseEventHandler<HTMLButtonElement>;
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 (
<MenuItem
style={{ padding: `0 ${config.space.S200}` }}
aria-pressed={pressed}
data-user-id={member.userId}
variant="Background"
radii="400"
onClick={onClick}
before={
<Avatar size="200">
<UserAvatar
userId={member.userId}
src={avatarUrl ?? undefined}
alt={name}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
}
after={
typing && (
<Badge size="300" variant="Secondary" fill="Soft" radii="Pill" outlined>
<TypingIndicator size="300" />
</Badge>
)
}
>
<Box grow="Yes">
<Text size="T400" truncate>
{name}
</Text>
</Box>
</MenuItem>
);
}
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000,
@@ -80,9 +183,10 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const searchInputRef = useRef<HTMLInputElement>(null);
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
const powerLevels = usePowerLevelsContext();
const [, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
const creators = useRoomCreators(room);
const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
const fetchingMembers = members.length < room.getJoinedMemberCount();
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
const openUserRoomProfile = useOpenUserRoomProfile();
const space = useSpaceOptionally();
const openProfileUserId = useUserRoomProfileState()?.userId;
@@ -91,20 +195,16 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
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((a, b) => b.powerLevel - a.powerLevel),
[members, membershipFilter, memberSort]
() => members.filter(membershipFilter.filterFn).sort(memberSort.sortFn).sort(memberPowerSort),
[members, membershipFilter, memberSort, memberPowerSort]
);
const [result, search, resetSearch] = useAsyncSearch(
@@ -116,11 +216,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const processMembers = result ? result.items : filteredMembers;
const PLTagOrRoomMember = useFlattenPowerLevelTagMembers(
processMembers,
getPowerLevel,
getPowerLevelTag
);
const PLTagOrRoomMember = useFlattenPowerTagMembers(processMembers, getPowerTag);
const virtualizer = useVirtualizer({
count: PLTagOrRoomMember.length,
@@ -140,9 +236,6 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
{ wait: 200 }
);
const getName = (member: RoomMember) =>
getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId;
const handleMemberClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
const btn = evt.currentTarget as HTMLButtonElement;
const userId = btn.getAttribute('data-user-id');
@@ -151,38 +244,12 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
};
return (
<Box className={css.MembersDrawer} shrink="No" direction="Column">
<Header className={css.MembersDrawerHeader} variant="Background" size="600">
<Box grow="Yes" alignItems="Center" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text title={`${room.getJoinedMemberCount()} Members`} size="H5" truncate>
{`${millify(room.getJoinedMemberCount())} Members`}
</Text>
</Box>
<Box shrink="No" alignItems="Center">
<TooltipProvider
position="Bottom"
align="End"
offset={4}
tooltip={
<Tooltip>
<Text>Close</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton
ref={triggerRef}
variant="Background"
onClick={() => setPeopleDrawer(false)}
>
<Icon src={Icons.Cross} />
</IconButton>
)}
</TooltipProvider>
</Box>
</Box>
</Header>
<Box
className={classNames(css.MembersDrawer, ContainerColor({ variant: 'Background' }))}
shrink="No"
direction="Column"
>
<MemberDrawerHeader room={room} />
<Box className={css.MemberDrawerContentBase} grow="Yes">
<Scroll ref={scrollRef} variant="Background" size="300" visibility="Hover" hideTrack>
<Box className={css.MemberDrawerContent} direction="Column" gap="200">
@@ -334,60 +401,28 @@ 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 (
<MenuItem
<div
style={{
padding: `0 ${config.space.S200}`,
transform: `translateY(${vItem.start}px)`,
}}
aria-pressed={openProfileUserId === member.userId}
data-index={vItem.index}
data-user-id={member.userId}
ref={virtualizer.measureElement}
key={`${room.roomId}-${member.userId}`}
className={css.DrawerVirtualItem}
variant="Background"
radii="400"
onClick={handleMemberClick}
before={
<Avatar size="200">
<UserAvatar
userId={member.userId}
src={avatarUrl ?? undefined}
alt={name}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
}
after={
typingMembers.find((receipt) => receipt.userId === member.userId) && (
<Badge size="300" variant="Secondary" fill="Soft" radii="Pill" outlined>
<TypingIndicator size="300" />
</Badge>
)
}
data-index={vItem.index}
key={`${room.roomId}-${tagOrMember.userId}`}
ref={virtualizer.measureElement}
>
<Box grow="Yes">
<Text size="T400" truncate>
{name}
</Text>
</Box>
</MenuItem>
<MemberItem
mx={mx}
useAuthentication={useAuthentication}
room={room}
member={tagOrMember}
onClick={handleMemberClick}
pressed={openProfileUserId === tagOrMember.userId}
typing={typingMembers.some(
(receipt) => receipt.userId === tagOrMember.userId
)}
/>
</div>
);
})}
</div>

View File

@@ -108,21 +108,23 @@ import { ReplyLayout, ThreadIndicator } from '../../components/message';
import { roomToParentsAtom } from '../../state/room/roomToParents';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
import { powerLevelAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { 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<HTMLElement>;
roomId: string;
room: Room;
getPowerLevelTag: GetPowerLevelTag;
accessibleTagColors: Map<string, string>;
}
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
({ editor, fileDropContainerRef, roomId, room, getPowerLevelTag, accessibleTagColors }, ref) => {
({ editor, fileDropContainerRef, roomId, room }, ref) => {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
@@ -134,13 +136,24 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const emojiBtnRef = useRef<HTMLButtonElement>(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 replyPowerTag = getPowerLevelTag(powerLevelAPI.getPowerLevel(powerLevels, replyUserID));
const replyPowerColor = replyPowerTag.color
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
? accessibleTagColors.get(replyPowerTag.color)
: undefined;
const replyUsernameColor =
@@ -277,7 +290,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
});
handleCancelUpload(uploads);
const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises));
contents.forEach((content) => mx.sendMessage(roomId, content));
contents.forEach((content) => mx.sendMessage(roomId, content as any));
};
const submit = useCallback(() => {
@@ -356,7 +369,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
content['m.relates_to'].is_falling_back = false;
}
}
mx.sendMessage(roomId, content);
mx.sendMessage(roomId, content as any);
resetEditor(editor);
resetEditorHistory(editor);
setReplyDraft(undefined);

View File

@@ -101,7 +101,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 { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
import { useKeyDown } from '../../hooks/useKeyDown';
import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
@@ -117,10 +117,15 @@ 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) => (
@@ -223,8 +228,6 @@ type RoomTimelineProps = {
eventId?: string;
roomInputRef: RefObject<HTMLElement>;
editor: Editor;
getPowerLevelTag: GetPowerLevelTag;
accessibleTagColors: Map<string, string>;
};
const PAGINATION_LIMIT = 80;
@@ -427,14 +430,7 @@ const getRoomUnreadInfo = (room: Room, scrollTo = false) => {
};
};
export function RoomTimeline({
room,
eventId,
roomInputRef,
editor,
getPowerLevelTag,
accessibleTagColors,
}: RoomTimelineProps) {
export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
@@ -459,13 +455,24 @@ export function RoomTimeline({
const setReplyDraft = useSetAtom(roomIdToReplyDraftAtomFamily(room.roomId));
const powerLevels = usePowerLevelsContext();
const { canDoAction, canSendEvent, canSendStateEvent, getPowerLevel } =
usePowerLevelsAPI(powerLevels);
const creators = useRoomCreators(room);
const myPowerLevel = getPowerLevel(mx.getUserId() ?? '');
const canRedact = canDoAction('redact', myPowerLevel);
const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, myPowerLevel);
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 [editId, setEditId] = useState<string>();
const roomToParents = useAtomValue(roomToParentsAtom);
@@ -990,7 +997,7 @@ export function RoomTimeline({
(reactions.find(eventWithShortcode)?.getContent().shortcode as string | undefined);
mx.sendEvent(
room.roomId,
MessageEvent.Reaction,
MessageEvent.Reaction as any,
getReactionContent(targetEventId, key, rShortcode)
);
},
@@ -1025,7 +1032,6 @@ export function RoomTimeline({
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;
@@ -1059,9 +1065,8 @@ export function RoomTimeline({
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenReply}
getPowerLevel={getPowerLevel}
getPowerLevelTag={getPowerLevelTag}
accessibleTagColors={accessibleTagColors}
getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
/>
)
@@ -1080,8 +1085,8 @@ export function RoomTimeline({
}
hideReadReceipts={hideActivity}
showDeveloperTools={showDeveloperTools}
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors}
memberPowerTag={getMemberPowerTag(senderId)}
accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
@@ -1111,7 +1116,6 @@ export function RoomTimeline({
const hasReactions = reactions && reactions.length > 0;
const { replyEventId, threadRootId } = mEvent;
const highlighted = focusItem?.index === item && focusItem.highlight;
const senderPowerLevel = getPowerLevel(mEvent.getSender());
return (
<Message
@@ -1143,9 +1147,8 @@ export function RoomTimeline({
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenReply}
getPowerLevel={getPowerLevel}
getPowerLevelTag={getPowerLevelTag}
accessibleTagColors={accessibleTagColors}
getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
/>
)
@@ -1164,8 +1167,8 @@ export function RoomTimeline({
}
hideReadReceipts={hideActivity}
showDeveloperTools={showDeveloperTools}
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors}
memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')}
accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
@@ -1232,7 +1235,6 @@ export function RoomTimeline({
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
const hasReactions = reactions && reactions.length > 0;
const highlighted = focusItem?.index === item && focusItem.highlight;
const senderPowerLevel = getPowerLevel(mEvent.getSender());
return (
<Message
@@ -1268,8 +1270,8 @@ export function RoomTimeline({
}
hideReadReceipts={hideActivity}
showDeveloperTools={showDeveloperTools}
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
accessibleTagColors={accessibleTagColors}
memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')}
accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}

View File

@@ -5,7 +5,7 @@ import { ReactEditor } from 'slate-react';
import { isKeyHotkey } from 'is-hotkey';
import { useStateEvent } from '../../hooks/useStateEvent';
import { StateEvent } from '../../../types/matrix/room';
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useEditor } from '../../components/editor';
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
@@ -21,8 +21,8 @@ import { editableActiveElement } from '../../utils/dom';
import navigation from '../../../client/state/navigation';
import { settingsAtom } from '../../state/settings';
import { useSetting } from '../../state/hooks/settings';
import { useAccessibleTagColors, usePowerLevelTags } from '../../hooks/usePowerLevelTags';
import { useTheme } from '../../hooks/useTheme';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useRoomCreators } from '../../hooks/useRoomCreators';
const FN_KEYS_REGEX = /^F\d+$/;
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
@@ -70,15 +70,10 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
const tombstoneEvent = useStateEvent(room, StateEvent.RoomTombstone);
const powerLevels = usePowerLevelsContext();
const { getPowerLevel, canSendEvent } = usePowerLevelsAPI(powerLevels);
const myUserId = mx.getUserId();
const canMessage = myUserId
? canSendEvent(EventType.RoomMessage, getPowerLevel(myUserId))
: false;
const creators = useRoomCreators(room);
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
const theme = useTheme();
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
const permissions = useRoomPermissions(creators, powerLevels);
const canMessage = permissions.event(EventType.RoomMessage, mx.getSafeUserId());
useKeyDown(
window,
@@ -109,8 +104,6 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
eventId={eventId}
roomInputRef={roomInputRef}
editor={editor}
getPowerLevelTag={getPowerLevelTag}
accessibleTagColors={accessibleTagColors}
/>
<RoomViewTyping room={room} />
</Box>
@@ -131,8 +124,6 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
roomId={roomId}
fileDropContainerRef={roomViewRef}
ref={roomInputRef}
getPowerLevelTag={getPowerLevelTag}
accessibleTagColors={accessibleTagColors}
/>
)}
{!canMessage && (

View File

@@ -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 { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { markAsRead } from '../../../client/action/notifications';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { openInviteUser } from '../../../client/action/navigation';
@@ -67,6 +67,8 @@ 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;
@@ -77,8 +79,10 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevelsContext();
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
const canInvite = permissions.action('invite', mx.getSafeUserId());
const notificationPreferences = useRoomsNotificationPreferencesContext();
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
const { navigateRoom } = useRoomNavigate();

View File

@@ -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 { StateEvent } from '../../../../types/matrix/room';
import { getTagIconSrc, PowerLevelTag } from '../../../hooks/usePowerLevelTags';
import { MemberPowerTag, StateEvent } from '../../../../types/matrix/room';
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, pinContent);
mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents as any, pinContent);
onClose?.();
};
@@ -679,7 +679,7 @@ export type MessageProps = {
reactions?: ReactNode;
hideReadReceipts?: boolean;
showDeveloperTools?: boolean;
powerLevelTag?: PowerLevelTag;
memberPowerTag?: MemberPowerTag;
accessibleTagColors?: Map<string, string>;
legacyUsernameColor?: boolean;
hour24Clock: boolean;
@@ -710,7 +710,7 @@ export const Message = as<'div', MessageProps>(
reactions,
hideReadReceipts,
showDeveloperTools,
powerLevelTag,
memberPowerTag,
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 = powerLevelTag?.color
? accessibleTagColors?.get(powerLevelTag.color)
const tagColor = memberPowerTag?.color
? accessibleTagColors?.get(memberPowerTag.color)
: undefined;
const tagIconSrc = powerLevelTag?.icon
? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
const tagIconSrc = memberPowerTag?.icon
? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
: undefined;
const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor;

View File

@@ -69,18 +69,23 @@ import { Image } from '../../../components/media';
import { ImageViewer } from '../../../components/image-viewer';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { VirtualTile } from '../../../components/virtualizer';
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../../hooks/usePowerLevels';
import { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { ContainerColor } from '../../../styles/ContainerColor.css';
import {
getTagIconSrc,
useAccessibleTagColors,
usePowerLevelTags,
} from '../../../hooks/usePowerLevelTags';
import { 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;
@@ -88,22 +93,27 @@ type PinnedMessageProps = {
renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>;
onOpen: (roomId: string, eventId: string) => void;
canPinEvent: boolean;
getMemberPowerTag: GetMemberPowerTag;
accessibleTagColors: Map<string, string>;
legacyUsernameColor: boolean;
hour24Clock: boolean;
dateFormatString: string;
};
function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: PinnedMessageProps) {
function PinnedMessage({
room,
eventId,
renderContent,
onOpen,
canPinEvent,
getMemberPowerTag,
accessibleTagColors,
legacyUsernameColor,
hour24Clock,
dateFormatString,
}: 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(() => {
@@ -169,14 +179,15 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
const senderAvatarMxc = getMemberAvatarMxc(room, sender);
const getContent = (() => pinnedEvent.getContent()) as GetContentCallback;
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)
const memberPowerTag = getMemberPowerTag(sender);
const tagColor = memberPowerTag?.color
? accessibleTagColors?.get(memberPowerTag.color)
: undefined;
const tagIconSrc = memberPowerTag?.icon
? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
: undefined;
const usernameColor = legacyUsernameColor || direct ? colorMXID(sender) : tagColor;
const usernameColor = legacyUsernameColor ? colorMXID(sender) : tagColor;
return (
<ModernLayout
@@ -222,8 +233,7 @@ function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: Pi
replyEventId={pinnedEvent.replyEventId}
threadRootId={pinnedEvent.threadRootId}
onClick={handleOpenClick}
getPowerLevel={getPowerLevel}
getPowerLevelTag={getPowerLevelTag}
getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor}
/>
@@ -242,14 +252,34 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
const mx = useMatrixClient();
const userId = mx.getUserId()!;
const powerLevels = usePowerLevelsContext();
const { canSendStateEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels);
const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, getPowerLevel(userId));
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 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<HTMLDivElement>(null);
@@ -464,6 +494,11 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
renderContent={renderMatrixEvent}
onOpen={handleOpen}
canPinEvent={canPinEvent}
getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
</SequenceCard>
</VirtualTile>