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