mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-17 20:50:29 +03:00
Improve Members Right Panel (#1286)
* fix room members hook * fix resize observer hook * add intersection observer hook * install react-virtual lib * improve right panel - WIP * add filters for members * fix bug in async search * categories members and add search * show spinner on room member fetch * make invite member btn clickable * so no member text * add line between room view and member drawer * fix imports * add screen size hook * fix set setting hook * make member drawer responsive * extract power level tags hook * fix room members hook * fix use async search api * produce search result on filter change
This commit is contained in:
parent
da32d0d9e7
commit
c07905c360
19 changed files with 984 additions and 79 deletions
64
src/app/organisms/room/MembersDrawer.css.ts
Normal file
64
src/app/organisms/room/MembersDrawer.css.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
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({
|
||||
flexShrink: 0,
|
||||
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
});
|
||||
|
||||
export const MemberDrawerContentBase = style({
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const MemberDrawerContent = style({
|
||||
padding: `${config.space.S300} 0`,
|
||||
});
|
||||
|
||||
const ScrollBtnAnime = keyframes({
|
||||
'0%': {
|
||||
transform: `translate(-50%, -100%) scale(0)`,
|
||||
},
|
||||
'100%': {
|
||||
transform: `translate(-50%, 0) scale(1)`,
|
||||
},
|
||||
});
|
||||
|
||||
export const DrawerScrollTop = style({
|
||||
position: 'absolute',
|
||||
top: config.space.S200,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 1,
|
||||
animation: `${ScrollBtnAnime} 100ms`,
|
||||
});
|
||||
|
||||
export const DrawerGroup = style({
|
||||
padding: `0 ${config.space.S100} 0 ${config.space.S300}`,
|
||||
});
|
||||
|
||||
export const MembersGroup = style({
|
||||
paddingLeft: config.space.S200,
|
||||
});
|
||||
export const MembersGroupLabel = style({
|
||||
padding: config.space.S200,
|
||||
selectors: {
|
||||
'&:not(:first-child)': {
|
||||
paddingTop: config.space.S500,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const DrawerVirtualItem = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
});
|
||||
528
src/app/organisms/room/MembersDrawer.tsx
Normal file
528
src/app/organisms/room/MembersDrawer.tsx
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
import React, {
|
||||
ChangeEventHandler,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
Box,
|
||||
Chip,
|
||||
ContainerColor,
|
||||
Header,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Input,
|
||||
Menu,
|
||||
MenuItem,
|
||||
PopOut,
|
||||
Scroll,
|
||||
Spinner,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
config,
|
||||
} from 'folds';
|
||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import millify from 'millify';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
|
||||
import * as css from './MembersDrawer.css';
|
||||
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import {
|
||||
getIntersectionObserverEntry,
|
||||
useIntersectionObserver,
|
||||
} from '../../hooks/useIntersectionObserver';
|
||||
import { Membership } from '../../../types/matrix/room';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { UseAsyncSearchOptions, useAsyncSearch } from '../../hooks/useAsyncSearch';
|
||||
import { useDebounce } from '../../hooks/useDebounce';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { usePowerLevelTags, PowerLevelTag } from '../../hooks/usePowerLevelTags';
|
||||
|
||||
export const MembershipFilters = {
|
||||
filterJoined: (m: RoomMember) => m.membership === Membership.Join,
|
||||
filterInvited: (m: RoomMember) => m.membership === Membership.Invite,
|
||||
filterLeaved: (m: RoomMember) =>
|
||||
m.membership === Membership.Leave &&
|
||||
m.events.member?.getStateKey() === m.events.member?.getSender(),
|
||||
filterKicked: (m: RoomMember) =>
|
||||
m.membership === Membership.Leave &&
|
||||
m.events.member?.getStateKey() !== m.events.member?.getSender(),
|
||||
filterBanned: (m: RoomMember) => m.membership === Membership.Ban,
|
||||
};
|
||||
|
||||
export type MembershipFilterFn = (m: RoomMember) => boolean;
|
||||
|
||||
export type MembershipFilter = {
|
||||
name: string;
|
||||
filterFn: MembershipFilterFn;
|
||||
color: ContainerColor;
|
||||
};
|
||||
|
||||
const useMembershipFilterMenu = (): MembershipFilter[] =>
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'Joined',
|
||||
filterFn: MembershipFilters.filterJoined,
|
||||
color: 'Surface',
|
||||
},
|
||||
{
|
||||
name: 'Invited',
|
||||
filterFn: MembershipFilters.filterInvited,
|
||||
color: 'Success',
|
||||
},
|
||||
{
|
||||
name: 'Left',
|
||||
filterFn: MembershipFilters.filterLeaved,
|
||||
color: 'Secondary',
|
||||
},
|
||||
{
|
||||
name: 'Kicked',
|
||||
filterFn: MembershipFilters.filterKicked,
|
||||
color: 'Warning',
|
||||
},
|
||||
{
|
||||
name: 'Banned',
|
||||
filterFn: MembershipFilters.filterBanned,
|
||||
color: 'Critical',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
export const SortFilters = {
|
||||
filterAscending: (a: RoomMember, b: RoomMember) =>
|
||||
a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1,
|
||||
filterDescending: (a: RoomMember, b: RoomMember) =>
|
||||
a.name.toLowerCase() > b.name.toLowerCase() ? -1 : 1,
|
||||
filterNewestFirst: (a: RoomMember, b: RoomMember) =>
|
||||
(b.events.member?.getTs() ?? 0) - (a.events.member?.getTs() ?? 0),
|
||||
filterOldest: (a: RoomMember, b: RoomMember) =>
|
||||
(a.events.member?.getTs() ?? 0) - (b.events.member?.getTs() ?? 0),
|
||||
};
|
||||
|
||||
export type SortFilterFn = (a: RoomMember, b: RoomMember) => number;
|
||||
|
||||
export type SortFilter = {
|
||||
name: string;
|
||||
filterFn: SortFilterFn;
|
||||
};
|
||||
|
||||
const useSortFilterMenu = (): SortFilter[] =>
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'A to Z',
|
||||
filterFn: SortFilters.filterAscending,
|
||||
},
|
||||
{
|
||||
name: 'Z to A',
|
||||
filterFn: SortFilters.filterDescending,
|
||||
},
|
||||
{
|
||||
name: 'Newest First',
|
||||
filterFn: SortFilters.filterNewestFirst,
|
||||
},
|
||||
{
|
||||
name: 'Oldest First',
|
||||
filterFn: SortFilters.filterOldest,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
export type MembersFilterOptions = {
|
||||
membershipFilter: MembershipFilter;
|
||||
sortFilter: SortFilter;
|
||||
};
|
||||
|
||||
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
|
||||
limit: 100,
|
||||
matchOptions: {
|
||||
contain: true,
|
||||
},
|
||||
};
|
||||
const getMemberItemStr = (m: RoomMember) => [m.name, m.userId];
|
||||
|
||||
type MembersDrawerProps = {
|
||||
room: Room;
|
||||
};
|
||||
export function MembersDrawer({ room }: MembersDrawerProps) {
|
||||
const mx = useMatrixClient();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const members = useRoomMembers(mx, room.roomId);
|
||||
const getPowerLevelTag = usePowerLevelTags();
|
||||
const fetchingMembers = members.length < room.getJoinedMemberCount();
|
||||
|
||||
const membershipFilterMenu = useMembershipFilterMenu();
|
||||
const sortFilterMenu = useSortFilterMenu();
|
||||
const [filter, setFilter] = useState<MembersFilterOptions>({
|
||||
membershipFilter: membershipFilterMenu[0],
|
||||
sortFilter: sortFilterMenu[0],
|
||||
});
|
||||
const [onTop, setOnTop] = useState(true);
|
||||
|
||||
const filteredMembers = useMemo(
|
||||
() =>
|
||||
members
|
||||
.filter(filter.membershipFilter.filterFn)
|
||||
.sort(filter.sortFilter.filterFn)
|
||||
.sort((a, b) => b.powerLevel - a.powerLevel),
|
||||
[members, filter]
|
||||
);
|
||||
|
||||
const [result, search, resetSearch] = useAsyncSearch(
|
||||
filteredMembers,
|
||||
getMemberItemStr,
|
||||
SEARCH_OPTIONS
|
||||
);
|
||||
if (!result && searchInputRef.current?.value) search(searchInputRef.current.value);
|
||||
|
||||
const processMembers = result ? result.items : filteredMembers;
|
||||
|
||||
const PLTagOrRoomMember = useMemo(() => {
|
||||
let prevTag: PowerLevelTag | undefined;
|
||||
const tagOrMember: Array<PowerLevelTag | RoomMember> = [];
|
||||
processMembers.forEach((m) => {
|
||||
const plTag = getPowerLevelTag(m.powerLevel);
|
||||
if (plTag !== prevTag) {
|
||||
prevTag = plTag;
|
||||
tagOrMember.push(plTag);
|
||||
}
|
||||
tagOrMember.push(m);
|
||||
});
|
||||
return tagOrMember;
|
||||
}, [processMembers, getPowerLevelTag]);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: PLTagOrRoomMember.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 40,
|
||||
overscan: 10,
|
||||
});
|
||||
|
||||
useIntersectionObserver(
|
||||
useCallback((intersectionEntries) => {
|
||||
if (!scrollTopAnchorRef.current) return;
|
||||
const entry = getIntersectionObserverEntry(scrollTopAnchorRef.current, intersectionEntries);
|
||||
if (entry) setOnTop(entry.isIntersecting);
|
||||
}, []),
|
||||
useCallback(() => ({ root: scrollRef.current }), []),
|
||||
useCallback(() => scrollTopAnchorRef.current, [])
|
||||
);
|
||||
|
||||
const handleSearchChange: ChangeEventHandler<HTMLInputElement> = useDebounce(
|
||||
useCallback(
|
||||
(evt) => {
|
||||
if (evt.target.value) search(evt.target.value);
|
||||
else resetSearch();
|
||||
},
|
||||
[search, resetSearch]
|
||||
),
|
||||
{ wait: 200 }
|
||||
);
|
||||
|
||||
const handleMemberClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
const btn = evt.currentTarget as HTMLButtonElement;
|
||||
const userId = btn.getAttribute('data-user-id');
|
||||
openProfileViewer(userId, room.roomId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={css.MembersDrawer} 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 size="H5" truncate>
|
||||
{`${millify(room.getJoinedMemberCount(), { precision: 1 })} Members`}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No" alignItems="Center">
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
align="End"
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>Invite Member</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton
|
||||
ref={triggerRef}
|
||||
variant="Background"
|
||||
onClick={() => openInviteUser(room.roomId)}
|
||||
>
|
||||
<Icon src={Icons.UserPlus} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</Box>
|
||||
</Box>
|
||||
</Header>
|
||||
<Box className={css.MemberDrawerContentBase} grow="Yes">
|
||||
<Scroll ref={scrollRef} variant="Background" size="300" visibility="Hover">
|
||||
<Box className={css.MemberDrawerContent} direction="Column" gap="400">
|
||||
<Box className={css.DrawerGroup} direction="Column" gap="100">
|
||||
<Text size="L400">Filter</Text>
|
||||
<Box alignItems="Center" gap="100" wrap="Wrap">
|
||||
<UseStateProvider initial={false}>
|
||||
{(open, setOpen) => (
|
||||
<PopOut
|
||||
open={open}
|
||||
position="Bottom"
|
||||
align="Start"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setOpen(false),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
}}
|
||||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
{membershipFilterMenu.map((menuItem) => (
|
||||
<MenuItem
|
||||
key={menuItem.name}
|
||||
variant={
|
||||
menuItem.name === filter.membershipFilter.name
|
||||
? menuItem.color
|
||||
: 'Surface'
|
||||
}
|
||||
radii="300"
|
||||
onClick={() => {
|
||||
setFilter((f) => ({ ...f, membershipFilter: menuItem }));
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Text>{menuItem.name}</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
{(anchorRef) => (
|
||||
<Chip
|
||||
ref={anchorRef}
|
||||
onClick={() => setOpen(!open)}
|
||||
variant={filter.membershipFilter.color}
|
||||
radii="400"
|
||||
outlined
|
||||
after={<Icon src={Icons.ChevronBottom} size="50" />}
|
||||
>
|
||||
<Text size="T200">{filter.membershipFilter.name}</Text>
|
||||
</Chip>
|
||||
)}
|
||||
</PopOut>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
<UseStateProvider initial={false}>
|
||||
{(open, setOpen) => (
|
||||
<PopOut
|
||||
open={open}
|
||||
position="Bottom"
|
||||
align="Start"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setOpen(false),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
}}
|
||||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
{sortFilterMenu.map((menuItem) => (
|
||||
<MenuItem
|
||||
key={menuItem.name}
|
||||
variant="Surface"
|
||||
aria-pressed={menuItem.name === filter.sortFilter.name}
|
||||
radii="300"
|
||||
onClick={() => {
|
||||
setFilter((f) => ({ ...f, sortFilter: menuItem }));
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Text>{menuItem.name}</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
{(anchorRef) => (
|
||||
<Chip
|
||||
ref={anchorRef}
|
||||
onClick={() => setOpen(!open)}
|
||||
variant="Surface"
|
||||
radii="400"
|
||||
outlined
|
||||
after={<Icon src={Icons.ChevronBottom} size="50" />}
|
||||
>
|
||||
<Text size="T200">{`Order: ${filter.sortFilter.name}`}</Text>
|
||||
</Chip>
|
||||
)}
|
||||
</PopOut>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box ref={scrollTopAnchorRef} className={css.DrawerGroup} direction="Column" gap="100">
|
||||
<Text size="L400">Search</Text>
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
onChange={handleSearchChange}
|
||||
style={{ paddingRight: config.space.S200 }}
|
||||
placeholder="Type name..."
|
||||
variant="Surface"
|
||||
size="400"
|
||||
outlined
|
||||
radii="400"
|
||||
before={<Icon size="50" src={Icons.Search} />}
|
||||
after={
|
||||
result && (
|
||||
<Chip
|
||||
variant={result.items.length > 0 ? 'Success' : 'Critical'}
|
||||
size="400"
|
||||
radii="Pill"
|
||||
onClick={() => {
|
||||
if (searchInputRef.current) searchInputRef.current.value = '';
|
||||
resetSearch();
|
||||
}}
|
||||
after={<Icon size="50" src={Icons.Cross} />}
|
||||
>
|
||||
<Text size="B300">{`${result.items.length || 'No'} ${
|
||||
result.items.length === 1 ? 'Result' : 'Results'
|
||||
}`}</Text>
|
||||
</Chip>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{!onTop && (
|
||||
<Box className={css.DrawerScrollTop}>
|
||||
<IconButton
|
||||
onClick={() => virtualizer.scrollToOffset(0)}
|
||||
variant="Surface"
|
||||
radii="Pill"
|
||||
outlined
|
||||
size="300"
|
||||
aria-label="Scroll to Top"
|
||||
>
|
||||
<Icon src={Icons.ChevronTop} size="300" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!fetchingMembers && !result && processMembers.length === 0 && (
|
||||
<Text style={{ padding: config.space.S300 }} align="Center">
|
||||
{`No "${filter.membershipFilter.name}" Members`}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box className={css.MembersGroup} direction="Column" gap="100">
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: virtualizer.getTotalSize(),
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((vItem) => {
|
||||
const tagOrMember = PLTagOrRoomMember[vItem.index];
|
||||
if (!('userId' in tagOrMember)) {
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
transform: `translateY(${vItem.start}px)`,
|
||||
}}
|
||||
data-index={vItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
key={`${room.roomId}-${vItem.index}`}
|
||||
className={classNames(css.MembersGroupLabel, css.DrawerVirtualItem)}
|
||||
size="O400"
|
||||
>
|
||||
{tagOrMember.name}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const member = tagOrMember;
|
||||
const avatarUrl = member.getAvatarUrl(
|
||||
mx.baseUrl,
|
||||
100,
|
||||
100,
|
||||
'crop',
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
style={{
|
||||
padding: config.space.S200,
|
||||
transform: `translateY(${vItem.start}px)`,
|
||||
}}
|
||||
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">
|
||||
{avatarUrl ? (
|
||||
<AvatarImage src={avatarUrl} />
|
||||
) : (
|
||||
<AvatarFallback
|
||||
style={{
|
||||
background: colorMXID(member.userId),
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<Text size="T200">{member.name[0]}</Text>
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
<Text size="T400" truncate>
|
||||
{member.name}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{fetchingMembers && (
|
||||
<Box justifyContent="Center">
|
||||
<Spinner />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import './Room.scss';
|
||||
import { Line } from 'folds';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import settings from '../../../client/state/settings';
|
||||
import RoomTimeline from '../../../client/state/RoomTimeline';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { openNavigation } from '../../../client/action/navigation';
|
||||
|
|
@ -11,7 +11,10 @@ import { openNavigation } from '../../../client/action/navigation';
|
|||
import Welcome from '../welcome/Welcome';
|
||||
import RoomView from './RoomView';
|
||||
import RoomSettings from './RoomSettings';
|
||||
import PeopleDrawer from './PeopleDrawer';
|
||||
import { MembersDrawer } from './MembersDrawer';
|
||||
import { ScreenSize, useScreenSize } from '../../hooks/useScreenSize';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
|
||||
function Room() {
|
||||
const [roomInfo, setRoomInfo] = useState({
|
||||
|
|
@ -19,7 +22,8 @@ function Room() {
|
|||
roomTimeline: null,
|
||||
eventId: null,
|
||||
});
|
||||
const [isDrawer, setIsDrawer] = useState(settings.isPeopleDrawer);
|
||||
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [screenSize] = useScreenSize();
|
||||
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
||||
|
|
@ -49,14 +53,6 @@ function Room() {
|
|||
};
|
||||
}, [roomInfo, mx]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleDrawerToggling = (visiblity) => setIsDrawer(visiblity);
|
||||
settings.on(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
||||
return () => {
|
||||
settings.removeListener(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { room, roomTimeline, eventId } = roomInfo;
|
||||
if (roomTimeline === null) {
|
||||
setTimeout(() => openNavigation());
|
||||
|
|
@ -69,7 +65,13 @@ function Room() {
|
|||
<RoomSettings roomId={roomTimeline.roomId} />
|
||||
<RoomView room={room} roomTimeline={roomTimeline} eventId={eventId} />
|
||||
</div>
|
||||
{isDrawer && <PeopleDrawer roomId={roomTimeline.roomId} />}
|
||||
|
||||
{screenSize === ScreenSize.Desktop && isDrawer && (
|
||||
<>
|
||||
<Line variant="Background" direction="Vertical" size="300" />
|
||||
<MembersDrawer room={room} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ import { MessageReply } from '../../molecules/message/Message';
|
|||
import colorMXID from '../../../util/colorMXID';
|
||||
import { parseReplyBody, parseReplyFormattedBody } from '../../utils/room';
|
||||
import { sanitizeText } from '../../utils/sanitize';
|
||||
import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver';
|
||||
import { useScreenSize } from '../../hooks/useScreenSize';
|
||||
|
||||
interface RoomInputProps {
|
||||
roomViewRef: RefObject<HTMLElement>;
|
||||
|
|
@ -161,15 +161,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
const handlePaste = useFilePasteHandler(handleFiles);
|
||||
const dropZoneVisible = useFileDropZone(roomViewRef, handleFiles);
|
||||
|
||||
const [mobile, setMobile] = useState(document.body.clientWidth < 500);
|
||||
useResizeObserver(
|
||||
document.body,
|
||||
useCallback((entries) => {
|
||||
const bodyEntry = getResizeObserverEntry(document.body, entries);
|
||||
if (bodyEntry && bodyEntry.contentRect.width < 500) setMobile(true);
|
||||
else setMobile(false);
|
||||
}, [])
|
||||
);
|
||||
const [, screenWidth] = useScreenSize();
|
||||
const hideStickerBtn = screenWidth < 500;
|
||||
|
||||
useEffect(() => {
|
||||
Transforms.insertFragment(editor, msgDraft);
|
||||
|
|
@ -515,7 +508,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
>
|
||||
{(anchorRef) => (
|
||||
<>
|
||||
{!mobile && (
|
||||
{!hideStickerBtn && (
|
||||
<IconButton
|
||||
aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
|
||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Sticker)}
|
||||
|
|
@ -532,7 +525,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
<IconButton
|
||||
ref={anchorRef}
|
||||
aria-pressed={
|
||||
mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
||||
hideStickerBtn ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
||||
}
|
||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
|
||||
variant="SurfaceVariant"
|
||||
|
|
@ -542,7 +535,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
<Icon
|
||||
src={Icons.Smile}
|
||||
filled={
|
||||
mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
||||
hideStickerBtn
|
||||
? !!emojiBoardTab
|
||||
: emojiBoardTab === EmojiBoardTab.Emoji
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
|
|
|
|||
|
|
@ -486,7 +486,6 @@ function RoomViewContent({ roomInputRef, eventId, roomTimeline }) {
|
|||
}, [newEvent]);
|
||||
|
||||
useResizeObserver(
|
||||
roomInputRef.current,
|
||||
useCallback((entries) => {
|
||||
if (!roomInputRef.current) return;
|
||||
const editorBaseEntry = getResizeObserverEntry(roomInputRef.current, entries);
|
||||
|
|
@ -497,7 +496,8 @@ function RoomViewContent({ roomInputRef, eventId, roomTimeline }) {
|
|||
if (timelineScroll.bottom < 40 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
|
||||
timelineScroll.scrollToBottom();
|
||||
}
|
||||
}, [roomInputRef])
|
||||
}, [roomInputRef]),
|
||||
useCallback(() => roomInputRef.current, [roomInputRef]),
|
||||
);
|
||||
|
||||
const listenKeyboard = useCallback((event) => {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@ import { blurOnBubbling } from '../../atoms/button/script';
|
|||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';
|
||||
import { togglePeopleDrawer } from '../../../client/action/settings';
|
||||
import {
|
||||
toggleRoomSettings,
|
||||
openReusableContextMenu,
|
||||
openNavigation,
|
||||
} from '../../../client/action/navigation';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
|
|
@ -28,23 +31,26 @@ import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg
|
|||
import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg';
|
||||
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
import { useSetSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
|
||||
function RoomViewHeader({ roomId }) {
|
||||
const [, forceUpdate] = useForceUpdate();
|
||||
const mx = initMatrix.matrixClient;
|
||||
const isDM = initMatrix.roomList.directs.has(roomId);
|
||||
const room = mx.getRoom(roomId);
|
||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
||||
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
|
||||
avatarSrc = isDM
|
||||
? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop')
|
||||
: avatarSrc;
|
||||
const roomName = room.name;
|
||||
|
||||
const roomHeaderBtnRef = useRef(null);
|
||||
useEffect(() => {
|
||||
const settingsToggle = (isVisibile) => {
|
||||
const rawIcon = roomHeaderBtnRef.current.lastElementChild;
|
||||
rawIcon.style.transform = isVisibile
|
||||
? 'rotateX(180deg)'
|
||||
: 'rotateX(0deg)';
|
||||
rawIcon.style.transform = isVisibile ? 'rotateX(180deg)' : 'rotateX(0deg)';
|
||||
};
|
||||
navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
|
||||
return () => {
|
||||
|
|
@ -66,11 +72,9 @@ function RoomViewHeader({ roomId }) {
|
|||
}, [roomId]);
|
||||
|
||||
const openRoomOptions = (e) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(e, '.ic-btn'),
|
||||
(closeMenu) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
|
||||
);
|
||||
openReusableContextMenu('bottom', getEventCords(e, '.ic-btn'), (closeMenu) => (
|
||||
<RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -90,18 +94,34 @@ function RoomViewHeader({ roomId }) {
|
|||
>
|
||||
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
|
||||
<TitleWrapper>
|
||||
<Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
|
||||
<Text variant="h2" weight="medium" primary>
|
||||
{twemojify(roomName)}
|
||||
</Text>
|
||||
</TitleWrapper>
|
||||
<RawIcon src={ChevronBottomIC} />
|
||||
</button>
|
||||
{mx.isRoomEncrypted(roomId) === false && <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />}
|
||||
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
||||
<IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} />
|
||||
{mx.isRoomEncrypted(roomId) === false && (
|
||||
<IconButton
|
||||
onClick={() => toggleRoomSettings(tabText.SEARCH)}
|
||||
tooltip="Search"
|
||||
src={SearchIC}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={openRoomOptions}
|
||||
tooltip="Options"
|
||||
src={VerticalMenuIC}
|
||||
className="room-header__drawer-btn"
|
||||
onClick={() => {
|
||||
setPeopleDrawer((t) => !t);
|
||||
}}
|
||||
tooltip="People"
|
||||
src={UserIC}
|
||||
/>
|
||||
<IconButton
|
||||
className="room-header__members-btn"
|
||||
onClick={() => toggleRoomSettings(tabText.MEMBERS)}
|
||||
tooltip="Members"
|
||||
src={UserIC}
|
||||
/>
|
||||
<IconButton onClick={openRoomOptions} tooltip="Options" src={VerticalMenuIC} />
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue