Add story gestures
Add swipe up and down gestures
This commit is contained in:
parent
eddbd476b6
commit
9caa8a560b
@ -8,7 +8,12 @@ import type { RealTouchEvent } from '../../util/captureEvents';
|
|||||||
|
|
||||||
import { animateNumber, timingFunctions } from '../../util/animation';
|
import { animateNumber, timingFunctions } from '../../util/animation';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { captureEvents, IOS_SCREEN_EDGE_THRESHOLD } from '../../util/captureEvents';
|
import {
|
||||||
|
captureEvents,
|
||||||
|
IOS_SCREEN_EDGE_THRESHOLD,
|
||||||
|
SWIPE_DIRECTION_THRESHOLD,
|
||||||
|
SWIPE_DIRECTION_TOLERANCE,
|
||||||
|
} from '../../util/captureEvents';
|
||||||
import { clamp, isBetween, round } from '../../util/math';
|
import { clamp, isBetween, round } from '../../util/math';
|
||||||
import { debounce } from '../../util/schedulers';
|
import { debounce } from '../../util/schedulers';
|
||||||
import { IS_IOS, IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
import { IS_IOS, IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||||
@ -459,8 +464,6 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
|||||||
const absOffsetX = Math.abs(dragOffsetX);
|
const absOffsetX = Math.abs(dragOffsetX);
|
||||||
const absOffsetY = Math.abs(dragOffsetY);
|
const absOffsetY = Math.abs(dragOffsetY);
|
||||||
const { x, y, scale } = transformRef.current;
|
const { x, y, scale } = transformRef.current;
|
||||||
const threshold = 10;
|
|
||||||
const tolerance = 1.5;
|
|
||||||
|
|
||||||
// If user is inactive but is still touching the screen
|
// If user is inactive but is still touching the screen
|
||||||
// we reset last gesture time
|
// we reset last gesture time
|
||||||
@ -490,7 +493,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
|||||||
// If user is swiping horizontally or horizontal shift is dominant
|
// If user is swiping horizontally or horizontal shift is dominant
|
||||||
// we change only horizontal position
|
// we change only horizontal position
|
||||||
if (swipeDirectionRef.current === SwipeDirection.Horizontal
|
if (swipeDirectionRef.current === SwipeDirection.Horizontal
|
||||||
|| Math.abs(x) > threshold || absOffsetX / absOffsetY > tolerance) {
|
|| Math.abs(x) > SWIPE_DIRECTION_THRESHOLD || absOffsetX / absOffsetY > SWIPE_DIRECTION_TOLERANCE) {
|
||||||
swipeDirectionRef.current = SwipeDirection.Horizontal;
|
swipeDirectionRef.current = SwipeDirection.Horizontal;
|
||||||
setIsActive(false);
|
setIsActive(false);
|
||||||
const limit = windowWidth + SLIDES_GAP;
|
const limit = windowWidth + SLIDES_GAP;
|
||||||
@ -512,7 +515,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
// If vertical shift is dominant we change only vertical position
|
// If vertical shift is dominant we change only vertical position
|
||||||
if (swipeDirectionRef.current === SwipeDirection.Vertical
|
if (swipeDirectionRef.current === SwipeDirection.Vertical
|
||||||
|| Math.abs(y) > threshold || absOffsetY / absOffsetX > tolerance) {
|
|| Math.abs(y) > SWIPE_DIRECTION_THRESHOLD || absOffsetY / absOffsetX > SWIPE_DIRECTION_TOLERANCE) {
|
||||||
swipeDirectionRef.current = SwipeDirection.Vertical;
|
swipeDirectionRef.current = SwipeDirection.Vertical;
|
||||||
const limit = windowHeight;
|
const limit = windowHeight;
|
||||||
const y1 = clamp(dragOffsetY, -limit, limit);
|
const y1 = clamp(dragOffsetY, -limit, limit);
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.closeButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
|
|||||||
@ -76,7 +76,7 @@ const StealthModeModal = ({ isOpen, stealthMode, isCurrentUserPremium } : StateP
|
|||||||
round
|
round
|
||||||
color="translucent"
|
color="translucent"
|
||||||
size="smaller"
|
size="smaller"
|
||||||
className={styles.close}
|
className={styles.closeButton}
|
||||||
ariaLabel={lang('Close')}
|
ariaLabel={lang('Close')}
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -497,11 +497,11 @@ function Story({
|
|||||||
ripple={!isMobile}
|
ripple={!isMobile}
|
||||||
size="tiny"
|
size="tiny"
|
||||||
color="translucent-white"
|
color="translucent-white"
|
||||||
className={isOpen ? 'active' : ''}
|
|
||||||
onClick={onTrigger}
|
onClick={onTrigger}
|
||||||
|
className={buildClassName(styles.button, isOpen && 'active')}
|
||||||
ariaLabel={lang('AccDescrOpenMenu2')}
|
ariaLabel={lang('AccDescrOpenMenu2')}
|
||||||
>
|
>
|
||||||
<i className={buildClassName('icon icon-more', styles.topIcon)} aria-hidden />
|
<i className={buildClassName('icon icon-more')} aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -604,7 +604,7 @@ function Story({
|
|||||||
{renderStoryPrivacyButton()}
|
{renderStoryPrivacyButton()}
|
||||||
{isVideo && (
|
{isVideo && (
|
||||||
<Button
|
<Button
|
||||||
className={buildClassName(styles.button, styles.buttonVolume)}
|
className={styles.button}
|
||||||
round
|
round
|
||||||
ripple={!isMobile}
|
ripple={!isMobile}
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@ -617,14 +617,13 @@ function Story({
|
|||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'icon',
|
'icon',
|
||||||
isMuted || noSound ? 'icon-speaker-muted-story' : 'icon-speaker-story',
|
isMuted || noSound ? 'icon-speaker-muted-story' : 'icon-speaker-story',
|
||||||
styles.topIcon,
|
|
||||||
)}
|
)}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
className={buildClassName(styles.button, styles.buttonMenu)}
|
className={styles.buttonMenu}
|
||||||
trigger={MenuButton}
|
trigger={MenuButton}
|
||||||
positionX="right"
|
positionX="right"
|
||||||
onOpen={handleDropdownMenuOpen}
|
onOpen={handleDropdownMenuOpen}
|
||||||
@ -646,6 +645,16 @@ function Story({
|
|||||||
{!isOut && <MenuItem icon="flag" onClick={handleReportStoryClick}>{lang('lng_report_story')}</MenuItem>}
|
{!isOut && <MenuItem icon="flag" onClick={handleReportStoryClick}>{lang('lng_report_story')}</MenuItem>}
|
||||||
{isOut && <MenuItem icon="delete" destructive onClick={handleDeleteStoryClick}>{lang('Delete')}</MenuItem>}
|
{isOut && <MenuItem icon="delete" destructive onClick={handleDeleteStoryClick}>{lang('Delete')}</MenuItem>}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
<Button
|
||||||
|
className={buildClassName(styles.button, styles.closeButton)}
|
||||||
|
round
|
||||||
|
size="tiny"
|
||||||
|
color="translucent-white"
|
||||||
|
ariaLabel={lang('Close')}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<i className={buildClassName('icon icon-close')} aria-hidden />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -727,7 +736,7 @@ function Story({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isLoadedStory && fullMediaData && (
|
{isLoadedStory && fullMediaData && (
|
||||||
<MediaAreaOverlay story={story} className={styles.mediaAreaOverlay} isActive />
|
<MediaAreaOverlay story={story} isActive />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -4,20 +4,35 @@ import React, {
|
|||||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { ApiPeerStories, ApiTypeStory } from '../../api/types';
|
import type { ApiPeerStories, ApiTypeStory } from '../../api/types';
|
||||||
|
import type { RealTouchEvent } from '../../util/captureEvents';
|
||||||
|
|
||||||
import { ANIMATION_END_DELAY } from '../../config';
|
import { ANIMATION_END_DELAY, EDITABLE_STORY_INPUT_ID } from '../../config';
|
||||||
|
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||||
import { getStoryKey } from '../../global/helpers';
|
import { getStoryKey } from '../../global/helpers';
|
||||||
import { selectIsStoryViewerOpen, selectPeer, selectTabState } from '../../global/selectors';
|
import { selectIsStoryViewerOpen, selectPeer, selectTabState } from '../../global/selectors';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import buildStyle from '../../util/buildStyle';
|
import buildStyle from '../../util/buildStyle';
|
||||||
import { IS_FIREFOX, IS_SAFARI } from '../../util/windowEnvironment';
|
import {
|
||||||
|
captureEvents,
|
||||||
|
IOS_SCREEN_EDGE_THRESHOLD,
|
||||||
|
SWIPE_DIRECTION_THRESHOLD,
|
||||||
|
SWIPE_DIRECTION_TOLERANCE,
|
||||||
|
} from '../../util/captureEvents';
|
||||||
|
import focusEditableElement from '../../util/focusEditableElement';
|
||||||
|
import { clamp } from '../../util/math';
|
||||||
|
import { disableScrolling, enableScrolling } from '../../util/scrollLock';
|
||||||
|
import {
|
||||||
|
IS_FIREFOX, IS_IOS, IS_SAFARI,
|
||||||
|
} from '../../util/windowEnvironment';
|
||||||
import { calculateOffsetX } from './helpers/dimensions';
|
import { calculateOffsetX } from './helpers/dimensions';
|
||||||
|
|
||||||
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
import useSignal from '../../hooks/useSignal';
|
import useSignal from '../../hooks/useSignal';
|
||||||
|
import useWindowSize from '../../hooks/useWindowSize';
|
||||||
import useSlideSizes from './hooks/useSlideSizes';
|
import useSlideSizes from './hooks/useSlideSizes';
|
||||||
|
|
||||||
import Story from './Story';
|
import Story from './Story';
|
||||||
@ -47,6 +62,13 @@ interface StateProps {
|
|||||||
|
|
||||||
const ANIMATION_DURATION_MS = 350 + (IS_SAFARI || IS_FIREFOX ? ANIMATION_END_DELAY : 20);
|
const ANIMATION_DURATION_MS = 350 + (IS_SAFARI || IS_FIREFOX ? ANIMATION_END_DELAY : 20);
|
||||||
const ACTIVE_SLIDE_VERTICAL_CORRECTION_REM = 1.75;
|
const ACTIVE_SLIDE_VERTICAL_CORRECTION_REM = 1.75;
|
||||||
|
const SWIPE_Y_THRESHOLD = 50;
|
||||||
|
const SCROLL_RELEASE_DELAY = 1500;
|
||||||
|
|
||||||
|
enum SwipeDirection {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
function StorySlides({
|
function StorySlides({
|
||||||
peerIds,
|
peerIds,
|
||||||
@ -65,6 +87,8 @@ function StorySlides({
|
|||||||
onReport,
|
onReport,
|
||||||
}: OwnProps & StateProps) {
|
}: OwnProps & StateProps) {
|
||||||
const { stopActiveReaction } = getActions();
|
const { stopActiveReaction } = getActions();
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [renderingPeerId, setRenderingPeerId] = useState(currentPeerId);
|
const [renderingPeerId, setRenderingPeerId] = useState(currentPeerId);
|
||||||
const [renderingStoryId, setRenderingStoryId] = useState(currentStoryId);
|
const [renderingStoryId, setRenderingStoryId] = useState(currentStoryId);
|
||||||
const prevPeerId = usePrevious(currentPeerId);
|
const prevPeerId = usePrevious(currentPeerId);
|
||||||
@ -73,6 +97,11 @@ function StorySlides({
|
|||||||
const renderingIsSinglePeer = useCurrentOrPrev(isSinglePeer, true);
|
const renderingIsSinglePeer = useCurrentOrPrev(isSinglePeer, true);
|
||||||
const renderingIsSingleStory = useCurrentOrPrev(isSingleStory, true);
|
const renderingIsSingleStory = useCurrentOrPrev(isSingleStory, true);
|
||||||
const slideSizes = useSlideSizes();
|
const slideSizes = useSlideSizes();
|
||||||
|
const { height: windowHeight, width: windowWidth } = useWindowSize();
|
||||||
|
const swipeDirectionRef = useRef<SwipeDirection | undefined>(undefined);
|
||||||
|
const isReleasedRef = useRef(false);
|
||||||
|
const { isMobile } = useAppLayout();
|
||||||
|
const animationDuration = isMobile ? 0 : ANIMATION_DURATION_MS;
|
||||||
|
|
||||||
const rendersRef = useRef<Record<string, { current: HTMLDivElement | null }>>({});
|
const rendersRef = useRef<Record<string, { current: HTMLDivElement | null }>>({});
|
||||||
const [getIsAnimating, setIsAnimating] = useSignal(false);
|
const [getIsAnimating, setIsAnimating] = useSignal(false);
|
||||||
@ -128,12 +157,12 @@ function StorySlides({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeoutId = window.setTimeout(() => {
|
const timeoutId = window.setTimeout(() => {
|
||||||
setRenderingPeerId(currentPeerId);
|
setRenderingPeerId(currentPeerId);
|
||||||
}, ANIMATION_DURATION_MS);
|
}, animationDuration);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId);
|
||||||
};
|
};
|
||||||
}, [currentPeerId]);
|
}, [animationDuration, currentPeerId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeOutId: number | undefined;
|
let timeOutId: number | undefined;
|
||||||
@ -141,7 +170,7 @@ function StorySlides({
|
|||||||
if (renderingPeerId !== currentPeerId) {
|
if (renderingPeerId !== currentPeerId) {
|
||||||
timeOutId = window.setTimeout(() => {
|
timeOutId = window.setTimeout(() => {
|
||||||
setRenderingStoryId(currentStoryId);
|
setRenderingStoryId(currentStoryId);
|
||||||
}, ANIMATION_DURATION_MS);
|
}, animationDuration);
|
||||||
} else if (currentStoryId !== renderingStoryId) {
|
} else if (currentStoryId !== renderingStoryId) {
|
||||||
setRenderingStoryId(currentStoryId);
|
setRenderingStoryId(currentStoryId);
|
||||||
}
|
}
|
||||||
@ -149,7 +178,7 @@ function StorySlides({
|
|||||||
return () => {
|
return () => {
|
||||||
window.clearTimeout(timeOutId);
|
window.clearTimeout(timeOutId);
|
||||||
};
|
};
|
||||||
}, [renderingPeerId, currentStoryId, currentPeerId, renderingStoryId]);
|
}, [renderingPeerId, currentStoryId, currentPeerId, renderingStoryId, animationDuration]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeOutId: number | undefined;
|
let timeOutId: number | undefined;
|
||||||
@ -158,14 +187,14 @@ function StorySlides({
|
|||||||
setIsAnimating(true);
|
setIsAnimating(true);
|
||||||
timeOutId = window.setTimeout(() => {
|
timeOutId = window.setTimeout(() => {
|
||||||
setIsAnimating(false);
|
setIsAnimating(false);
|
||||||
}, ANIMATION_DURATION_MS);
|
}, animationDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setIsAnimating(false);
|
setIsAnimating(false);
|
||||||
window.clearTimeout(timeOutId);
|
window.clearTimeout(timeOutId);
|
||||||
};
|
};
|
||||||
}, [prevPeerId, currentPeerId, setIsAnimating]);
|
}, [prevPeerId, currentPeerId, setIsAnimating, animationDuration]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -210,6 +239,91 @@ function StorySlides({
|
|||||||
}, {});
|
}, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || !isOpen) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offsetY = 0;
|
||||||
|
|
||||||
|
const getCurrentStoryRef = () => {
|
||||||
|
return renderingPeerId ? rendersRef.current[renderingPeerId]?.current : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRelease = (event: MouseEvent | TouchEvent | WheelEvent) => {
|
||||||
|
// This allows to prevent onRelease triggered by debounced wheel event
|
||||||
|
// after onRelease was triggered manually in onDrag
|
||||||
|
if (isReleasedRef.current) {
|
||||||
|
isReleasedRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const current = getCurrentStoryRef();
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
|
if (offsetY < -SWIPE_Y_THRESHOLD) {
|
||||||
|
const composer = document.getElementById(EDITABLE_STORY_INPUT_ID);
|
||||||
|
if (composer) {
|
||||||
|
requestMutation(() => {
|
||||||
|
focusEditableElement(composer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offsetY > SWIPE_Y_THRESHOLD) {
|
||||||
|
onClose();
|
||||||
|
if (event.type === 'wheel') {
|
||||||
|
disableScrolling();
|
||||||
|
setTimeout(enableScrolling, SCROLL_RELEASE_DELAY);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestMutation(() => {
|
||||||
|
current.style.setProperty('--slide-translate-y', '0px');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return captureEvents(containerRef.current, {
|
||||||
|
isNotPassive: true,
|
||||||
|
withNativeDrag: true,
|
||||||
|
excludedClosestSelector: '.Composer',
|
||||||
|
onDrag: (event, captureEvent, {
|
||||||
|
dragOffsetX, dragOffsetY,
|
||||||
|
}) => {
|
||||||
|
if (isReleasedRef.current) return;
|
||||||
|
// Avoid conflicts with swipe-to-back gestures
|
||||||
|
if (IS_IOS && captureEvent.type === 'touchstart') {
|
||||||
|
const { pageX } = (captureEvent as RealTouchEvent).touches[0];
|
||||||
|
if (pageX <= IOS_SCREEN_EDGE_THRESHOLD || pageX >= windowWidth - IOS_SCREEN_EDGE_THRESHOLD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.type === 'mousemove') return;
|
||||||
|
const absOffsetX = Math.abs(dragOffsetX);
|
||||||
|
const absOffsetY = Math.abs(dragOffsetY);
|
||||||
|
const current = getCurrentStoryRef();
|
||||||
|
if (!current) return;
|
||||||
|
// If vertical shift is dominant we change only vertical position
|
||||||
|
if (swipeDirectionRef.current === SwipeDirection.Vertical
|
||||||
|
|| Math.abs(absOffsetY) > SWIPE_DIRECTION_THRESHOLD || absOffsetY / absOffsetX > SWIPE_DIRECTION_TOLERANCE) {
|
||||||
|
swipeDirectionRef.current = SwipeDirection.Vertical;
|
||||||
|
const limit = windowHeight;
|
||||||
|
offsetY = clamp(dragOffsetY, -limit, limit);
|
||||||
|
if (offsetY > 0) {
|
||||||
|
requestMutation(() => {
|
||||||
|
current.style.setProperty('--slide-translate-y', `${-offsetY}px`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (event.type === 'wheel' && Math.abs(offsetY) > SWIPE_Y_THRESHOLD * 2) {
|
||||||
|
onRelease(event);
|
||||||
|
isReleasedRef.current = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRelease,
|
||||||
|
});
|
||||||
|
}, [isOpen, renderingPeerId, onClose, windowWidth, windowHeight]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const transformX = calculateTransformX();
|
const transformX = calculateTransformX();
|
||||||
|
|
||||||
@ -225,25 +339,33 @@ function StorySlides({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scale = String(currentPeerId === peerId ? slideSizes.toActiveScale
|
const getScale = () => {
|
||||||
: peerId === renderingPeerId ? slideSizes.fromActiveScale : 1);
|
if (isMobile) return '1';
|
||||||
|
if (currentPeerId === peerId) {
|
||||||
|
return String(slideSizes.toActiveScale);
|
||||||
|
}
|
||||||
|
if (peerId === renderingPeerId) {
|
||||||
|
return String(slideSizes.fromActiveScale);
|
||||||
|
}
|
||||||
|
return '1';
|
||||||
|
};
|
||||||
|
|
||||||
let offsetY = 0;
|
let offsetY = 0;
|
||||||
if (peerId === renderingPeerId) {
|
if (peerId === renderingPeerId) {
|
||||||
offsetY = -ACTIVE_SLIDE_VERTICAL_CORRECTION_REM * slideSizes.fromActiveScale;
|
if (!isMobile) offsetY = -ACTIVE_SLIDE_VERTICAL_CORRECTION_REM * slideSizes.fromActiveScale;
|
||||||
current.classList.add(styles.slideAnimationFromActive);
|
current.classList.add(styles.slideAnimationFromActive);
|
||||||
}
|
}
|
||||||
if (peerId === currentPeerId) {
|
if (peerId === currentPeerId) {
|
||||||
offsetY = ACTIVE_SLIDE_VERTICAL_CORRECTION_REM;
|
if (!isMobile) offsetY = ACTIVE_SLIDE_VERTICAL_CORRECTION_REM;
|
||||||
current.classList.add(styles.slideAnimationToActive);
|
current.classList.add(styles.slideAnimationToActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
current.classList.add(styles.slideAnimation);
|
current.classList.add(styles.slideAnimation);
|
||||||
current.style.setProperty('--slide-translate-x', `${transformX[peerId] || 0}px`);
|
current.style.setProperty('--slide-translate-x', `${transformX[peerId] || 0}px`);
|
||||||
current.style.setProperty('--slide-translate-y', `${offsetY}rem`);
|
current.style.setProperty('--slide-translate-y', `${offsetY}rem`);
|
||||||
current.style.setProperty('--slide-translate-scale', scale);
|
current.style.setProperty('--slide-translate-scale', getScale());
|
||||||
});
|
});
|
||||||
}, [currentPeerId, getIsAnimating, renderingPeerId, slideSizes]);
|
}, [currentPeerId, getIsAnimating, renderingPeerId, isMobile, slideSizes]);
|
||||||
|
|
||||||
function renderStoryPreview(peerId: string, index: number, position: number) {
|
function renderStoryPreview(peerId: string, index: number, position: number) {
|
||||||
const style = buildStyle(
|
const style = buildStyle(
|
||||||
@ -303,7 +425,11 @@ function StorySlides({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} style={`--story-viewer-scale: ${slideSizes.scale}`}>
|
<div
|
||||||
|
className={styles.wrapper}
|
||||||
|
ref={containerRef}
|
||||||
|
style={`--story-viewer-scale: ${slideSizes.scale}`}
|
||||||
|
>
|
||||||
<div className={styles.fullSize} onClick={onClose} />
|
<div className={styles.fullSize} onClick={onClose} />
|
||||||
{renderingPeerIds.length > 1 && (
|
{renderingPeerIds.length > 1 && (
|
||||||
<div className={styles.backdropNonInteractive} style={`height: ${slideSizes.slide.height}px`} />
|
<div className={styles.backdropNonInteractive} style={`height: ${slideSizes.slide.height}px`} />
|
||||||
|
|||||||
@ -98,7 +98,7 @@
|
|||||||
border-top: 0.0625rem solid var(--color-borders);
|
border-top: 0.0625rem solid var(--color-borders);
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.closeButton {
|
||||||
margin-block: 0.25rem;
|
margin-block: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -67,9 +67,8 @@
|
|||||||
right: 1rem;
|
right: 1rem;
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
top: 1.25rem;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,35 +93,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.slideAnimationToActive {
|
.slideAnimationToActive {
|
||||||
@media (min-width: 600.001px) {
|
--border-radius-default-small: 0.25rem;
|
||||||
--border-radius-default-small: 0.25rem;
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 4.5rem;
|
height: 4.5rem;
|
||||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
|
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slideAnimationFromActive {
|
.slideAnimationFromActive {
|
||||||
@media (min-width: 600.001px) {
|
.composer,
|
||||||
.composer,
|
.caption,
|
||||||
.caption,
|
.storyIndicators {
|
||||||
.storyIndicators {
|
transition: opacity 250ms ease-in-out;
|
||||||
transition: opacity 250ms ease-in-out;
|
opacity: 0;
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,7 +191,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transform: none;
|
transform: translate3d(0, calc(var(--slide-translate-y, 0px) * -1), 0);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
display: none;
|
display: none;
|
||||||
@ -243,9 +238,9 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
width: 100% !important;
|
width: calc(100% - 1rem) !important; // Update `MOBILE_MEDIA_OFFSET_X` in MediaAreaOverlay on change
|
||||||
height: calc(100% - 4rem) !important; // Update `MOBILE_MEDIA_BOTTOM_MARGIN` in MediaAreaOverlay on change
|
height: calc(100% - 4.5rem) !important; // Update `MOBILE_MEDIA_OFFSET_Y` in MediaAreaOverlay on change
|
||||||
border-radius: 0;
|
margin: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(body.ghost-animating) & {
|
:global(body.ghost-animating) & {
|
||||||
@ -267,8 +262,6 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 0;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(body.ghost-animating) .activeSlide & {
|
:global(body.ghost-animating) .activeSlide & {
|
||||||
@ -276,18 +269,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mediaAreaOverlay {
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
right: auto;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@ -348,6 +329,13 @@
|
|||||||
.storyHeader {
|
.storyHeader {
|
||||||
@include story-header;
|
@include story-header;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
width: auto;
|
||||||
|
left: 0.5rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
:global(body.ghost-animating) & {
|
:global(body.ghost-animating) & {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
@ -425,8 +413,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.topIcon {
|
|
||||||
color: var(--color-white);
|
.closeButton {
|
||||||
|
display: none;
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@ -434,12 +426,21 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
:global(.Button) {
|
||||||
position: relative;
|
color: white;
|
||||||
right: 3.25rem;
|
@media (max-width: 600px) {
|
||||||
|
display: flex;
|
||||||
|
&:active,
|
||||||
|
&.active,
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.visibilityButton {
|
.visibilityButton {
|
||||||
min-width: 1.5rem;
|
min-width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
@ -451,6 +452,7 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
margin: 0 0.375rem;
|
||||||
|
|
||||||
> :global(.icon + .icon) {
|
> :global(.icon + .icon) {
|
||||||
margin-left: 0.125rem;
|
margin-left: 0.125rem;
|
||||||
@ -490,7 +492,9 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 7rem;
|
height: 7rem;
|
||||||
|
overflow: hidden;
|
||||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, transparent 100%);
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, transparent 100%);
|
||||||
|
border-radius: 0 0 var(--border-radius-default-small) var(--border-radius-default-small);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -590,6 +594,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
transition: transform var(--layer-transition);
|
||||||
|
|
||||||
&:global(.Composer) {
|
&:global(.Composer) {
|
||||||
--base-height: 3rem;
|
--base-height: 3rem;
|
||||||
@ -633,6 +638,10 @@
|
|||||||
|
|
||||||
max-height: 8rem;
|
max-height: 8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.is-symbol-menu-open) & {
|
||||||
|
transform: translate3d(0, calc(-1 * (var(--symbol-menu-height))), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigate {
|
.navigate {
|
||||||
@ -673,6 +682,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
border-radius: var(--border-radius-default-small);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ghost {
|
.ghost {
|
||||||
@ -702,7 +715,4 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
@media (max-width: 600px) {
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -155,7 +155,7 @@ function StoryViewer({
|
|||||||
ariaLabel={lang('Close')}
|
ariaLabel={lang('Close')}
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
<i className={buildClassName('icon icon-close', styles.topIcon)} aria-hidden />
|
<i className={buildClassName('icon icon-close')} aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<StorySlides
|
<StorySlides
|
||||||
|
|||||||
@ -15,8 +15,8 @@ import storyRibbonStyles from '../StoryRibbon.module.scss';
|
|||||||
import styles from '../StoryViewer.module.scss';
|
import styles from '../StoryViewer.module.scss';
|
||||||
|
|
||||||
const ANIMATION_DURATION = 200;
|
const ANIMATION_DURATION = 200;
|
||||||
const OFFSET_DESKTOP = 3.5 * REM;
|
const OFFSET_BOTTOM = 3.5 * REM;
|
||||||
const OFFSET_MOBILE = 4 * REM;
|
const MOBILE_OFFSET = 0.5 * REM;
|
||||||
const MOBILE_WIDTH = 600;
|
const MOBILE_WIDTH = 600;
|
||||||
|
|
||||||
export function animateOpening(
|
export function animateOpening(
|
||||||
@ -36,12 +36,12 @@ export function animateOpening(
|
|||||||
const isMobile = windowWidth <= MOBILE_WIDTH;
|
const isMobile = windowWidth <= MOBILE_WIDTH;
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
toWidth = windowWidth;
|
toWidth = windowWidth - 2 * MOBILE_OFFSET;
|
||||||
toHeight = windowHeight - OFFSET_MOBILE;
|
toHeight = windowHeight - OFFSET_BOTTOM - 2 * MOBILE_OFFSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toLeft = isMobile ? 0 : (windowWidth - toWidth) / 2;
|
const toLeft = isMobile ? MOBILE_OFFSET : (windowWidth - toWidth) / 2;
|
||||||
const toTop = isMobile ? 0 : (windowHeight - (toHeight + OFFSET_DESKTOP)) / 2;
|
const toTop = isMobile ? MOBILE_OFFSET : (windowHeight - (toHeight + OFFSET_BOTTOM)) / 2;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
top: fromTop, left: fromLeft, width: fromWidth, height: fromHeight,
|
top: fromTop, left: fromLeft, width: fromWidth, height: fromHeight,
|
||||||
|
|||||||
@ -332,11 +332,11 @@ function createGhost(sourceEl: HTMLElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPeerId(el: HTMLElement) {
|
function getPeerId(el: HTMLElement) {
|
||||||
return el.getAttribute('data-peer-id');
|
return el?.getAttribute('data-peer-id');
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectByPeerId(el: HTMLElement, id: string) {
|
function selectByPeerId(el: HTMLElement, id: string) {
|
||||||
return el.querySelector<HTMLElement>(`[data-peer-id="${id}"]`);
|
return el?.querySelector<HTMLElement>(`[data-peer-id="${id}"]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDelayedCallback(callback: NoneToVoidFunction, ms: number) {
|
function createDelayedCallback(callback: NoneToVoidFunction, ms: number) {
|
||||||
|
|||||||
@ -5,8 +5,16 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: var(--media-width, auto);
|
width: var(--media-width, auto);
|
||||||
|
height: var(--media-height, auto);
|
||||||
aspect-ratio: 9 / 16;
|
aspect-ratio: 9 / 16;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
right: auto;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mediaArea {
|
.mediaArea {
|
||||||
@ -22,6 +30,7 @@
|
|||||||
|
|
||||||
.shiny {
|
.shiny {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius-default-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shiny::before {
|
.shiny::before {
|
||||||
|
|||||||
@ -22,7 +22,8 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const STORY_ASPECT_RATIO = 9 / 16;
|
const STORY_ASPECT_RATIO = 9 / 16;
|
||||||
const MOBILE_MEDIA_BOTTOM_MARGIN = 4 * REM;
|
const MOBILE_MEDIA_OFFSET_X = Number(REM);
|
||||||
|
const MOBILE_MEDIA_OFFSET_Y = 4.5 * REM;
|
||||||
|
|
||||||
const MediaAreaOverlay = ({
|
const MediaAreaOverlay = ({
|
||||||
story, isActive, className,
|
story, isActive, className,
|
||||||
@ -41,17 +42,22 @@ const MediaAreaOverlay = ({
|
|||||||
if (windowSize.width > MOBILE_SCREEN_MAX_WIDTH) {
|
if (windowSize.width > MOBILE_SCREEN_MAX_WIDTH) {
|
||||||
requestMutation(() => {
|
requestMutation(() => {
|
||||||
element.style.removeProperty('--media-width');
|
element.style.removeProperty('--media-width');
|
||||||
|
element.style.removeProperty('--media-height');
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const adaptedHeight = windowSize.height - MOBILE_MEDIA_BOTTOM_MARGIN;
|
const adaptedHeight = windowSize.height - MOBILE_MEDIA_OFFSET_Y;
|
||||||
|
const adoptedWidth = windowSize.width - MOBILE_MEDIA_OFFSET_X;
|
||||||
|
|
||||||
const screenAspectRatio = windowSize.width / adaptedHeight;
|
const screenAspectRatio = windowSize.width / windowSize.height;
|
||||||
|
|
||||||
|
const width = screenAspectRatio < STORY_ASPECT_RATIO ? adaptedHeight * STORY_ASPECT_RATIO : adoptedWidth;
|
||||||
|
const height = screenAspectRatio < STORY_ASPECT_RATIO ? adaptedHeight : adoptedWidth / STORY_ASPECT_RATIO;
|
||||||
|
|
||||||
const width = screenAspectRatio > STORY_ASPECT_RATIO ? adaptedHeight * STORY_ASPECT_RATIO : windowSize.width;
|
|
||||||
requestMutation(() => {
|
requestMutation(() => {
|
||||||
element.style.setProperty('--media-width', `${width}px`);
|
element.style.setProperty('--media-width', `${width}px`);
|
||||||
|
element.style.setProperty('--media-height', `${height}px`);
|
||||||
});
|
});
|
||||||
}, [isActive, windowSize]);
|
}, [isActive, windowSize]);
|
||||||
|
|
||||||
|
|||||||
@ -273,7 +273,7 @@ $color-message-story-mention-to: #74bcff;
|
|||||||
--z-sticky-date: 9;
|
--z-sticky-date: 9;
|
||||||
--z-register-add-avatar: 5;
|
--z-register-add-avatar: 5;
|
||||||
--z-media-viewer-head: 3;
|
--z-media-viewer-head: 3;
|
||||||
--z-symbol-menu-mobile: 2;
|
--z-symbol-menu-mobile: calc(var(--z-story-viewer) + 1);
|
||||||
--z-resize-handle: 2;
|
--z-resize-handle: 2;
|
||||||
--z-below: -1;
|
--z-below: -1;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Lethargy } from './lethargy';
|
import { Lethargy } from './lethargy';
|
||||||
import { clamp, round } from './math';
|
import { clamp, round } from './math';
|
||||||
import { debounce } from './schedulers';
|
import { debounce } from './schedulers';
|
||||||
import { IS_IOS } from './windowEnvironment';
|
import { IS_IOS, IS_WINDOWS } from './windowEnvironment';
|
||||||
import windowSize from './windowSize';
|
import windowSize from './windowSize';
|
||||||
|
|
||||||
export enum SwipeDirection {
|
export enum SwipeDirection {
|
||||||
@ -68,6 +68,9 @@ type TSwipeAxis =
|
|||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
export const IOS_SCREEN_EDGE_THRESHOLD = 20;
|
export const IOS_SCREEN_EDGE_THRESHOLD = 20;
|
||||||
|
export const SWIPE_DIRECTION_THRESHOLD = 10;
|
||||||
|
export const SWIPE_DIRECTION_TOLERANCE = 1.5;
|
||||||
|
|
||||||
const MOVED_THRESHOLD = 15;
|
const MOVED_THRESHOLD = 15;
|
||||||
const SWIPE_THRESHOLD = 50;
|
const SWIPE_THRESHOLD = 50;
|
||||||
const RELEASE_WHEEL_ZOOM_DELAY = 150;
|
const RELEASE_WHEEL_ZOOM_DELAY = 150;
|
||||||
@ -89,7 +92,7 @@ let lastClickTime = 0;
|
|||||||
const lethargy = new Lethargy({
|
const lethargy = new Lethargy({
|
||||||
stability: 5,
|
stability: 5,
|
||||||
sensitivity: 25,
|
sensitivity: 25,
|
||||||
tolerance: 0.6,
|
tolerance: IS_WINDOWS ? 1 : 0.6, // Windows scrollDelta does not die down to 0
|
||||||
delay: 150,
|
delay: 150,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export class Lethargy {
|
|||||||
const newSum = lastDeltasNew.reduce((t, s) => t + s);
|
const newSum = lastDeltasNew.reduce((t, s) => t + s);
|
||||||
const oldAverage = oldSum / lastDeltasOld.length;
|
const oldAverage = oldSum / lastDeltasOld.length;
|
||||||
const newAverage = newSum / lastDeltasNew.length;
|
const newAverage = newSum / lastDeltasNew.length;
|
||||||
return Math.abs(oldAverage) < Math.abs(newAverage * this.tolerance)
|
return Math.abs(oldAverage) <= Math.abs(newAverage * this.tolerance)
|
||||||
&& this.sensitivity < Math.abs(newAverage);
|
&& this.sensitivity < Math.abs(newAverage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user