diff --git a/src/app/components/BackRouteHandler.tsx b/src/app/components/BackRouteHandler.tsx
index fa3d759292..2e2eb02db3 100644
--- a/src/app/components/BackRouteHandler.tsx
+++ b/src/app/components/BackRouteHandler.tsx
@@ -1,86 +1,10 @@
-import { ReactNode, useCallback } from 'react';
-import { matchPath, useLocation, useNavigate } from 'react-router-dom';
-import {
- getDirectPath,
- getExplorePath,
- getHomePath,
- getInboxPath,
- getSpacePath,
-} from '../pages/pathUtils';
-import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from '../pages/paths';
+import { ReactNode } from 'react';
+import { useGoBack } from '../hooks/useGoBack';
type BackRouteHandlerProps = {
children: (onBack: () => void) => ReactNode;
};
export function BackRouteHandler({ children }: BackRouteHandlerProps) {
- const navigate = useNavigate();
- const location = useLocation();
-
- const goBack = useCallback(() => {
- if (
- matchPath(
- {
- path: HOME_PATH,
- caseSensitive: true,
- end: false,
- },
- location.pathname
- )
- ) {
- navigate(getHomePath());
- return;
- }
- if (
- matchPath(
- {
- path: DIRECT_PATH,
- caseSensitive: true,
- end: false,
- },
- location.pathname
- )
- ) {
- navigate(getDirectPath());
- return;
- }
- const spaceMatch = matchPath(
- {
- path: SPACE_PATH,
- caseSensitive: true,
- end: false,
- },
- location.pathname
- );
- if (spaceMatch?.params.spaceIdOrAlias) {
- navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias));
- return;
- }
- if (
- matchPath(
- {
- path: EXPLORE_PATH,
- caseSensitive: true,
- end: false,
- },
- location.pathname
- )
- ) {
- navigate(getExplorePath());
- return;
- }
- if (
- matchPath(
- {
- path: INBOX_PATH,
- caseSensitive: true,
- end: false,
- },
- location.pathname
- )
- ) {
- navigate(getInboxPath());
- }
- }, [navigate, location]);
-
+ const goBack = useGoBack();
return children(goBack);
}
diff --git a/src/app/components/SlideMenuChild.tsx b/src/app/components/SlideMenuChild.tsx
new file mode 100644
index 0000000000..0add2306be
--- /dev/null
+++ b/src/app/components/SlideMenuChild.tsx
@@ -0,0 +1,23 @@
+import { ReactNode } from "react";
+import { matchPath, useLocation } from "react-router-dom";
+import { HOME_PATH, DIRECT_PATH, SPACE_PATH, EXPLORE_PATH, INBOX_PATH } from "../pages/paths";
+import React from "react";
+import { Direct } from "../pages/client/direct";
+import { Explore } from "../pages/client/explore";
+import { Home } from "../pages/client/home";
+import { Inbox } from "../pages/client/inbox";
+import { Space } from "../pages/client/space";
+
+// A component to match path and return the corresponding slide menu parent.
+export function SlideMenuChild() {
+ const location = useLocation();
+
+ let previousComponent: ReactNode;
+ if (matchPath({ path: HOME_PATH, caseSensitive: true, end: false, }, location.pathname)) previousComponent = ;
+ else if (matchPath({ path: DIRECT_PATH, caseSensitive: true, end: false, }, location.pathname)) previousComponent = ;
+ else if (matchPath({ path: EXPLORE_PATH, caseSensitive: true, end: false, }, location.pathname)) previousComponent = ;
+ else if (matchPath({ path: INBOX_PATH, caseSensitive: true, end: false, }, location.pathname)) previousComponent = ;
+ else if (matchPath({ path: SPACE_PATH, caseSensitive: true, end: false, }, location.pathname)) previousComponent = ;
+
+ return previousComponent;
+}
\ No newline at end of file
diff --git a/src/app/components/page/Page.tsx b/src/app/components/page/Page.tsx
index a8b9ea0446..85b8ba010c 100644
--- a/src/app/components/page/Page.tsx
+++ b/src/app/components/page/Page.tsx
@@ -1,4 +1,4 @@
-import React, { ComponentProps, MutableRefObject, ReactNode } from 'react';
+import React, { ComponentProps, CSSProperties, MutableRefObject, ReactNode } from 'react';
import { Box, Header, Line, Scroll, Text, as } from 'folds';
import classNames from 'classnames';
import { ContainerColor } from '../../styles/ContainerColor.css';
@@ -146,3 +146,12 @@ export function PageHero({
export const PageContentCenter = as<'div'>(({ className, ...props }, ref) => (
));
+
+// Only used in mobile for slide menu
+export function PageRootFloat({ children, style }: { children: ReactNode, style: CSSProperties }) {
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/components/page/style.css.ts b/src/app/components/page/style.css.ts
index 23f2da4941..f104ea967c 100644
--- a/src/app/components/page/style.css.ts
+++ b/src/app/components/page/style.css.ts
@@ -78,3 +78,13 @@ export const PageContentCenter = style([
margin: 'auto',
},
]);
+
+export const PageRootFloat = style([
+ DefaultReset,
+ {
+ position: "fixed",
+ left: 0, top: 0,
+ width: "100vw", height: "100vh",
+ transition: "transform ease .25s"
+ }
+]);
\ No newline at end of file
diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx
index ee3e702740..11fe94f1d1 100644
--- a/src/app/features/room/Room.tsx
+++ b/src/app/features/room/Room.tsx
@@ -13,6 +13,10 @@ import { useKeyDown } from '../../hooks/useKeyDown';
import { markAsRead } from '../../../client/action/notifications';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomMembers } from '../../hooks/useRoomMembers';
+import { PageRootFloat } from '../../components/page';
+import { SidebarNav } from '../../pages/client/SidebarNav';
+import { SlideMenuChild } from '../../components/SlideMenuChild';
+import { useSlideMenu } from '../../hooks/useSlideMenu';
export function Room() {
const { eventId } = useParams();
@@ -24,6 +28,8 @@ export function Room() {
const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId);
+ const { offset, offsetOverride, onTouchStart, onTouchEnd, onTouchMove } = useSlideMenu();
+
useKeyDown(
window,
useCallback(
@@ -38,7 +44,7 @@ export function Room() {
return (
-
+
{screenSize === ScreenSize.Desktop && isDrawer && (
<>
@@ -47,6 +53,14 @@ export function Room() {
>
)}
+ {/* Create a slide menu offscreen for mobile. Same for all other slide menus. */}
+ {screenSize === ScreenSize.Mobile &&
+
+
+ }
);
}
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index 63b3d3e2cd..f58d3fbde3 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -117,6 +117,10 @@ import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { useTouchOffset } from '../../hooks/useTouchOffset';
+import { PageRoot } from '../../components/page';
+import { Space } from '../../pages/client/space';
+import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => (
@@ -888,13 +892,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
[mx, room, editor]
);
- const handleReplyClick: MouseEventHandler = useCallback(
- (evt) => {
- const replyId = evt.currentTarget.getAttribute('data-event-id');
- if (!replyId) {
- console.warn('Button should have "data-event-id" attribute!');
- return;
- }
+ // This is been changed to accept the replyId directly instead of a mouse event in order to be used for swipe left reply.
+ const handleReply = useCallback(
+ (replyId: string) => {
const replyEvt = room.findEventById(replyId);
if (!replyEvt) return;
const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet());
@@ -990,7 +990,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
relations={hasReactions ? reactionRelations : undefined}
onUserClick={handleUserClick}
onUsernameClick={handleUsernameClick}
- onReplyClick={handleReplyClick}
+ onReply={handleReply}
onReactionToggle={handleReactionToggle}
onEditId={handleEdit}
reply={
@@ -1062,7 +1062,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
relations={hasReactions ? reactionRelations : undefined}
onUserClick={handleUserClick}
onUsernameClick={handleUsernameClick}
- onReplyClick={handleReplyClick}
+ onReply={handleReply}
onReactionToggle={handleReactionToggle}
onEditId={handleEdit}
reply={
@@ -1170,7 +1170,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
relations={hasReactions ? reactionRelations : undefined}
onUserClick={handleUserClick}
onUsernameClick={handleUsernameClick}
- onReplyClick={handleReplyClick}
+ onReply={handleReply}
onReactionToggle={handleReactionToggle}
reactions={
reactionRelations && (
diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx
index 21b186422d..97ab721e5f 100644
--- a/src/app/features/room/message/Message.tsx
+++ b/src/app/features/room/message/Message.tsx
@@ -76,6 +76,7 @@ import { getViaServers } from '../../../plugins/via-servers';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents';
import { StateEvent } from '../../../../types/matrix/room';
+import { useTouchOffset } from '../../../hooks/useTouchOffset';
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
@@ -666,7 +667,7 @@ export type MessageProps = {
messageSpacing: MessageSpacing;
onUserClick: MouseEventHandler;
onUsernameClick: MouseEventHandler;
- onReplyClick: MouseEventHandler;
+ onReply: (replyId: string) => void;
onEditId?: (eventId?: string) => void;
onReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void;
reply?: ReactNode;
@@ -690,7 +691,7 @@ export const Message = as<'div', MessageProps>(
messageSpacing,
onUserClick,
onUsernameClick,
- onReplyClick,
+ onReply,
onReactionToggle,
onEditId,
reply,
@@ -708,6 +709,21 @@ export const Message = as<'div', MessageProps>(
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
const [menuAnchor, setMenuAnchor] = useState();
const [emojiBoardAnchor, setEmojiBoardAnchor] = useState();
+ // Swipe left gesture that, if it is pulled to the left by 20% of screen width, trigger a reply
+ const { offset, onTouchStart, onTouchEnd, onTouchMove } = useTouchOffset({ offsetLimit: [-0.25 * window.innerWidth, 0, 0, 0], touchEndCallback: ([x]) => {
+ if (x < -0.2 * window.innerWidth)
+ onReply(mEvent.getId()!);
+ }});
+
+ // Wrapper of the new onReply for the old onReplyClick
+ const onReplyClick: MouseEventHandler = useCallback((evt) => {
+ const replyId = evt.currentTarget.getAttribute('data-event-id');
+ if (!replyId) {
+ console.warn('Button should have "data-event-id" attribute!');
+ return;
+ }
+ onReply(replyId);
+ }, []);
const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
@@ -838,6 +854,10 @@ export const Message = as<'div', MessageProps>(
collapse={collapse}
highlight={highlight}
selected={!!menuAnchor || !!emojiBoardAnchor}
+ onTouchStart={onTouchStart}
+ onTouchEnd={onTouchEnd}
+ onTouchMove={onTouchMove}
+ style={{ transform: `translateX(${offset[0]}px)`, transition: offset[0] ? "none" : "" }}
{...props}
{...hoverProps}
{...focusWithinProps}
diff --git a/src/app/features/room/message/styles.css.ts b/src/app/features/room/message/styles.css.ts
index b87cb50548..4dcfa62784 100644
--- a/src/app/features/room/message/styles.css.ts
+++ b/src/app/features/room/message/styles.css.ts
@@ -3,6 +3,7 @@ import { DefaultReset, config, toRem } from 'folds';
export const MessageBase = style({
position: 'relative',
+ transition: 'transform ease .25s'
});
export const MessageOptionsBase = style([
diff --git a/src/app/hooks/useGoBack.ts b/src/app/hooks/useGoBack.ts
new file mode 100644
index 0000000000..bcdb7c2d76
--- /dev/null
+++ b/src/app/hooks/useGoBack.ts
@@ -0,0 +1,78 @@
+import { useCallback } from "react";
+import { useNavigate, useLocation, matchPath } from "react-router-dom";
+import { HOME_PATH, DIRECT_PATH, SPACE_PATH, EXPLORE_PATH, INBOX_PATH } from "../pages/paths";
+import { getHomePath, getDirectPath, getSpacePath, getExplorePath, getInboxPath } from "../pages/pathUtils";
+
+// Moved goBack from BackRouteHandler for reusabilitiy
+export function useGoBack() {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const goBack = useCallback(() => {
+ if (
+ matchPath(
+ {
+ path: HOME_PATH,
+ caseSensitive: true,
+ end: false,
+ },
+ location.pathname
+ )
+ ) {
+ navigate(getHomePath());
+ return;
+ }
+ if (
+ matchPath(
+ {
+ path: DIRECT_PATH,
+ caseSensitive: true,
+ end: false,
+ },
+ location.pathname
+ )
+ ) {
+ navigate(getDirectPath());
+ return;
+ }
+ const spaceMatch = matchPath(
+ {
+ path: SPACE_PATH,
+ caseSensitive: true,
+ end: false,
+ },
+ location.pathname
+ );
+ if (spaceMatch?.params.spaceIdOrAlias) {
+ navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias));
+ return;
+ }
+ if (
+ matchPath(
+ {
+ path: EXPLORE_PATH,
+ caseSensitive: true,
+ end: false,
+ },
+ location.pathname
+ )
+ ) {
+ navigate(getExplorePath());
+ return;
+ }
+ if (
+ matchPath(
+ {
+ path: INBOX_PATH,
+ caseSensitive: true,
+ end: false,
+ },
+ location.pathname
+ )
+ ) {
+ navigate(getInboxPath());
+ }
+ }, [navigate, location]);
+
+ return goBack;
+}
\ No newline at end of file
diff --git a/src/app/hooks/useSlideMenu.ts b/src/app/hooks/useSlideMenu.ts
new file mode 100644
index 0000000000..72ccb7982e
--- /dev/null
+++ b/src/app/hooks/useSlideMenu.ts
@@ -0,0 +1,22 @@
+import { useState } from "react";
+import { useGoBack } from "./useGoBack";
+import { useTouchOffset } from "./useTouchOffset";
+
+// Reusable slide menu gesture handler
+export function useSlideMenu() {
+ const goBack = useGoBack();
+ const [offsetOverride, setOffsetOverride] = useState(false);
+ const { offset, onTouchStart, onTouchEnd, onTouchMove } = useTouchOffset({
+ startLimit: [0, 0.1 * window.innerWidth, undefined, undefined],
+ offsetLimit: [0, window.innerWidth, 0, 0],
+ touchEndCallback: (offset, velocity, averageVelocity) => {
+ if (offset[0] > window.innerWidth * 0.5 || (velocity && velocity[0] > 0.9) || (averageVelocity && averageVelocity[0] > 0.8)) {
+ setOffsetOverride(true);
+ // Slide menu transition takes .25s so we wait for that.
+ setTimeout(() => goBack(), 250);
+ }
+ }
+ });
+
+ return { offset, offsetOverride, onTouchStart, onTouchEnd, onTouchMove };
+}
\ No newline at end of file
diff --git a/src/app/hooks/useTouchOffset.ts b/src/app/hooks/useTouchOffset.ts
new file mode 100644
index 0000000000..8ea11becba
--- /dev/null
+++ b/src/app/hooks/useTouchOffset.ts
@@ -0,0 +1,87 @@
+import React, { useState } from "react";
+
+type Vec2 = [number, number]; // x, y
+type Limit = [number | undefined, number | undefined, number | undefined, number | undefined]; // min x, max x, min y, max y
+
+// Function for calculating average touch position from multiple touches.
+function getTouchListAverageCoordinates(list: React.TouchList) {
+ return Array.from(list).map(touch => [touch.clientX, touch.clientY]).reduce((a, b) => {
+ a[0] += b[0];
+ a[1] += b[1];
+ return a;
+ }).map(coord => coord / list.length) as Vec2;
+}
+
+// Used for offset limits. It makes sure the offset value is within the range. Undefined means there is no limit for min/max.
+function clamp(val: number, min?: number, max?: number) {
+ if (min === undefined && max === undefined) return val;
+ if (min === undefined) return Math.min(val, max!);
+ if (max === undefined) return Math.max(val, min!);
+ return Math.max(Math.min(val, max), min);
+}
+
+// Used for start limits. Returns a boolean indicating if a value is valid to be considered as a starting point.
+function isBetween(val: number, min?: number, max?: number) {
+ if (min === undefined && max === undefined) return true;
+ if (min === undefined) return val <= max!;
+ if (max === undefined) return val >= min!;
+ return val >= min && val <= max;
+}
+
+// Returns the offset from start point to end point
+export function useTouchOffset(options?: { startLimit?: Limit, offsetLimit?: Limit, touchEndCallback?: (offset: Vec2, velocity?: Vec2, averageVelocity?: Vec2) => any }) {
+ const [startPos, setStartPos] = useState(null);
+ const [offset, setOffset] = useState([0, 0]);
+ const [velocity, setVelocity] = useState([0, 0]);
+ const [_, setLastTime] = useState(Date.now());
+ const [startTime, setStartTime] = useState(Date.now());
+
+ // Wrapper of setOffset and setVelocity, making sure offset is within limit
+ const limitedSetOffset = (coords: Vec2) => {
+ setLastTime(lt => {
+ const now = Date.now();
+ setVelocity(Array.from(coords).map((coord, ii) => (coord - offset[ii]) / (now - lt)) as Vec2);
+ return Date.now();
+ });
+ if (!options?.offsetLimit) setOffset(coords);
+ else setOffset(coords.map((coord, ii) => clamp(coord, options.offsetLimit![ii * 2], options.offsetLimit![ii * 2 + 1])) as Vec2);
+ };
+
+ const onTouchStart = (event: React.TouchEvent) => {
+ const coords = getTouchListAverageCoordinates(event.touches);
+ if (!startPos) {
+ // Start if limit is not set, or the touch point is within limits
+ if (!options?.startLimit || coords.map((coord, ii) => isBetween(coord, options.startLimit![ii * 2], options.startLimit![ii * 2 + 1])).every(x => x)) {
+ setStartPos(coords);
+ setLastTime(Date.now());
+ setStartTime(Date.now());
+ }
+ }
+ // Touches after the first one. Calculate offset using the average point.
+ else limitedSetOffset(coords.map((coord, ii) => coord - startPos[ii]) as Vec2);
+ };
+
+ const onTouchEnd = (event: React.TouchEvent) => {
+ // Length 0 means there are no more touching points
+ if (event.touches.length == 0) {
+ // A callback with offset, velocity and average velocity. Used to determine if the gesture is strong enough for actions.
+ if (options?.touchEndCallback) options.touchEndCallback(offset, velocity, !startPos ? undefined : offset.map(((coord, ii) => (coord - startPos[ii]) / (Date.now() - startTime))) as Vec2);
+ // Reset starting point, offset and velocity.
+ setStartPos(null);
+ setOffset([0, 0]);
+ setVelocity([0, 0]);
+ } else if (startPos) {
+ // There are still point(s) being touched. Use the average of them to calculate offset.
+ const coords = getTouchListAverageCoordinates(event.touches);
+ limitedSetOffset(coords.map((coord, ii) => coord - startPos[ii]) as Vec2);
+ }
+ };
+
+ const onTouchMove = (event: React.TouchEvent) => {
+ const coords = getTouchListAverageCoordinates(event.touches);
+ // Update offset according to touch point movement
+ if (startPos) limitedSetOffset(coords.map((coord, ii) => coord - startPos[ii]) as Vec2);
+ };
+
+ return { offset, velocity, onTouchStart, onTouchEnd, onTouchMove };
+}
\ No newline at end of file
diff --git a/src/app/pages/client/explore/Featured.tsx b/src/app/pages/client/explore/Featured.tsx
index f056cbb5cd..9821fafb84 100644
--- a/src/app/pages/client/explore/Featured.tsx
+++ b/src/app/pages/client/explore/Featured.tsx
@@ -12,12 +12,16 @@ import {
PageHeader,
PageHero,
PageHeroSection,
+ PageRootFloat,
} from '../../../components/page';
import { RoomTopicViewer } from '../../../components/room-topic-viewer';
import * as css from './style.css';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
+import { useSlideMenu } from '../../../hooks/useSlideMenu';
+import { SlideMenuChild } from '../../../components/SlideMenuChild';
+import { SidebarNav } from '../SidebarNav';
export function FeaturedRooms() {
const { featuredCommunities } = useClientConfig();
@@ -25,6 +29,7 @@ export function FeaturedRooms() {
const allRooms = useAtomValue(allRoomsAtom);
const screenSize = useScreenSizeContext();
const { navigateSpace, navigateRoom } = useRoomNavigate();
+ const { offset, offsetOverride, onTouchStart, onTouchEnd, onTouchMove } = useSlideMenu();
return (
@@ -41,7 +46,7 @@ export function FeaturedRooms() {
)}
-
+
@@ -133,6 +138,14 @@ export function FeaturedRooms() {
+ {/* Create a slide menu offscreen for mobile. Same for all other slide menus. */}
+ {screenSize === ScreenSize.Mobile &&
+
+
+ }
);
}
diff --git a/src/app/pages/client/explore/Server.tsx b/src/app/pages/client/explore/Server.tsx
index 1f493df17f..7ea3758714 100644
--- a/src/app/pages/client/explore/Server.tsx
+++ b/src/app/pages/client/explore/Server.tsx
@@ -32,7 +32,7 @@ import FocusTrap from 'focus-trap-react';
import { useAtomValue } from 'jotai';
import { useQuery } from '@tanstack/react-query';
import { MatrixClient, Method, RoomType } from 'matrix-js-sdk';
-import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
+import { Page, PageContent, PageContentCenter, PageHeader, PageRootFloat } from '../../../components/page';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { RoomTopicViewer } from '../../../components/room-topic-viewer';
import { RoomCard, RoomCardBase, RoomCardGrid } from '../../../components/room-card';
@@ -45,6 +45,9 @@ import { getMxIdServer } from '../../../utils/matrix';
import { stopPropagation } from '../../../utils/keyboard';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
+import { useSlideMenu } from '../../../hooks/useSlideMenu';
+import { SlideMenuChild } from '../../../components/SlideMenuChild';
+import { SidebarNav } from '../SidebarNav';
const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams =>
useMemo(
@@ -356,6 +359,7 @@ export function PublicRooms() {
const searchInputRef = useRef(null);
const navigate = useNavigate();
const roomTypeFilters = useRoomTypeFilters();
+ const { offset, offsetOverride, onTouchStart, onTouchEnd, onTouchMove } = useSlideMenu();
const currentLimit: number = useMemo(() => {
const limitParam = serverSearchParams.limit;
@@ -516,7 +520,7 @@ export function PublicRooms() {
>
)}
-
+
@@ -661,6 +665,14 @@ export function PublicRooms() {
+ {/* Create a slide menu offscreen for mobile. Same for all other slide menus. */}
+ {screenSize === ScreenSize.Mobile &&
+
+
+ }
);
}
diff --git a/src/app/pages/client/inbox/Invites.tsx b/src/app/pages/client/inbox/Invites.tsx
index 8dcfa1c209..8db40a7cb6 100644
--- a/src/app/pages/client/inbox/Invites.tsx
+++ b/src/app/pages/client/inbox/Invites.tsx
@@ -18,7 +18,7 @@ import {
import { useAtomValue } from 'jotai';
import FocusTrap from 'focus-trap-react';
import { MatrixError, Room } from 'matrix-js-sdk';
-import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
+import { Page, PageContent, PageContentCenter, PageHeader, PageRootFloat } from '../../../components/page';
import { useDirectInvites, useRoomInvites, useSpaceInvites } from '../../../state/hooks/inviteList';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { allInvitesAtom } from '../../../state/room-list/inviteList';
@@ -43,6 +43,9 @@ import { useRoomTopic } from '../../../hooks/useRoomMeta';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { useSlideMenu } from '../../../hooks/useSlideMenu';
+import { SlideMenuChild } from '../../../components/SlideMenuChild';
+import { SidebarNav } from '../SidebarNav';
const COMPACT_CARD_WIDTH = 548;
@@ -214,6 +217,8 @@ export function Invites() {
const { navigateRoom, navigateSpace } = useRoomNavigate();
+ const { offset, offsetOverride, onTouchStart, onTouchEnd, onTouchMove } = useSlideMenu();
+
const renderInvite = (roomId: string, direct: boolean, handleNavigate: (rId: string) => void) => {
const room = mx.getRoom(roomId);
if (!room) return null;
@@ -253,7 +258,7 @@ export function Invites() {
-
+
@@ -304,6 +309,14 @@ export function Invites() {
+ {/* Create a slide menu offscreen for mobile. Same for all other slide menus. */}
+ {screenSize === ScreenSize.Mobile &&
+
+
+ }
);
}
diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx
index 0c832b0946..c801ee7d90 100644
--- a/src/app/pages/client/inbox/Notifications.tsx
+++ b/src/app/pages/client/inbox/Notifications.tsx
@@ -26,7 +26,7 @@ import {
import { useVirtualizer } from '@tanstack/react-virtual';
import { HTMLReactParserOptions } from 'html-react-parser';
import { Opts as LinkifyOpts } from 'linkifyjs';
-import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
+import { Page, PageContent, PageContentCenter, PageHeader, PageRootFloat } from '../../../components/page';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { InboxNotificationsPathSearchParams } from '../../paths';
@@ -82,6 +82,9 @@ import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { useSlideMenu } from '../../../hooks/useSlideMenu';
+import { SlideMenuChild } from '../../../components/SlideMenuChild';
+import { SidebarNav } from '../SidebarNav';
type RoomNotificationsGroup = {
roomId: string;
@@ -506,6 +509,7 @@ export function Notifications() {
const scrollRef = useRef(null);
const scrollTopAnchorRef = useRef(null);
const [refreshIntervalTime, setRefreshIntervalTime] = useState(DEFAULT_REFRESH_MS);
+ const { offset, offsetOverride, onTouchStart, onTouchEnd, onTouchMove } = useSlideMenu();
const onlyHighlight = notificationsSearchParams.only === 'highlight';
const setOnlyHighlighted = (highlight: boolean) => {
@@ -587,7 +591,7 @@ export function Notifications() {
-
+
@@ -711,6 +715,14 @@ export function Notifications() {
+ {/* Create a slide menu offscreen for mobile. Same for all other slide menus. */}
+ {screenSize === ScreenSize.Mobile &&
+
+
+ }
);
}