diff --git a/src/app/components/scroll-top-container/ScrollTopContainer.tsx b/src/app/components/scroll-top-container/ScrollTopContainer.tsx new file mode 100644 index 00000000..08547112 --- /dev/null +++ b/src/app/components/scroll-top-container/ScrollTopContainer.tsx @@ -0,0 +1,35 @@ +import React, { RefObject, useCallback, useState } from 'react'; +import { Box, as } from 'folds'; +import classNames from 'classnames'; +import * as css from './style.css'; +import { + getIntersectionObserverEntry, + useIntersectionObserver, +} from '../../hooks/useIntersectionObserver'; + +export const ScrollTopContainer = as< + 'div', + { + scrollRef?: RefObject; + anchorRef: RefObject; + } +>(({ className, scrollRef, anchorRef, ...props }, ref) => { + const [onTop, setOnTop] = useState(true); + + useIntersectionObserver( + useCallback( + (intersectionEntries) => { + if (!anchorRef.current) return; + const entry = getIntersectionObserverEntry(anchorRef.current, intersectionEntries); + if (entry) setOnTop(entry.isIntersecting); + }, + [anchorRef] + ), + useCallback(() => ({ root: scrollRef?.current }), [scrollRef]), + useCallback(() => anchorRef.current, [anchorRef]) + ); + + if (onTop) return null; + + return ; +}); diff --git a/src/app/components/scroll-top-container/index.ts b/src/app/components/scroll-top-container/index.ts new file mode 100644 index 00000000..392e1b23 --- /dev/null +++ b/src/app/components/scroll-top-container/index.ts @@ -0,0 +1 @@ +export * from './ScrollTopContainer'; diff --git a/src/app/components/scroll-top-container/style.css.ts b/src/app/components/scroll-top-container/style.css.ts new file mode 100644 index 00000000..7b1269cb --- /dev/null +++ b/src/app/components/scroll-top-container/style.css.ts @@ -0,0 +1,20 @@ +import { keyframes, style } from '@vanilla-extract/css'; +import { config } from 'folds'; + +const ScrollContainerAnime = keyframes({ + '0%': { + transform: `translate(-50%, -100%) scale(0)`, + }, + '100%': { + transform: `translate(-50%, 0) scale(1)`, + }, +}); + +export const ScrollTopContainer = style({ + position: 'absolute', + top: config.space.S200, + left: '50%', + transform: 'translateX(-50%)', + zIndex: 1, + animation: `${ScrollContainerAnime} 100ms`, +}); diff --git a/src/app/organisms/room/MembersDrawer.tsx b/src/app/organisms/room/MembersDrawer.tsx index 29f52b8a..54a68ae9 100644 --- a/src/app/organisms/room/MembersDrawer.tsx +++ b/src/app/organisms/room/MembersDrawer.tsx @@ -39,10 +39,6 @@ import { openInviteUser, openProfileViewer } from '../../../client/action/naviga 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 { @@ -60,6 +56,7 @@ import { getMxIdLocalPart } from '../../utils/matrix'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { millify } from '../../plugins/millify'; +import { ScrollTopContainer } from '../../components/scroll-top-container'; export const MembershipFilters = { filterJoined: (m: RoomMember) => m.membership === Membership.Join, @@ -190,8 +187,6 @@ export function MembersDrawer({ room }: MembersDrawerProps) { const membershipFilter = membershipFilterMenu[membershipFilterIndex] ?? membershipFilterMenu[0]; const sortFilter = sortFilterMenu[sortFilterIndex] ?? sortFilterMenu[0]; - const [onTop, setOnTop] = useState(true); - const typingMembers = useAtomValue( useMemo(() => selectRoomTypingMembersAtom(room.roomId, roomIdToTypingMembersAtom), [room]) ); @@ -235,16 +230,6 @@ export function MembersDrawer({ room }: MembersDrawerProps) { 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 = useDebounce( useCallback( (evt) => { @@ -446,20 +431,18 @@ export function MembersDrawer({ room }: MembersDrawerProps) { - {!onTop && ( - - virtualizer.scrollToOffset(0)} - variant="Surface" - radii="Pill" - outlined - size="300" - aria-label="Scroll to Top" - > - - - - )} + + virtualizer.scrollToOffset(0)} + variant="Surface" + radii="Pill" + outlined + size="300" + aria-label="Scroll to Top" + > + + + {!fetchingMembers && !result && processMembers.length === 0 && ( diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx index 36cf78fb..00a120fe 100644 --- a/src/app/pages/client/inbox/Notifications.tsx +++ b/src/app/pages/client/inbox/Notifications.tsx @@ -1,5 +1,17 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Avatar, Box, Chip, Header, Icon, Icons, Scroll, Text, config, toRem } from 'folds'; +import { + Avatar, + Box, + Chip, + Header, + Icon, + IconButton, + Icons, + Scroll, + Text, + config, + toRem, +} from 'folds'; import { useSearchParams } from 'react-router-dom'; import { INotification, INotificationsResponse, Method } from 'matrix-js-sdk'; import { useVirtualizer } from '@tanstack/react-virtual'; @@ -12,6 +24,7 @@ import { SequenceCard } from '../../../components/sequence-card'; import { RoomAvatar } from '../../../components/room-avatar'; import { nameInitials } from '../../../utils/common'; import { getRoomAvatarUrl } from '../../../utils/room'; +import { ScrollTopContainer } from '../../../components/scroll-top-container'; type RoomNotificationsGroup = { roomId: string; @@ -94,6 +107,7 @@ export function Notifications() { const [searchParams, setSearchParams] = useSearchParams(); const notificationsSearchParams = getNotificationsSearchParams(searchParams); const scrollRef = useRef(null); + const scrollTopAnchorRef = useRef(null); const onlyHighlight = notificationsSearchParams.only === 'highlight'; const setOnlyHighlighted = (highlight: boolean) => { @@ -146,12 +160,12 @@ export function Notifications() { - + - + Filter @@ -187,6 +201,18 @@ export function Notifications() { + + virtualizer.scrollToOffset(0)} + variant="Surface" + radii="Pill" + outlined + size="300" + aria-label="Scroll to Top" + > + + +