Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca51f93ecd | ||
| 47c525cc5a | |||
| a282fce14a |
@@ -22,6 +22,7 @@ type GestureState = {
|
||||
startX: number;
|
||||
startY: number;
|
||||
messageEl: HTMLElement | null;
|
||||
transformEl: HTMLElement | null;
|
||||
messageId: string | null;
|
||||
edgeBack: boolean;
|
||||
offset: number;
|
||||
@@ -33,6 +34,7 @@ const IDLE_STATE: GestureState = {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
messageEl: null,
|
||||
transformEl: null,
|
||||
messageId: null,
|
||||
edgeBack: false,
|
||||
offset: 0,
|
||||
@@ -65,6 +67,12 @@ const findMessageElement = (target: EventTarget | null): HTMLElement | null => {
|
||||
return target.closest('[data-message-id]') as HTMLElement | null;
|
||||
};
|
||||
|
||||
const findReplyTransformTarget = (messageEl: HTMLElement): HTMLElement => {
|
||||
const content = messageEl.querySelector('[data-swipe-reply-content]');
|
||||
if (content instanceof HTMLElement) return content;
|
||||
return messageEl;
|
||||
};
|
||||
|
||||
const applyBackOffset = (dx: number, width: number): number => {
|
||||
const positive = Math.max(0, dx);
|
||||
if (positive <= width) return positive;
|
||||
@@ -123,18 +131,18 @@ export function MobileSwipeGestureHost({ children }: MobileSwipeGestureHostProps
|
||||
document.documentElement.classList.remove('mobile-gesture-lock');
|
||||
}, []);
|
||||
|
||||
const resetReplyTransform = useCallback((messageEl?: HTMLElement | null) => {
|
||||
const resetReplyTransform = useCallback((transformEl?: HTMLElement | null) => {
|
||||
const bridge = mobileSwipeReplyBridgeRef.current;
|
||||
const el = messageEl ?? gestureRef.current.messageEl;
|
||||
const el = transformEl ?? gestureRef.current.transformEl;
|
||||
resetElementTransform(el);
|
||||
bridge?.setIndicator(null, false);
|
||||
document.documentElement.classList.remove('mobile-gesture-lock');
|
||||
}, []);
|
||||
|
||||
const resetGesture = useCallback(
|
||||
(messageEl?: HTMLElement | null) => {
|
||||
(transformEl?: HTMLElement | null) => {
|
||||
resetBackTransform();
|
||||
resetReplyTransform(messageEl);
|
||||
resetReplyTransform(transformEl);
|
||||
unlockScroll();
|
||||
gestureRef.current = { ...IDLE_STATE };
|
||||
},
|
||||
@@ -150,6 +158,14 @@ export function MobileSwipeGestureHost({ children }: MobileSwipeGestureHostProps
|
||||
bridge.setIndicator(top, Math.abs(offset) >= REPLY_THRESHOLD * 0.6);
|
||||
}, []);
|
||||
|
||||
const applyReplyTransform = useCallback(
|
||||
(transformEl: HTMLElement, messageEl: HTMLElement, offset: number) => {
|
||||
setElementTransform(transformEl, offset);
|
||||
updateReplyIndicator(messageEl, offset);
|
||||
},
|
||||
[updateReplyIndicator]
|
||||
);
|
||||
|
||||
const lockGesture = useCallback(
|
||||
(phase: 'back' | 'reply', scrollAnchor?: HTMLElement | null) => {
|
||||
gestureRef.current.phase = phase;
|
||||
@@ -183,9 +199,9 @@ export function MobileSwipeGestureHost({ children }: MobileSwipeGestureHostProps
|
||||
if (gesture.phase === 'reply' && gesture.messageEl && gesture.messageId) {
|
||||
const bridge = mobileSwipeReplyBridgeRef.current;
|
||||
const shouldReply = Math.abs(gesture.offset) >= REPLY_THRESHOLD;
|
||||
const { messageEl, messageId } = gesture;
|
||||
const { messageEl, messageId, transformEl } = gesture;
|
||||
|
||||
resetGesture(messageEl);
|
||||
resetGesture(transformEl);
|
||||
|
||||
if (shouldReply && bridge) {
|
||||
startReplyToEvent(bridge.room, messageId, bridge.setReplyDraft, bridge.editor);
|
||||
@@ -232,6 +248,7 @@ export function MobileSwipeGestureHost({ children }: MobileSwipeGestureHostProps
|
||||
startX: touch.clientX,
|
||||
startY: touch.clientY,
|
||||
messageEl: null,
|
||||
transformEl: null,
|
||||
messageId: null,
|
||||
edgeBack: true,
|
||||
offset: 0,
|
||||
@@ -246,6 +263,7 @@ export function MobileSwipeGestureHost({ children }: MobileSwipeGestureHostProps
|
||||
startX: touch.clientX,
|
||||
startY: touch.clientY,
|
||||
messageEl: canStartReply ? messageEl : null,
|
||||
transformEl: canStartReply && messageEl ? findReplyTransformTarget(messageEl) : null,
|
||||
messageId: canStartReply ? messageId : null,
|
||||
edgeBack: false,
|
||||
offset: 0,
|
||||
@@ -277,7 +295,7 @@ export function MobileSwipeGestureHost({ children }: MobileSwipeGestureHostProps
|
||||
|
||||
if (deltaX > 0 && backEnabled) {
|
||||
lockGesture('back', contentRef.current);
|
||||
} else if (deltaX < 0 && gesture.messageEl && gesture.messageId) {
|
||||
} else if (deltaX < 0 && gesture.messageEl && gesture.messageId && gesture.transformEl) {
|
||||
lockGesture('reply', gesture.messageEl);
|
||||
} else {
|
||||
resetGesture();
|
||||
@@ -296,14 +314,13 @@ export function MobileSwipeGestureHost({ children }: MobileSwipeGestureHostProps
|
||||
return;
|
||||
}
|
||||
|
||||
if (gesture.phase === 'reply' && gesture.messageEl) {
|
||||
if (gesture.phase === 'reply' && gesture.transformEl && gesture.messageEl) {
|
||||
if (!evt.cancelable) return;
|
||||
evt.preventDefault();
|
||||
|
||||
const offset = Math.max(deltaX, -REPLY_MAX);
|
||||
gesture.offset = offset;
|
||||
setElementTransform(gesture.messageEl, offset);
|
||||
updateReplyIndicator(gesture.messageEl, offset);
|
||||
applyReplyTransform(gesture.transformEl, gesture.messageEl, offset);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -329,7 +346,7 @@ export function MobileSwipeGestureHost({ children }: MobileSwipeGestureHostProps
|
||||
document.removeEventListener('touchcancel', handleTouchEnd, { capture: true });
|
||||
resetGesture();
|
||||
};
|
||||
}, [backEnabled, compact, finishGesture, lockGesture, replyEnabled, resetGesture, updateReplyIndicator]);
|
||||
}, [backEnabled, compact, finishGesture, lockGesture, replyEnabled, resetGesture, applyReplyTransform]);
|
||||
|
||||
useEffect(() => {
|
||||
resetGesture();
|
||||
@@ -339,16 +356,8 @@ export function MobileSwipeGestureHost({ children }: MobileSwipeGestureHostProps
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const showBackChrome = backEnabled;
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className={css.SwipeBackRoot}>
|
||||
{showBackChrome && (
|
||||
<div className={css.SwipeBackUnderlay} aria-hidden>
|
||||
<div className={css.SwipeBackSidebarPeek} />
|
||||
<div className={css.SwipeBackChannelPeek} />
|
||||
</div>
|
||||
)}
|
||||
<div ref={contentRef} className={css.SwipeBackContent}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -13,37 +13,38 @@ export const SwipeBackRoot = style({
|
||||
|
||||
export const SwipeBackContent = style({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: color.Background.Container,
|
||||
boxShadow: '0 0 24px rgba(0, 0, 0, 0.35)',
|
||||
boxShadow: '-4px 0 16px rgba(0, 0, 0, 0.28)',
|
||||
willChange: 'transform',
|
||||
touchAction: 'pan-y',
|
||||
});
|
||||
|
||||
export const SwipeBackUnderlay = style({
|
||||
export const CompactDetailRoot = style({
|
||||
position: 'relative',
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const CompactNavUnderlay = style({
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
pointerEvents: 'none',
|
||||
flexDirection: 'column',
|
||||
zIndex: 0,
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const SwipeBackSidebarPeek = style({
|
||||
width: toRem(66),
|
||||
flexShrink: 0,
|
||||
backgroundColor: color.Background.Container,
|
||||
borderRight: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
|
||||
});
|
||||
|
||||
export const SwipeBackChannelPeek = style({
|
||||
flex: 1,
|
||||
export const CompactDetailLayer = style({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
minWidth: 0,
|
||||
backgroundColor: color.Background.Container,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const SwipeToReplyLayer = style({
|
||||
|
||||
203
overlay/src/app/components/page/Page.tsx
Normal file
203
overlay/src/app/components/page/Page.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, { ComponentProps, MutableRefObject, ReactNode } from 'react';
|
||||
import { Box, Header, Line, Scroll, Text, as } from 'folds';
|
||||
import classNames from 'classnames';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import * as css from './style.css';
|
||||
import * as swipeCss from '../mobile/mobile-gestures.css';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { SidebarDockedCallPanel } from '../../features/call/SidebarDockedCallPanel';
|
||||
import { useShowCompactMasterView } from '../../hooks/useCompactNav';
|
||||
|
||||
type PageRootProps = {
|
||||
nav: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function PageRoot({ nav, children }: PageRootProps) {
|
||||
const screenSize = useScreenSizeContext();
|
||||
const showCompactMaster = useShowCompactMasterView();
|
||||
const compact = screenSize === ScreenSize.Mobile;
|
||||
|
||||
if (compact && showCompactMaster) {
|
||||
return (
|
||||
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
||||
{nav}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (compact && !showCompactMaster) {
|
||||
return (
|
||||
<Box
|
||||
grow="Yes"
|
||||
className={classNames(ContainerColor({ variant: 'Background' }), swipeCss.CompactDetailRoot)}
|
||||
>
|
||||
{nav && (
|
||||
<Box className={swipeCss.CompactNavUnderlay} aria-hidden>
|
||||
{nav}
|
||||
</Box>
|
||||
)}
|
||||
<Box grow="Yes" className={swipeCss.CompactDetailLayer}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
||||
{nav}
|
||||
{screenSize !== ScreenSize.Mobile && (
|
||||
<Line variant="Background" size="300" direction="Vertical" />
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type ClientDrawerLayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
export function PageNav({
|
||||
size,
|
||||
children,
|
||||
className,
|
||||
}: ClientDrawerLayoutProps & css.PageNavVariants & { className?: string }) {
|
||||
const screenSize = useScreenSizeContext();
|
||||
const isMobile = screenSize === ScreenSize.Mobile;
|
||||
|
||||
return (
|
||||
<Box
|
||||
grow={isMobile ? 'Yes' : undefined}
|
||||
className={classNames(css.PageNav({ size }), className)}
|
||||
shrink={isMobile ? 'Yes' : 'No'}
|
||||
>
|
||||
<Box grow="Yes" direction="Column">
|
||||
{children}
|
||||
<SidebarDockedCallPanel />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export const PageNavHeader = as<'header', css.PageNavHeaderVariants>(
|
||||
({ className, outlined, ...props }, ref) => (
|
||||
<Header
|
||||
className={classNames(css.PageNavHeader({ outlined }), className)}
|
||||
variant="Background"
|
||||
size="600"
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export function PageNavContent({
|
||||
scrollRef,
|
||||
scrollProps,
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
scrollRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
scrollProps?: React.ComponentProps<typeof Scroll>;
|
||||
}) {
|
||||
return (
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Scroll
|
||||
ref={scrollRef}
|
||||
variant="Background"
|
||||
direction="Vertical"
|
||||
size="300"
|
||||
hideTrack
|
||||
visibility="Hover"
|
||||
{...scrollProps}
|
||||
>
|
||||
<div className={css.PageNavContent}>{children}</div>
|
||||
</Scroll>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export const Page = as<'div'>(({ className, ...props }, ref) => (
|
||||
<Box
|
||||
grow="Yes"
|
||||
direction="Column"
|
||||
className={classNames(ContainerColor({ variant: 'Surface' }), className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
|
||||
export const PageHeader = as<'div', css.PageHeaderVariants>(
|
||||
({ className, outlined, balance, ...props }, ref) => (
|
||||
<Header
|
||||
as="header"
|
||||
size="600"
|
||||
className={classNames(css.PageHeader({ balance, outlined }), className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export const PageContent = as<'div'>(({ className, ...props }, ref) => (
|
||||
<div className={classNames(css.PageContent, className)} {...props} ref={ref} />
|
||||
));
|
||||
|
||||
export function PageHeroEmpty({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Box
|
||||
className={classNames(ContainerColor({ variant: 'SurfaceVariant' }), css.PageHeroEmpty)}
|
||||
direction="Column"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
gap="200"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export const PageHeroSection = as<'div', ComponentProps<typeof Box>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<Box
|
||||
direction="Column"
|
||||
className={classNames(css.PageHeroSection, className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export function PageHero({
|
||||
icon,
|
||||
title,
|
||||
subTitle,
|
||||
children,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
title: ReactNode;
|
||||
subTitle: ReactNode;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<Box direction="Column" alignItems="Center" gap="200">
|
||||
{icon}
|
||||
</Box>
|
||||
<Box as="h2" direction="Column" gap="200" alignItems="Center">
|
||||
<Text align="Center" size="H2">
|
||||
{title}
|
||||
</Text>
|
||||
<Text align="Center" priority="400">
|
||||
{subTitle}
|
||||
</Text>
|
||||
</Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export const PageContentCenter = as<'div'>(({ className, ...props }, ref) => (
|
||||
<div className={classNames(css.PageContentCenter, className)} {...props} ref={ref} />
|
||||
));
|
||||
1405
overlay/src/app/features/room/message/Message.tsx
Normal file
1405
overlay/src/app/features/room/message/Message.tsx
Normal file
File diff suppressed because it is too large
Load Diff
20
overlay/src/app/pages/MobileFriendly.tsx
Normal file
20
overlay/src/app/pages/MobileFriendly.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type MobileFriendlyClientNavProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/** Server sidebar stays mounted on compact detail routes for swipe-back reveal. */
|
||||
export function MobileFriendlyClientNav({ children }: MobileFriendlyClientNavProps) {
|
||||
return children;
|
||||
}
|
||||
|
||||
type MobileFriendlyPageNavProps = {
|
||||
path: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/** Channel list stays mounted on compact detail routes for swipe-back reveal. */
|
||||
export function MobileFriendlyPageNav({ path: _path, children }: MobileFriendlyPageNavProps) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paarrot",
|
||||
"version": "4.11.116",
|
||||
"version": "4.11.117",
|
||||
"description": "Paarrot - A Matrix client based on Cinny",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
Reference in New Issue
Block a user