From eeb79a6966053ef7c573db4c0ad1bc28d10c4577 Mon Sep 17 00:00:00 2001 From: NorthWestWind Date: Tue, 27 Feb 2024 16:16:08 +0800 Subject: [PATCH 01/10] mobile slide menu --- src/app/templates/client/Client.jsx | 54 +++++++++++++++++++++++++--- src/app/templates/client/Client.scss | 22 ++++++++++++ src/app/utils/common.ts | 6 ++++ 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/app/templates/client/Client.jsx b/src/app/templates/client/Client.jsx index e9be6b16e..1579621c9 100644 --- a/src/app/templates/client/Client.jsx +++ b/src/app/templates/client/Client.jsx @@ -15,6 +15,7 @@ import Dialogs from '../../organisms/pw/Dialogs'; import initMatrix from '../../../client/initMatrix'; import navigation from '../../../client/state/navigation'; +import { openNavigation } from '../../../client/action/navigation'; import cons from '../../../client/state/cons'; import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; @@ -22,6 +23,7 @@ import { MatrixClientProvider } from '../../hooks/useMatrixClient'; import { ClientContent } from './ClientContent'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; +import { clamp } from '../../utils/common'; function SystemEmojiFeature() { const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); @@ -38,27 +40,69 @@ function SystemEmojiFeature() { function Client() { const [isLoading, changeLoading] = useState(true); const [loadingMsg, setLoadingMsg] = useState('Heating up'); + const [isTouchingSide, setTouchingSide] = useState(false); + const [sideMoved, setSideMoved] = useState(0); const classNameHidden = 'client__item-hidden'; + const classNameBackground = 'client__item-background'; + const classNameSided = 'client__item-sided'; const navWrapperRef = useRef(null); const roomWrapperRef = useRef(null); function onRoomSelected() { - navWrapperRef.current?.classList.add(classNameHidden); + navWrapperRef.current?.classList.add(classNameSided); roomWrapperRef.current?.classList.remove(classNameHidden); } function onNavigationSelected() { - navWrapperRef.current?.classList.remove(classNameHidden); + navWrapperRef.current?.classList.remove(classNameSided); roomWrapperRef.current?.classList.add(classNameHidden); } + // Touch variables. Don't use states as we don't want any re-render. + function onTouchStart(event) { + if (!navWrapperRef.current?.classList.contains(classNameSided)) return; + if (event.touches.length != 1) return setTouchingSide(false); + if (event.touches[0].clientX < window.innerWidth * 0.1) setTouchingSide(true); + } + function onTouchEnd(event) { + if (!navWrapperRef.current?.classList.contains(classNameSided)) return; + setTouchingSide(isTouchingSide => { + if (isTouchingSide) { + event.preventDefault(); + setSideMoved(sideMoved => { + if (sideMoved > 0.5) openNavigation(); + return 0; + }); + } + return false; + }); + } + function onTouchMove(event) { + if (!navWrapperRef.current?.classList.contains(classNameSided)) return; + setTouchingSide(isTouchingSide => { + if (isTouchingSide) { + event.preventDefault(); + if (event.changedTouches.length != 1) return setSideMoved(0); + setSideMoved(event.changedTouches[0].clientX / window.innerWidth); + } + return isTouchingSide; + }); + } useEffect(() => { navigation.on(cons.events.navigation.ROOM_SELECTED, onRoomSelected); navigation.on(cons.events.navigation.NAVIGATION_OPENED, onNavigationSelected); + window.addEventListener("touchstart", onTouchStart); + window.addEventListener("touchend", onTouchEnd); + window.addEventListener("touchmove", onTouchMove); + return () => { navigation.removeListener(cons.events.navigation.ROOM_SELECTED, onRoomSelected); navigation.removeListener(cons.events.navigation.NAVIGATION_OPENED, onNavigationSelected); + + window.removeEventListener("touchstart", onTouchStart); + window.removeEventListener("touchend", onTouchEnd); + window.removeEventListener("touchmove", onTouchMove); }; }, []); @@ -119,10 +163,10 @@ function Client() { return ( +
+ +
-
- -
diff --git a/src/app/templates/client/Client.scss b/src/app/templates/client/Client.scss index cdb8fcc94..60eb55e98 100644 --- a/src/app/templates/client/Client.scss +++ b/src/app/templates/client/Client.scss @@ -3,13 +3,26 @@ .client-container { display: flex; height: 100%; + + @include screen.biggerThan(mobileBreakpoint) { + position: fixed; + top: 0; + right: 0; + width: calc(100% - var(--navigation-width)); + } } .navigation__wrapper { width: var(--navigation-width); @include screen.smallerThan(mobileBreakpoint) { + position: fixed; + left: 0; + top: 0; width: 100%; + height: 100%; + transition: all .25s ease; + z-index: 1000; } } @@ -22,6 +35,15 @@ .client__item-hidden { display: none; } + + .client__item-background { + filter: brightness(50%); + } + + .client__item-sided { + transition: all .25s ease; + transform: translateX(-100%); + } } .loading-display { diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts index 5cbe3806b..d33485231 100644 --- a/src/app/utils/common.ts +++ b/src/app/utils/common.ts @@ -95,3 +95,9 @@ export const trimLeadingSlash = (str: string): string => str.replace(START_SLASH export const trimTrailingSlash = (str: string): string => str.replace(END_SLASHES_REG, ''); export const trimSlash = (str: string): string => trimLeadingSlash(trimTrailingSlash(str)); + +export const clamp = (val: number, min: number, max: number) => { + if (val <= min) return min; + if (val >= max) return max; + return val; +} \ No newline at end of file From 636c0723c28f963fd177559b1c965559ef7a4068 Mon Sep 17 00:00:00 2001 From: North-West-Wind Date: Thu, 29 Feb 2024 21:46:24 +0800 Subject: [PATCH 02/10] slide out menu and image touch pan --- .../components/image-viewer/ImageViewer.tsx | 3 +- src/app/hooks/usePan.ts | 63 ++++++++++++++++++- src/app/organisms/room/RoomView.scss | 2 +- src/app/templates/client/Client.jsx | 27 +++++--- src/app/templates/client/Client.scss | 10 +-- 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/src/app/components/image-viewer/ImageViewer.tsx b/src/app/components/image-viewer/ImageViewer.tsx index 4fd06b7a7..9d8d93b22 100644 --- a/src/app/components/image-viewer/ImageViewer.tsx +++ b/src/app/components/image-viewer/ImageViewer.tsx @@ -16,7 +16,7 @@ export type ImageViewerProps = { export const ImageViewer = as<'div', ImageViewerProps>( ({ className, alt, src, requestClose, ...props }, ref) => { const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2); - const { pan, cursor, onMouseDown } = usePan(zoom !== 1); + const { pan, cursor, onMouseDown, onTouchStart } = usePan(zoom > 1); const handleDownload = () => { FileSaver.saveAs(src, alt); @@ -87,6 +87,7 @@ export const ImageViewer = as<'div', ImageViewerProps>( src={src} alt={alt} onMouseDown={onMouseDown} + onTouchStart={onTouchStart} /> diff --git a/src/app/hooks/usePan.ts b/src/app/hooks/usePan.ts index 60d7954f3..6147768dd 100644 --- a/src/app/hooks/usePan.ts +++ b/src/app/hooks/usePan.ts @@ -1,20 +1,35 @@ -import { MouseEventHandler, useEffect, useState } from 'react'; +import { MouseEventHandler, TouchEventHandler, useEffect, useState } from 'react'; export type Pan = { translateX: number; translateY: number; }; +export type TouchPos = { + touchX: number; + touchY: number; + initX: number; + initY: number; +} + const INITIAL_PAN = { translateX: 0, translateY: 0, }; +const INITIAL_TOUCH_POS = { + touchX: 0, + touchY: 0, + initX: 0, + initY: 0 +}; + export const usePan = (active: boolean) => { const [pan, setPan] = useState(INITIAL_PAN); const [cursor, setCursor] = useState<'grab' | 'grabbing' | 'initial'>( active ? 'grab' : 'initial' ); + const [touchPos, setTouchPos] = useState(INITIAL_TOUCH_POS); useEffect(() => { setCursor(active ? 'grab' : 'initial'); @@ -50,6 +65,51 @@ export const usePan = (active: boolean) => { document.addEventListener('mouseup', handleMouseUp); }; + const handleTouchMove = (evt: TouchEvent) => { + evt.preventDefault(); + evt.stopPropagation(); + evt.stopImmediatePropagation(); + + let x = evt.touches[0].clientX; + let y = evt.touches[0].clientY; + + setTouchPos(pos => { + pos.touchX = x; + pos.touchY = y; + setPan({ translateX: pos.touchX - pos.initX, translateY: pos.touchY - pos.initY }); + return pos; + }); + } + + const handleTouchEnd = (evt: TouchEvent) => { + evt.preventDefault(); + evt.stopPropagation(); + evt.stopImmediatePropagation(); + setCursor('grab'); + + window.removeEventListener('touchmove', handleTouchMove); + window.removeEventListener('touchend', handleTouchEnd); + } + + const handleTouchStart: TouchEventHandler = (evt) => { + if (!active || evt.touches.length != 1) return; + evt.preventDefault(); + evt.stopPropagation(); + setCursor('grabbing'); + + let x = evt.touches[0].clientX; + let y = evt.touches[0].clientY; + setTouchPos({ + touchX: x, + touchY: y, + initX: x, + initY: y + }); + + window.addEventListener('touchmove', handleTouchMove); + window.addEventListener('touchend', handleTouchEnd); + } + useEffect(() => { if (!active) setPan(INITIAL_PAN); }, [active]); @@ -58,5 +118,6 @@ export const usePan = (active: boolean) => { pan, cursor, onMouseDown: handleMouseDown, + onTouchStart: handleTouchStart, }; }; diff --git a/src/app/organisms/room/RoomView.scss b/src/app/organisms/room/RoomView.scss index c70c2b092..05ba919f2 100644 --- a/src/app/organisms/room/RoomView.scss +++ b/src/app/organisms/room/RoomView.scss @@ -9,7 +9,7 @@ width: 100%; position: absolute; top: 0; - z-index: 999; + z-index: 998; box-shadow: none; transition: transform 200ms var(--fluid-slide-down); diff --git a/src/app/templates/client/Client.jsx b/src/app/templates/client/Client.jsx index 1579621c9..bf539af8f 100644 --- a/src/app/templates/client/Client.jsx +++ b/src/app/templates/client/Client.jsx @@ -50,26 +50,32 @@ function Client() { const roomWrapperRef = useRef(null); function onRoomSelected() { - navWrapperRef.current?.classList.add(classNameSided); roomWrapperRef.current?.classList.remove(classNameHidden); + navWrapperRef.current?.classList.add(classNameSided); } function onNavigationSelected() { navWrapperRef.current?.classList.remove(classNameSided); - roomWrapperRef.current?.classList.add(classNameHidden); + setTimeout(() => roomWrapperRef.current?.classList.add(classNameHidden), 250); } - // Touch variables. Don't use states as we don't want any re-render. + let lastTouch = 0, sideVelocity = 0; function onTouchStart(event) { if (!navWrapperRef.current?.classList.contains(classNameSided)) return; if (event.touches.length != 1) return setTouchingSide(false); - if (event.touches[0].clientX < window.innerWidth * 0.1) setTouchingSide(true); + if (event.touches[0].clientX < window.innerWidth * 0.1) { + setTouchingSide(true); + lastTouch = Date.now(); + } } function onTouchEnd(event) { if (!navWrapperRef.current?.classList.contains(classNameSided)) return; setTouchingSide(isTouchingSide => { if (isTouchingSide) { - event.preventDefault(); setSideMoved(sideMoved => { - if (sideMoved > 0.5) openNavigation(); + if (sideMoved) { + event.preventDefault(); + if (sideMoved > window.innerWidth * 0.5 || sideVelocity >= (window.innerWidth * 0.1 / 250)) openNavigation(); + } + sideVelocity = lastTouch = 0; return 0; }); } @@ -82,7 +88,12 @@ function Client() { if (isTouchingSide) { event.preventDefault(); if (event.changedTouches.length != 1) return setSideMoved(0); - setSideMoved(event.changedTouches[0].clientX / window.innerWidth); + setSideMoved(sideMoved => { + const newSideMoved = event.changedTouches[0].clientX; + sideVelocity = (newSideMoved - sideMoved) / (Date.now() - lastTouch); + lastTouch = Date.now(); + return newSideMoved; + }); } return isTouchingSide; }); @@ -163,7 +174,7 @@ function Client() { return ( -
+
diff --git a/src/app/templates/client/Client.scss b/src/app/templates/client/Client.scss index 60eb55e98..e71e91f77 100644 --- a/src/app/templates/client/Client.scss +++ b/src/app/templates/client/Client.scss @@ -14,15 +14,15 @@ .navigation__wrapper { width: var(--navigation-width); + position: fixed; + left: 0; + top: 0; + height: 100%; @include screen.smallerThan(mobileBreakpoint) { - position: fixed; - left: 0; - top: 0; width: 100%; - height: 100%; transition: all .25s ease; - z-index: 1000; + z-index: 999; } } From 1c4e302d89a913a9f2c129055d0ac6735e4652b6 Mon Sep 17 00:00:00 2001 From: North-West-Wind Date: Fri, 1 Mar 2024 10:51:46 +0800 Subject: [PATCH 03/10] start of swipe reply --- src/app/organisms/room/RoomTimeline.tsx | 51 +++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index 0c74de520..7fd39d38a 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -3,6 +3,7 @@ import React, { MouseEventHandler, RefObject, SetStateAction, + TouchEvent, useCallback, useEffect, useLayoutEffect, @@ -989,6 +990,53 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli [editor] ); + const [isTouchingSide, setTouchingSide] = useState(false); + const [sideMoved, setSideMoved] = useState(0); + const [swipingId, setSwipingId] = useState(""); + + let lastTouch = 0, sideVelocity = 0; + function onTouchStart(event: TouchEvent) { + if (event.touches.length != 1) return setTouchingSide(false); + if (event.touches[0].clientX > window.innerWidth * 0.1) { + setTouchingSide(true); + lastTouch = Date.now(); + } + } + function onTouchEnd(event: TouchEvent) { + setTouchingSide(isTouchingSide => { + if (isTouchingSide) { + setSideMoved(sideMoved => { + if (sideMoved) { + event.preventDefault(); + if (sideMoved > window.innerWidth * 0.5 || sideVelocity <= -(window.innerWidth * 0.1 / 250)) setSwipingId(swipingId => { + handleOpenReply(swipingId); + }); + } + sideVelocity = lastTouch = 0; + return 0; + }); + } + return false; + }); + } + function onTouchMove(event: TouchEvent, replyId: string | undefined) { + if (!replyId || event.touches.length != 1) return; + setSwipingId(replyId); + setTouchingSide(isTouchingSide => { + if (isTouchingSide) { + event.preventDefault(); + if (event.changedTouches.length != 1) setSideMoved(0); + else setSideMoved(sideMoved => { + const newSideMoved = event.changedTouches[0].clientX; + sideVelocity = (newSideMoved - sideMoved) / (Date.now() - lastTouch); + lastTouch = Date.now(); + return newSideMoved; + }); + } + return isTouchingSide; + }); + } + const renderBody = (body: string, customBody?: string) => { if (body === '') ; if (customBody) { @@ -1283,6 +1331,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli onReplyClick={handleReplyClick} onReactionToggle={handleReactionToggle} onEditId={handleEdit} + onTouchStart={(evt: TouchEvent) => onTouchStart(evt)} + onTouchMove={(evt: TouchEvent) => onTouchMove(evt)} + onTouchEnd={(evt: TouchEvent) => onTouchEnd(evt)} reply={ replyEventId && ( Date: Fri, 1 Mar 2024 10:51:56 +0800 Subject: [PATCH 04/10] clean up unused variable --- src/app/templates/client/Client.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/templates/client/Client.jsx b/src/app/templates/client/Client.jsx index bf539af8f..7ca7637cc 100644 --- a/src/app/templates/client/Client.jsx +++ b/src/app/templates/client/Client.jsx @@ -43,7 +43,6 @@ function Client() { const [isTouchingSide, setTouchingSide] = useState(false); const [sideMoved, setSideMoved] = useState(0); const classNameHidden = 'client__item-hidden'; - const classNameBackground = 'client__item-background'; const classNameSided = 'client__item-sided'; const navWrapperRef = useRef(null); From f5d6a546b369f50b8145099882113519486b3df3 Mon Sep 17 00:00:00 2001 From: NorthWestWind Date: Fri, 1 Mar 2024 12:25:22 +0800 Subject: [PATCH 05/10] swipe left to reply --- src/app/organisms/room/RoomTimeline.tsx | 57 ++++++++++++++++--------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index 7fd39d38a..0bc6cc434 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -1,5 +1,6 @@ import React, { Dispatch, + MouseEvent, MouseEventHandler, RefObject, SetStateAction, @@ -102,7 +103,7 @@ import { selectTab, } from '../../../client/action/navigation'; import { useForceUpdate } from '../../hooks/useForceUpdate'; -import { parseGeoUri, scaleYDimension } from '../../utils/common'; +import { clamp, parseGeoUri, scaleYDimension } from '../../utils/common'; import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer'; import { useRoomMsgContentRenderer } from '../../hooks/useRoomMsgContentRenderer'; import { IAudioContent, IImageContent, IVideoContent } from '../../../types/matrix/common'; @@ -929,9 +930,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli [mx, room, editor] ); - const handleReplyClick: MouseEventHandler = useCallback( - (evt) => { - const replyId = evt.currentTarget.getAttribute('data-event-id'); + const handleReplyId = useCallback( + (replyId: string | null) => { if (!replyId) { console.warn('Button should have "data-event-id" attribute!'); return; @@ -992,14 +992,19 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const [isTouchingSide, setTouchingSide] = useState(false); const [sideMoved, setSideMoved] = useState(0); + const [sideMovedInit, setSideMovedInit] = useState(0); const [swipingId, setSwipingId] = useState(""); let lastTouch = 0, sideVelocity = 0; - function onTouchStart(event: TouchEvent) { + function onTouchStart(event: TouchEvent, replyId: string | undefined) { if (event.touches.length != 1) return setTouchingSide(false); if (event.touches[0].clientX > window.innerWidth * 0.1) { setTouchingSide(true); + setSideMoved(event.touches[0].clientX); + setSideMovedInit(event.touches[0].clientX); + setSwipingId(replyId || ""); lastTouch = Date.now(); + console.log(replyId); } } function onTouchEnd(event: TouchEvent) { @@ -1007,9 +1012,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli if (isTouchingSide) { setSideMoved(sideMoved => { if (sideMoved) { - event.preventDefault(); - if (sideMoved > window.innerWidth * 0.5 || sideVelocity <= -(window.innerWidth * 0.1 / 250)) setSwipingId(swipingId => { - handleOpenReply(swipingId); + setSideMovedInit(sideMovedInit => { + if ((sideMoved - sideMovedInit) < -(window.innerWidth * 0.2) || sideVelocity <= -(window.innerWidth * 0.05 / 250)) setSwipingId(swipingId => { + event.preventDefault(); + handleReplyId(swipingId); + return ""; + }); + return 0; }); } sideVelocity = lastTouch = 0; @@ -1020,17 +1029,22 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli }); } function onTouchMove(event: TouchEvent, replyId: string | undefined) { - if (!replyId || event.touches.length != 1) return; - setSwipingId(replyId); + if (event.touches.length != 1) return; setTouchingSide(isTouchingSide => { if (isTouchingSide) { - event.preventDefault(); - if (event.changedTouches.length != 1) setSideMoved(0); - else setSideMoved(sideMoved => { - const newSideMoved = event.changedTouches[0].clientX; - sideVelocity = (newSideMoved - sideMoved) / (Date.now() - lastTouch); - lastTouch = Date.now(); - return newSideMoved; + setSwipingId(swipingId => { + if (swipingId == replyId) { + console.log(replyId); + event.preventDefault(); + if (event.changedTouches.length != 1) setSideMoved(0); + else setSideMoved(sideMoved => { + const newSideMoved = event.changedTouches[0].clientX; + sideVelocity = (newSideMoved - sideMoved) / (Date.now() - lastTouch); + lastTouch = Date.now(); + return newSideMoved; + }); + } + return swipingId; }); } return isTouchingSide; @@ -1328,12 +1342,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli relations={hasReactions ? reactionRelations : undefined} onUserClick={handleUserClick} onUsernameClick={handleUsernameClick} - onReplyClick={handleReplyClick} + onReplyClick={(evt: MouseEvent) => handleReplyId(evt.currentTarget.getAttribute('data-event-id'))} onReactionToggle={handleReactionToggle} onEditId={handleEdit} - onTouchStart={(evt: TouchEvent) => onTouchStart(evt)} - onTouchMove={(evt: TouchEvent) => onTouchMove(evt)} - onTouchEnd={(evt: TouchEvent) => onTouchEnd(evt)} + onTouchStart={(evt: TouchEvent) => onTouchStart(evt, mEvent.getId())} + onTouchMove={(evt: TouchEvent) => onTouchMove(evt, mEvent.getId())} + onTouchEnd={onTouchEnd} + style={isTouchingSide && mEvent.getId() == swipingId ? { transform: `translateX(${clamp(sideMoved - sideMovedInit, -window.innerWidth, 0)}px)` } : { transition: "all .25s ease" }} reply={ replyEventId && ( Date: Fri, 1 Mar 2024 12:32:33 +0800 Subject: [PATCH 06/10] fixed changed reply click handler --- src/app/organisms/room/RoomTimeline.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index 0bc6cc434..dcbacaca5 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -1404,7 +1404,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli relations={hasReactions ? reactionRelations : undefined} onUserClick={handleUserClick} onUsernameClick={handleUsernameClick} - onReplyClick={handleReplyClick} + onReplyClick={(evt: MouseEvent) => handleReplyId(evt.currentTarget.getAttribute('data-event-id'))} onReactionToggle={handleReactionToggle} onEditId={handleEdit} reply={ @@ -1479,7 +1479,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli relations={hasReactions ? reactionRelations : undefined} onUserClick={handleUserClick} onUsernameClick={handleUsernameClick} - onReplyClick={handleReplyClick} + onReplyClick={(evt: MouseEvent) => handleReplyId(evt.currentTarget.getAttribute('data-event-id'))} onReactionToggle={handleReactionToggle} reactions={ reactionRelations && ( @@ -1890,4 +1890,4 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli )} ); -} +} \ No newline at end of file From 9be2f56e78de03ee468e93d4c7be4556156ec339 Mon Sep 17 00:00:00 2001 From: NorthWestWind Date: Fri, 1 Mar 2024 14:30:33 +0800 Subject: [PATCH 07/10] mobile gesture comments --- src/app/hooks/usePan.ts | 3 ++- src/app/organisms/room/RoomTimeline.tsx | 11 +++++++++++ src/app/templates/client/Client.jsx | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/app/hooks/usePan.ts b/src/app/hooks/usePan.ts index 6147768dd..6fbf759cd 100644 --- a/src/app/hooks/usePan.ts +++ b/src/app/hooks/usePan.ts @@ -29,7 +29,7 @@ export const usePan = (active: boolean) => { const [cursor, setCursor] = useState<'grab' | 'grabbing' | 'initial'>( active ? 'grab' : 'initial' ); - const [touchPos, setTouchPos] = useState(INITIAL_TOUCH_POS); + const [_touchPos, setTouchPos] = useState(INITIAL_TOUCH_POS); useEffect(() => { setCursor(active ? 'grab' : 'initial'); @@ -65,6 +65,7 @@ export const usePan = (active: boolean) => { document.addEventListener('mouseup', handleMouseUp); }; + // Touch handlers for usePan. Intentionally not handling 2 touches (may do in the future). const handleTouchMove = (evt: TouchEvent) => { evt.preventDefault(); evt.stopPropagation(); diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index dcbacaca5..6afd99ce4 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -930,6 +930,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli [mx, room, editor] ); + // Replaces handleReplyClick. This takes an event id instead of the event to allow swipe-left-reply to directly cause a reply. const handleReplyId = useCallback( (replyId: string | null) => { if (!replyId) { @@ -990,11 +991,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli [editor] ); + // States used for swipe-left-reply. Mostly used for animations. const [isTouchingSide, setTouchingSide] = useState(false); const [sideMoved, setSideMoved] = useState(0); const [sideMovedInit, setSideMovedInit] = useState(0); const [swipingId, setSwipingId] = useState(""); + // Touch handlers for the Message components. If touch starts at 90% of the right, it will trigger the swipe-left-reply. let lastTouch = 0, sideVelocity = 0; function onTouchStart(event: TouchEvent, replyId: string | undefined) { if (event.touches.length != 1) return setTouchingSide(false); @@ -1407,6 +1410,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli onReplyClick={(evt: MouseEvent) => handleReplyId(evt.currentTarget.getAttribute('data-event-id'))} onReactionToggle={handleReactionToggle} onEditId={handleEdit} + onTouchStart={(evt: TouchEvent) => onTouchStart(evt, mEvent.getId())} + onTouchMove={(evt: TouchEvent) => onTouchMove(evt, mEvent.getId())} + onTouchEnd={onTouchEnd} + style={isTouchingSide && mEvent.getId() == swipingId ? { transform: `translateX(${clamp(sideMoved - sideMovedInit, -window.innerWidth, 0)}px)` } : { transition: "all .25s ease" }} reply={ replyEventId && ( handleReplyId(evt.currentTarget.getAttribute('data-event-id'))} onReactionToggle={handleReactionToggle} + onTouchStart={(evt: TouchEvent) => onTouchStart(evt, mEvent.getId())} + onTouchMove={(evt: TouchEvent) => onTouchMove(evt, mEvent.getId())} + onTouchEnd={onTouchEnd} + style={isTouchingSide && mEvent.getId() == swipingId ? { transform: `translateX(${clamp(sideMoved - sideMovedInit, -window.innerWidth, 0)}px)` } : { transition: "all .25s ease" }} reactions={ reactionRelations && ( roomWrapperRef.current?.classList.add(classNameHidden), 250); } + // Touch handlers for window object. If the touch starts at 10% of the left of the screen, it will trigger the swipe-right-menu. let lastTouch = 0, sideVelocity = 0; function onTouchStart(event) { if (!navWrapperRef.current?.classList.contains(classNameSided)) return; From af699cf7b0bde8f924e36bf9c097598fa30b101e Mon Sep 17 00:00:00 2001 From: NorthWestWind Date: Fri, 1 Mar 2024 15:26:17 +0800 Subject: [PATCH 08/10] fixed warning & no reply with active image viewer --- src/app/hooks/usePan.ts | 8 ++++---- src/app/organisms/room/RoomTimeline.tsx | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/app/hooks/usePan.ts b/src/app/hooks/usePan.ts index 6fbf759cd..9c8f6d6d7 100644 --- a/src/app/hooks/usePan.ts +++ b/src/app/hooks/usePan.ts @@ -88,8 +88,8 @@ export const usePan = (active: boolean) => { evt.stopImmediatePropagation(); setCursor('grab'); - window.removeEventListener('touchmove', handleTouchMove); - window.removeEventListener('touchend', handleTouchEnd); + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleTouchEnd); } const handleTouchStart: TouchEventHandler = (evt) => { @@ -107,8 +107,8 @@ export const usePan = (active: boolean) => { initY: y }); - window.addEventListener('touchmove', handleTouchMove); - window.addEventListener('touchend', handleTouchEnd); + document.addEventListener('touchmove', handleTouchMove); + document.addEventListener('touchend', handleTouchEnd); } useEffect(() => { diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index 6afd99ce4..6ea7b25ef 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -991,7 +991,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli [editor] ); - // States used for swipe-left-reply. Mostly used for animations. + // States used for swipe-left-reply. Used for animations and determining whether we should reply or not. const [isTouchingSide, setTouchingSide] = useState(false); const [sideMoved, setSideMoved] = useState(0); const [sideMovedInit, setSideMovedInit] = useState(0); @@ -1001,13 +1001,15 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli let lastTouch = 0, sideVelocity = 0; function onTouchStart(event: TouchEvent, replyId: string | undefined) { if (event.touches.length != 1) return setTouchingSide(false); - if (event.touches[0].clientX > window.innerWidth * 0.1) { + if ( + event.touches[0].clientX > window.innerWidth * 0.1 && + !Array.from(document.elementsFromPoint(event.touches[0].clientX, event.touches[0].clientY)[0].classList).some(c => c.startsWith("ImageViewer")) // Disable gesture if ImageViewer is up. There's probably a better way I don't know + ) { setTouchingSide(true); setSideMoved(event.touches[0].clientX); setSideMovedInit(event.touches[0].clientX); setSwipingId(replyId || ""); lastTouch = Date.now(); - console.log(replyId); } } function onTouchEnd(event: TouchEvent) { @@ -1018,7 +1020,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli setSideMovedInit(sideMovedInit => { if ((sideMoved - sideMovedInit) < -(window.innerWidth * 0.2) || sideVelocity <= -(window.innerWidth * 0.05 / 250)) setSwipingId(swipingId => { event.preventDefault(); - handleReplyId(swipingId); + setTimeout(() => handleReplyId(swipingId), 100); return ""; }); return 0; @@ -1037,7 +1039,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli if (isTouchingSide) { setSwipingId(swipingId => { if (swipingId == replyId) { - console.log(replyId); event.preventDefault(); if (event.changedTouches.length != 1) setSideMoved(0); else setSideMoved(sideMoved => { From 677ad454ef2f434b6cd08958be1e6930182657a3 Mon Sep 17 00:00:00 2001 From: North-West-Wind Date: Thu, 30 May 2024 16:22:43 +0800 Subject: [PATCH 09/10] use custom hook for swiping --- src/app/hooks/useSwipeLeft.ts | 77 +++++++++++++++++++++++++ src/app/hooks/useTouchMenu.ts | 63 ++++++++++++++++++++ src/app/organisms/room/RoomTimeline.tsx | 66 +-------------------- src/app/templates/client/Client.jsx | 46 +-------------- 4 files changed, 145 insertions(+), 107 deletions(-) create mode 100644 src/app/hooks/useSwipeLeft.ts create mode 100644 src/app/hooks/useTouchMenu.ts diff --git a/src/app/hooks/useSwipeLeft.ts b/src/app/hooks/useSwipeLeft.ts new file mode 100644 index 000000000..525a5831a --- /dev/null +++ b/src/app/hooks/useSwipeLeft.ts @@ -0,0 +1,77 @@ +import { TouchEvent, useState } from "react"; + +export const useSwipeLeft = (handleReplyId: (replyId: string | null) => void) => { + // States used for swipe-left-reply. Used for animations and determining whether we should reply or not. + const [isTouchingSide, setTouchingSide] = useState(false); + const [sideMoved, setSideMoved] = useState(0); + const [sideMovedInit, setSideMovedInit] = useState(0); + const [swipingId, setSwipingId] = useState(""); + + // Touch handlers for the Message components. If touch starts at 90% of the right, it will trigger the swipe-left-reply. + let lastTouch = 0, sideVelocity = 0; + function onTouchStart(event: TouchEvent, replyId: string | undefined) { + if (event.touches.length != 1) return setTouchingSide(false); + if ( + event.touches[0].clientX > window.innerWidth * 0.1 && + !Array.from(document.elementsFromPoint(event.touches[0].clientX, event.touches[0].clientY)[0].classList).some(c => c.startsWith("ImageViewer")) // Disable gesture if ImageViewer is up. There's probably a better way I don't know + ) { + setTouchingSide(true); + setSideMoved(event.touches[0].clientX); + setSideMovedInit(event.touches[0].clientX); + setSwipingId(replyId || ""); + lastTouch = Date.now(); + } + } + function onTouchEnd(event: TouchEvent) { + setTouchingSide(isTouchingSide => { + if (isTouchingSide) { + setSideMoved(sideMoved => { + if (sideMoved) { + setSideMovedInit(sideMovedInit => { + if ((sideMoved - sideMovedInit) < -(window.innerWidth * 0.2) || sideVelocity <= -(window.innerWidth * 0.05 / 250)) setSwipingId(swipingId => { + event.preventDefault(); + setTimeout(() => handleReplyId(swipingId), 100); + return ""; + }); + return 0; + }); + } + sideVelocity = lastTouch = 0; + return 0; + }); + } + return false; + }); + } + function onTouchMove(event: TouchEvent, replyId: string | undefined) { + if (event.touches.length != 1) return; + setTouchingSide(isTouchingSide => { + if (isTouchingSide) { + setSwipingId(swipingId => { + if (swipingId == replyId) { + event.preventDefault(); + if (event.changedTouches.length != 1) setSideMoved(0); + else setSideMoved(sideMoved => { + const newSideMoved = event.changedTouches[0].clientX; + sideVelocity = (newSideMoved - sideMoved) / (Date.now() - lastTouch); + lastTouch = Date.now(); + return newSideMoved; + }); + } + return swipingId; + }); + } + return isTouchingSide; + }); + } + + return { + isTouchingSide, + sideMoved, + sideMovedInit, + swipingId, + onTouchStart, + onTouchMove, + onTouchEnd + } +} \ No newline at end of file diff --git a/src/app/hooks/useTouchMenu.ts b/src/app/hooks/useTouchMenu.ts new file mode 100644 index 000000000..b1aa54ff2 --- /dev/null +++ b/src/app/hooks/useTouchMenu.ts @@ -0,0 +1,63 @@ +import { RefObject, TouchEvent, useState } from "react" +import { openNavigation } from "../../client/action/navigation"; + +export const useTouchMenu = (navWrapperRef: RefObject, classNameSided: string) => { + const [lastTouch, setLastTouch] = useState(0); + const [sideVelocity, setSideVelocity] = useState(0); + const [sideMoved, setSideMoved] = useState(0); + const [isTouchingSide, setTouchingSide] = useState(false); + + // Touch handlers for window object. If the touch starts at 10% of the left of the screen, it will trigger the swipe-right-menu. + const onTouchStart = (event: TouchEvent) => { + if (!navWrapperRef.current?.classList.contains(classNameSided)) return; + if (event.touches.length != 1) return setTouchingSide(false); + if (event.touches[0].clientX < window.innerWidth * 0.1) { + setTouchingSide(true); + setLastTouch(Date.now()); + } + } + const onTouchEnd = (event: TouchEvent) => { + if (!navWrapperRef.current?.classList.contains(classNameSided)) return; + setTouchingSide(isTouchingSide => { + if (isTouchingSide) { + setSideMoved(sideMoved => { + if (sideMoved) { + event.preventDefault(); + if (sideMoved > window.innerWidth * 0.5 || sideVelocity >= (window.innerWidth * 0.1 / 250)) openNavigation(); + } + setLastTouch(0); + setSideVelocity(0); + return 0; + }); + } + return false; + }); + } + const onTouchMove = (event: TouchEvent) => { + if (!navWrapperRef.current?.classList.contains(classNameSided)) return; + setTouchingSide(isTouchingSide => { + if (isTouchingSide) { + event.preventDefault(); + if (event.changedTouches.length != 1) { + setSideMoved(0); + return false; + } + setSideMoved(sideMoved => { + const newSideMoved = event.changedTouches[0].clientX; + setSideVelocity((newSideMoved - sideMoved) / (Date.now() - lastTouch)); + setLastTouch(Date.now()); + return newSideMoved; + }); + } + return isTouchingSide; + }); + } + + return { + isTouchingSide, + sideMoved, + onTouchStart, + onTouchMove, + onTouchEnd + } +} \ No newline at end of file diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index 6ea7b25ef..beb5d350c 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -143,6 +143,7 @@ import cons from '../../../client/state/cons'; import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange'; import { EMOJI_PATTERN, HTTP_URL_PATTERN, VARIATION_SELECTOR_PATTERN } from '../../utils/regex'; import { UrlPreviewCard, UrlPreviewHolder } from './message/UrlPreviewCard'; +import { useSwipeLeft } from '../../hooks/useSwipeLeft'; // Thumbs up emoji found to have Variation Selector 16 at the end // so included variation selector pattern in regex @@ -991,69 +992,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli [editor] ); - // States used for swipe-left-reply. Used for animations and determining whether we should reply or not. - const [isTouchingSide, setTouchingSide] = useState(false); - const [sideMoved, setSideMoved] = useState(0); - const [sideMovedInit, setSideMovedInit] = useState(0); - const [swipingId, setSwipingId] = useState(""); - - // Touch handlers for the Message components. If touch starts at 90% of the right, it will trigger the swipe-left-reply. - let lastTouch = 0, sideVelocity = 0; - function onTouchStart(event: TouchEvent, replyId: string | undefined) { - if (event.touches.length != 1) return setTouchingSide(false); - if ( - event.touches[0].clientX > window.innerWidth * 0.1 && - !Array.from(document.elementsFromPoint(event.touches[0].clientX, event.touches[0].clientY)[0].classList).some(c => c.startsWith("ImageViewer")) // Disable gesture if ImageViewer is up. There's probably a better way I don't know - ) { - setTouchingSide(true); - setSideMoved(event.touches[0].clientX); - setSideMovedInit(event.touches[0].clientX); - setSwipingId(replyId || ""); - lastTouch = Date.now(); - } - } - function onTouchEnd(event: TouchEvent) { - setTouchingSide(isTouchingSide => { - if (isTouchingSide) { - setSideMoved(sideMoved => { - if (sideMoved) { - setSideMovedInit(sideMovedInit => { - if ((sideMoved - sideMovedInit) < -(window.innerWidth * 0.2) || sideVelocity <= -(window.innerWidth * 0.05 / 250)) setSwipingId(swipingId => { - event.preventDefault(); - setTimeout(() => handleReplyId(swipingId), 100); - return ""; - }); - return 0; - }); - } - sideVelocity = lastTouch = 0; - return 0; - }); - } - return false; - }); - } - function onTouchMove(event: TouchEvent, replyId: string | undefined) { - if (event.touches.length != 1) return; - setTouchingSide(isTouchingSide => { - if (isTouchingSide) { - setSwipingId(swipingId => { - if (swipingId == replyId) { - event.preventDefault(); - if (event.changedTouches.length != 1) setSideMoved(0); - else setSideMoved(sideMoved => { - const newSideMoved = event.changedTouches[0].clientX; - sideVelocity = (newSideMoved - sideMoved) / (Date.now() - lastTouch); - lastTouch = Date.now(); - return newSideMoved; - }); - } - return swipingId; - }); - } - return isTouchingSide; - }); - } + // swipe left hook + const { isTouchingSide, sideMoved, sideMovedInit, swipingId, onTouchStart, onTouchMove, onTouchEnd } = useSwipeLeft(handleReplyId); const renderBody = (body: string, customBody?: string) => { if (body === '') ; diff --git a/src/app/templates/client/Client.jsx b/src/app/templates/client/Client.jsx index 7b5a58eda..04cc980a8 100644 --- a/src/app/templates/client/Client.jsx +++ b/src/app/templates/client/Client.jsx @@ -24,6 +24,7 @@ import { ClientContent } from './ClientContent'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { clamp } from '../../utils/common'; +import { useTouchMenu } from '../../hooks/useTouchMenu'; function SystemEmojiFeature() { const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); @@ -40,13 +41,12 @@ function SystemEmojiFeature() { function Client() { const [isLoading, changeLoading] = useState(true); const [loadingMsg, setLoadingMsg] = useState('Heating up'); - const [isTouchingSide, setTouchingSide] = useState(false); - const [sideMoved, setSideMoved] = useState(0); const classNameHidden = 'client__item-hidden'; const classNameSided = 'client__item-sided'; const navWrapperRef = useRef(null); const roomWrapperRef = useRef(null); + const { isTouchingSide, sideMoved, onTouchStart, onTouchMove, onTouchEnd } = useTouchMenu(navWrapperRef, classNameSided); function onRoomSelected() { roomWrapperRef.current?.classList.remove(classNameHidden); @@ -56,48 +56,6 @@ function Client() { navWrapperRef.current?.classList.remove(classNameSided); setTimeout(() => roomWrapperRef.current?.classList.add(classNameHidden), 250); } - // Touch handlers for window object. If the touch starts at 10% of the left of the screen, it will trigger the swipe-right-menu. - let lastTouch = 0, sideVelocity = 0; - function onTouchStart(event) { - if (!navWrapperRef.current?.classList.contains(classNameSided)) return; - if (event.touches.length != 1) return setTouchingSide(false); - if (event.touches[0].clientX < window.innerWidth * 0.1) { - setTouchingSide(true); - lastTouch = Date.now(); - } - } - function onTouchEnd(event) { - if (!navWrapperRef.current?.classList.contains(classNameSided)) return; - setTouchingSide(isTouchingSide => { - if (isTouchingSide) { - setSideMoved(sideMoved => { - if (sideMoved) { - event.preventDefault(); - if (sideMoved > window.innerWidth * 0.5 || sideVelocity >= (window.innerWidth * 0.1 / 250)) openNavigation(); - } - sideVelocity = lastTouch = 0; - return 0; - }); - } - return false; - }); - } - function onTouchMove(event) { - if (!navWrapperRef.current?.classList.contains(classNameSided)) return; - setTouchingSide(isTouchingSide => { - if (isTouchingSide) { - event.preventDefault(); - if (event.changedTouches.length != 1) return setSideMoved(0); - setSideMoved(sideMoved => { - const newSideMoved = event.changedTouches[0].clientX; - sideVelocity = (newSideMoved - sideMoved) / (Date.now() - lastTouch); - lastTouch = Date.now(); - return newSideMoved; - }); - } - return isTouchingSide; - }); - } useEffect(() => { navigation.on(cons.events.navigation.ROOM_SELECTED, onRoomSelected); From 67077d9a3f79145a7a7162cd084547953baa9358 Mon Sep 17 00:00:00 2001 From: North-West-Wind Date: Thu, 30 May 2024 16:29:11 +0800 Subject: [PATCH 10/10] put message transition in css --- src/app/organisms/room/RoomTimeline.tsx | 6 +++--- src/app/organisms/room/message/styles.css.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index beb5d350c..ba3cfa00e 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -1292,7 +1292,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli onTouchStart={(evt: TouchEvent) => onTouchStart(evt, mEvent.getId())} onTouchMove={(evt: TouchEvent) => onTouchMove(evt, mEvent.getId())} onTouchEnd={onTouchEnd} - style={isTouchingSide && mEvent.getId() == swipingId ? { transform: `translateX(${clamp(sideMoved - sideMovedInit, -window.innerWidth, 0)}px)` } : { transition: "all .25s ease" }} + style={{ transform: `translateX(${isTouchingSide && mEvent.getId() == swipingId ? clamp(sideMoved - sideMovedInit, -window.innerWidth, 0) : 0}px)` }} reply={ replyEventId && ( onTouchStart(evt, mEvent.getId())} onTouchMove={(evt: TouchEvent) => onTouchMove(evt, mEvent.getId())} onTouchEnd={onTouchEnd} - style={isTouchingSide && mEvent.getId() == swipingId ? { transform: `translateX(${clamp(sideMoved - sideMovedInit, -window.innerWidth, 0)}px)` } : { transition: "all .25s ease" }} + style={{ transform: `translateX(${isTouchingSide && mEvent.getId() == swipingId ? clamp(sideMoved - sideMovedInit, -window.innerWidth, 0) : 0}px)` }} reply={ replyEventId && ( onTouchStart(evt, mEvent.getId())} onTouchMove={(evt: TouchEvent) => onTouchMove(evt, mEvent.getId())} onTouchEnd={onTouchEnd} - style={isTouchingSide && mEvent.getId() == swipingId ? { transform: `translateX(${clamp(sideMoved - sideMovedInit, -window.innerWidth, 0)}px)` } : { transition: "all .25s ease" }} + style={{ transform: `translateX(${isTouchingSide && mEvent.getId() == swipingId ? clamp(sideMoved - sideMovedInit, -window.innerWidth, 0) : 0}px)` }} reactions={ reactionRelations && (