Dynamic Resize (#2291)

This commit is contained in:
Alexander Zinchuk 2023-01-24 00:21:55 +01:00
parent 9596bb1695
commit 3dab7609e3
79 changed files with 607 additions and 493 deletions

View File

@ -21,6 +21,7 @@ import AppInactive from './components/main/AppInactive';
import Transition from './components/ui/Transition';
import UiLoader from './components/common/UiLoader';
import { parseInitialLocationHash } from './util/routing';
import useAppLayout from './hooks/useAppLayout';
// import Test from './components/test/TestNoRedundancy';
type StateProps = {
@ -46,7 +47,8 @@ const App: FC<StateProps> = ({
const { disconnect } = getActions();
const [isInactive, markInactive] = useFlag(false);
const isMobile = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
const { isMobile } = useAppLayout();
const isMobileOs = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
useEffect(() => {
updateSizes();
@ -129,7 +131,7 @@ const App: FC<StateProps> = ({
} else if (hasPasscode) {
activeKey = AppScreens.lock;
} else {
page = isMobile ? 'authPhoneNumber' : 'authQrCode';
page = isMobileOs ? 'authPhoneNumber' : 'authQrCode';
activeKey = AppScreens.auth;
}
@ -150,7 +152,7 @@ const App: FC<StateProps> = ({
case AppScreens.auth:
return <Auth isActive={isActive} />;
case AppScreens.main:
return <Main />;
return <Main isMobile={isMobile} />;
case AppScreens.lock:
return <LockScreen isLocked={isScreenLocked} />;
case AppScreens.inactive:
@ -159,7 +161,7 @@ const App: FC<StateProps> = ({
}
return (
<UiLoader key="Loader" page={page}>
<UiLoader key="Loader" page={page} isMobile={isMobile}>
<Transition
name="fade"
activeKey={activeKey}

View File

@ -7,7 +7,6 @@ import type { GlobalState } from '../../global/types';
import '../../global/actions/initial';
import { pick } from '../../util/iteratees';
import { PLATFORM_ENV } from '../../util/environment';
import windowSize from '../../util/windowSize';
import useHistoryBack from '../../hooks/useHistoryBack';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
@ -56,15 +55,6 @@ const Auth: FC<OwnProps & StateProps> = ({
onBack: handleChangeAuthorizationMethod,
});
// Prevent refresh when rotating device
useEffect(() => {
windowSize.disableRefresh();
return () => {
windowSize.enableRefresh();
};
}, []);
// For animation purposes
const renderingAuthState = useCurrentOrPrev(
authState !== 'authorizationStateReady' ? authState : undefined,

View File

@ -17,7 +17,6 @@ import {
IS_ANDROID,
IS_IOS,
IS_REQUEST_FULLSCREEN_SUPPORTED,
IS_SINGLE_COLUMN_LAYOUT,
} from '../../../util/environment';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import buildClassName from '../../../util/buildClassName';
@ -28,6 +27,7 @@ import {
} from '../../../global/selectors/calls';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import Loading from '../../ui/Loading';
import Button from '../../ui/Button';
@ -84,12 +84,13 @@ const GroupCall: FC<OwnProps & StateProps> = ({
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const { isMobile, isLandscape } = useAppLayout();
const [isLeaving, setIsLeaving] = useState(false);
const [isFullscreen, openFullscreen, closeFullscreen] = useFlag();
const [isSidebarOpen, openSidebar, closeSidebar] = useFlag(true);
const hasVideoParticipants = Object.values(participants).some(({ video, presentation }) => video || presentation);
const isLandscape = isFullscreen && !IS_SINGLE_COLUMN_LAYOUT && hasVideoParticipants;
const isLandscapeLayout = isFullscreen && (!isMobile || isLandscape) && hasVideoParticipants;
const [participantMenu, setParticipantMenu] = useState<{
participant: TypeGroupCallParticipant;
@ -254,8 +255,8 @@ const GroupCall: FC<OwnProps & StateProps> = ({
onClose={toggleGroupCallPanel}
className={buildClassName(
'GroupCall',
IS_SINGLE_COLUMN_LAYOUT && 'single-column',
isLandscape && 'landscape',
(isMobile && !isLandscape) && 'single-column',
isLandscapeLayout && 'landscape',
!isSidebarOpen && 'no-sidebar',
)}
dialogRef={containerRef}
@ -274,7 +275,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
<i className={isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen'} />
</Button>
)}
{isLandscape && (
{isLandscapeLayout && (
<Button
round
size="smaller"
@ -321,7 +322,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
<div className="scrollable custom-scroll">
<GroupCallParticipantStreams onDoubleClick={handleStreamsDoubleClick} />
{(!isLandscape || isSidebarOpen)
{(!isLandscapeLayout || isSidebarOpen)
&& <GroupCallParticipantList openParticipantMenu={handleOpenParticipantMenu} />}
</div>

View File

@ -12,7 +12,6 @@ import {
IS_ANDROID,
IS_IOS,
IS_REQUEST_FULLSCREEN_SUPPORTED,
IS_SINGLE_COLUMN_LAYOUT,
} from '../../../util/environment';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import buildClassName from '../../../util/buildClassName';
@ -26,6 +25,7 @@ import {
} from '../../../lib/secret-sauce';
import useInterval from '../../../hooks/useInterval';
import useForceUpdate from '../../../hooks/useForceUpdate';
import useAppLayout from '../../../hooks/useAppLayout';
import Modal from '../../ui/Modal';
import Avatar from '../../common/Avatar';
@ -58,6 +58,7 @@ const PhoneCall: FC<StateProps> = ({
const containerRef = useRef<HTMLDivElement>(null);
const [isFullscreen, openFullscreen, closeFullscreen] = useFlag();
const { isMobile } = useAppLayout();
const toggleFullscreen = useCallback(() => {
if (isFullscreen) {
@ -230,7 +231,7 @@ const PhoneCall: FC<StateProps> = ({
onClose={handleClose}
className={buildClassName(
styles.root,
IS_SINGLE_COLUMN_LAYOUT && styles.singleColumn,
isMobile && styles.singleColumn,
)}
dialogRef={containerRef}
>

View File

@ -8,11 +8,12 @@ import React, {
import { fastRaf } from '../../util/schedulers';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import generateIdFor from '../../util/generateIdFor';
import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck';
import useBackgroundMode from '../../hooks/useBackgroundMode';
import useOnChange from '../../hooks/useOnChange';
import generateIdFor from '../../util/generateIdFor';
import useAppLayout from '../../hooks/useAppLayout';
export type OwnProps = {
ref?: RefObject<HTMLDivElement>;
@ -88,6 +89,7 @@ const AnimatedSticker: FC<OwnProps> = ({
const containerId = useMemo(() => generateIdFor(ID_STORE, true), []);
const { isMobile } = useAppLayout();
const [animation, setAnimation] = useState<RLottieInstance>();
const wasPlaying = useRef(false);
const isFrozen = useRef(false);
@ -138,6 +140,7 @@ const AnimatedSticker: FC<OwnProps> = ({
quality,
isLowPriority,
coords: sharedCanvasCoords,
isMobile,
},
color,
onEnded,
@ -164,7 +167,7 @@ const AnimatedSticker: FC<OwnProps> = ({
}
}, [
animation, animationId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop,
containerId, sharedCanvas, sharedCanvasCoords,
containerId, sharedCanvas, sharedCanvasCoords, isMobile,
]);
useEffect(() => {
@ -232,11 +235,14 @@ const AnimatedSticker: FC<OwnProps> = ({
}
}, [noLoop, animation]);
useOnChange(([prevSharedCanvasCoords]) => {
if (prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords) {
animation?.setSharedCanvasCoords(containerId, sharedCanvasCoords);
useOnChange(([prevSharedCanvasCoords, prevIsMobile]) => {
if (
(prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords)
|| (prevIsMobile !== undefined && isMobile !== prevIsMobile)
) {
animation?.setSharedCanvasCoords(containerId, sharedCanvasCoords, isMobile);
}
}, [sharedCanvasCoords, containerId, animation]);
}, [sharedCanvasCoords, isMobile, containerId, animation]);
useEffect(() => {
if (!animation) {

View File

@ -8,9 +8,11 @@ import type { ApiAudio, ApiMessage, ApiVoice } from '../../api/types';
import { ApiMediaFormat } from '../../api/types';
import type { ISettings } from '../../types';
import { AudioOrigin } from '../../types';
import type { LangFn } from '../../hooks/useLang';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '../../util/dateFormat';
import { MAX_EMPTY_WAVEFORM_POINTS, renderWaveform } from './helpers/waveform';
import renderText from './helpers/renderText';
import { getFileSizeString } from './helpers/documentInfo';
import {
getMediaDuration,
getMediaTransferState,
@ -19,22 +21,20 @@ import {
isMessageLocal,
isOwnMessage,
} from '../../global/helpers';
import { MAX_EMPTY_WAVEFORM_POINTS, renderWaveform } from './helpers/waveform';
import buildClassName from '../../util/buildClassName';
import renderText from './helpers/renderText';
import { getFileSizeString } from './helpers/documentInfo';
import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '../../util/dateFormat';
import { decodeWaveform, interpolateArray } from '../../util/waveform';
import { makeTrackId } from '../../util/audioPlayer';
import { getTranslation } from '../../util/langProvider';
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
import useShowTransition from '../../hooks/useShowTransition';
import type { BufferedRange } from '../../hooks/useBuffering';
import useBuffering from '../../hooks/useBuffering';
import useAudioPlayer from '../../hooks/useAudioPlayer';
import type { LangFn } from '../../hooks/useLang';
import useLang from '../../hooks/useLang';
import { captureEvents } from '../../util/captureEvents';
import useMedia from '../../hooks/useMedia';
import { makeTrackId } from '../../util/audioPlayer';
import { getTranslation } from '../../util/langProvider';
import useAppLayout from '../../hooks/useAppLayout';
import Button from '../ui/Button';
import ProgressSpinner from '../ui/ProgressSpinner';
@ -111,6 +111,7 @@ const Audio: FC<OwnProps> = ({
const lang = useLang();
const { isRtl } = lang;
const { isMobile } = useAppLayout();
const [isActivated, setIsActivated] = useState(false);
const shouldLoad = (isActivated || PRELOAD) && lastSyncTime;
const coverHash = getMessageMediaHash(message, 'pictogram');
@ -159,7 +160,7 @@ const Audio: FC<OwnProps> = ({
const isOwn = isOwnMessage(message);
const waveformCanvasRef = useWaveformCanvas(
theme, voice, (isMediaUnread && !isOwn) ? 1 : playProgress, isOwn, !noAvatars,
theme, voice, (isMediaUnread && !isOwn) ? 1 : playProgress, isOwn, !noAvatars, isMobile,
);
const withSeekline = isPlaying || (playProgress > 0 && playProgress < 1);
@ -345,7 +346,7 @@ const Audio: FC<OwnProps> = ({
)}
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
size="smaller"
color={coverBlobUrl ? 'translucent-white' : 'primary'}
className={buttonClassNames.join(' ')}
@ -414,10 +415,10 @@ const Audio: FC<OwnProps> = ({
);
};
function getSeeklineSpikeAmounts(withAvatar?: boolean) {
function getSeeklineSpikeAmounts(isMobile?: boolean, withAvatar?: boolean) {
return {
MIN_SPIKES: IS_SINGLE_COLUMN_LAYOUT ? (TINY_SCREEN_WIDTH_MQL.matches ? 16 : 20) : 25,
MAX_SPIKES: IS_SINGLE_COLUMN_LAYOUT
MIN_SPIKES: isMobile ? (TINY_SCREEN_WIDTH_MQL.matches ? 16 : 20) : 25,
MAX_SPIKES: isMobile
? (TINY_SCREEN_WIDTH_MQL.matches
? 35
: (withAvatar && WITH_AVATAR_TINY_SCREEN_WIDTH_MQL.matches ? 40 : 45))
@ -540,6 +541,7 @@ function useWaveformCanvas(
playProgress = 0,
isOwn = false,
withAvatar = false,
isMobile = false,
) {
// eslint-disable-next-line no-null/no-null
const canvasRef = useRef<HTMLCanvasElement>(null);
@ -557,13 +559,13 @@ function useWaveformCanvas(
};
}
const { MIN_SPIKES, MAX_SPIKES } = getSeeklineSpikeAmounts(withAvatar);
const { MIN_SPIKES, MAX_SPIKES } = getSeeklineSpikeAmounts(isMobile, withAvatar);
const durationFactor = Math.min(duration / AVG_VOICE_DURATION, 1);
const spikesCount = Math.round(MIN_SPIKES + (MAX_SPIKES - MIN_SPIKES) * durationFactor);
const decodedWaveform = decodeWaveform(new Uint8Array(waveform));
return interpolateArray(decodedWaveform, spikesCount);
}, [voice, withAvatar]) || {};
}, [isMobile, voice, withAvatar]) || {};
useLayoutEffect(() => {
const canvas = canvasRef.current;

View File

@ -2,16 +2,17 @@ import type { RefObject } from 'react';
import type { FC } from '../../lib/teact/teact';
import React, { memo, useRef, useState } from '../../lib/teact/teact';
import { IS_CANVAS_FILTER_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import useShowTransition from '../../hooks/useShowTransition';
import useMediaTransition from '../../hooks/useMediaTransition';
import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/environment';
import buildClassName from '../../util/buildClassName';
import { formatMediaDateTime, formatPastTimeShort } from '../../util/dateFormat';
import { getColorFromExtension, getFileSizeString } from './helpers/documentInfo';
import { getDocumentThumbnailDimensions } from './helpers/mediaDimensions';
import renderText from './helpers/renderText';
import useShowTransition from '../../hooks/useShowTransition';
import useMediaTransition from '../../hooks/useMediaTransition';
import useLang from '../../hooks/useLang';
import useCanvasBlur from '../../hooks/useCanvasBlur';
import useAppLayout from '../../hooks/useAppLayout';
import ProgressSpinner from '../ui/ProgressSpinner';
import Link from '../ui/Link';
@ -66,9 +67,10 @@ const File: FC<OwnProps> = ({
elementRef = ref;
}
const { isMobile } = useAppLayout();
const [withThumb] = useState(!previewData);
const noThumb = Boolean(previewData);
const thumbRef = useCanvasBlur(thumbnailDataUri, noThumb, IS_SINGLE_COLUMN_LAYOUT && !IS_CANVAS_FILTER_SUPPORTED);
const thumbRef = useCanvasBlur(thumbnailDataUri, noThumb, isMobile && !IS_CANVAS_FILTER_SUPPORTED);
const thumbClassNames = useMediaTransition(!noThumb);
const {

View File

@ -5,11 +5,12 @@ import React, {
} from '../../lib/teact/teact';
import { MIN_PASSWORD_LENGTH } from '../../config';
import { IS_TOUCH_ENV, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { IS_TOUCH_ENV } from '../../util/environment';
import buildClassName from '../../util/buildClassName';
import stopEvent from '../../util/stopEvent';
import useLang from '../../hooks/useLang';
import useTimeout from '../../hooks/useTimeout';
import useAppLayout from '../../hooks/useAppLayout';
import Button from '../ui/Button';
@ -31,8 +32,6 @@ type OwnProps = {
onSubmit?: (password: string) => void;
};
const FOCUS_DELAY_TIMEOUT_MS = IS_SINGLE_COLUMN_LAYOUT ? 550 : 400;
const PasswordForm: FC<OwnProps> = ({
isLoading = false,
isPasswordVisible,
@ -54,8 +53,10 @@ const PasswordForm: FC<OwnProps> = ({
const inputRef = useRef<HTMLInputElement>(null);
const lang = useLang();
const { isMobile } = useAppLayout();
const [password, setPassword] = useState('');
const [canSubmit, setCanSubmit] = useState(false);
const focusDelayTimeoutMs = isMobile ? 550 : 400;
useEffect(() => {
if (shouldResetValue) {
@ -67,7 +68,7 @@ const PasswordForm: FC<OwnProps> = ({
if (!IS_TOUCH_ENV) {
inputRef.current!.focus();
}
}, FOCUS_DELAY_TIMEOUT_MS);
}, focusDelayTimeoutMs);
useEffect(() => {
if (error) {

View File

@ -2,13 +2,13 @@ import type { FC } from '../../lib/teact/teact';
import React, { useCallback, memo } from '../../lib/teact/teact';
import { STICKER_SIZE_AUTH, STICKER_SIZE_AUTH_MOBILE, STICKER_SIZE_TWO_FA } from '../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { LOCAL_TGS_URLS } from './helpers/animatedAssets';
import AnimatedSticker from './AnimatedSticker';
import useTimeout from '../../hooks/useTimeout';
import useFlag from '../../hooks/useFlag';
import useAppLayout from '../../hooks/useAppLayout';
import './PasswordMonkey.scss';
@ -21,11 +21,13 @@ const PEEK_MONKEY_SHOW_DELAY = 2000;
const SEGMENT_COVER_EYES: [number, number] = [0, 50];
const SEGMENT_UNCOVER_EYE: [number, number] = [0, 20];
const SEGMENT_COVER_EYE: [number, number] = [20, 0];
const STICKER_SIZE = IS_SINGLE_COLUMN_LAYOUT ? STICKER_SIZE_AUTH_MOBILE : STICKER_SIZE_AUTH;
const PasswordMonkey: FC<OwnProps> = ({ isPasswordVisible, isBig }) => {
const [isFirstMonkeyLoaded, markFirstMonkeyLoaded] = useFlag(false);
const [isPeekShown, markPeekShown] = useFlag(false);
const { isMobile } = useAppLayout();
const stikerSize = isMobile ? STICKER_SIZE_AUTH_MOBILE : STICKER_SIZE_AUTH;
useTimeout(markPeekShown, PEEK_MONKEY_SHOW_DELAY);
const handleFirstMonkeyLoad = useCallback(markFirstMonkeyLoaded, [markFirstMonkeyLoaded]);
@ -36,7 +38,7 @@ const PasswordMonkey: FC<OwnProps> = ({ isPasswordVisible, isBig }) => {
<div className="monkey-preview" />
)}
<AnimatedSticker
size={isBig ? STICKER_SIZE_TWO_FA : STICKER_SIZE}
size={isBig ? STICKER_SIZE_TWO_FA : stikerSize}
className={isPeekShown ? 'hidden' : 'shown'}
tgsUrl={LOCAL_TGS_URLS.MonkeyClose}
playSegment={SEGMENT_COVER_EYES}
@ -44,7 +46,7 @@ const PasswordMonkey: FC<OwnProps> = ({ isPasswordVisible, isBig }) => {
onLoad={handleFirstMonkeyLoad}
/>
<AnimatedSticker
size={isBig ? STICKER_SIZE_TWO_FA : STICKER_SIZE}
size={isBig ? STICKER_SIZE_TWO_FA : stikerSize}
className={isPeekShown ? 'shown' : 'hidden'}
tgsUrl={LOCAL_TGS_URLS.MonkeyPeek}
playSegment={isPasswordVisible ? SEGMENT_UNCOVER_EYE : SEGMENT_COVER_EYE}

View File

@ -3,7 +3,7 @@ import React, { memo, useEffect, useRef } from '../../lib/teact/teact';
import type { FC, TeactNode } from '../../lib/teact/teact';
import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types';
import { IS_CANVAS_FILTER_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/environment';
import {
getChatAvatarHash,
getChatTitle,
@ -21,6 +21,7 @@ import useLang from '../../hooks/useLang';
import useFlag from '../../hooks/useFlag';
import useMediaTransition from '../../hooks/useMediaTransition';
import useCanvasBlur from '../../hooks/useCanvasBlur';
import useAppLayout from '../../hooks/useAppLayout';
import Spinner from '../ui/Spinner';
import OptimizedVideo from '../ui/OptimizedVideo';
@ -50,6 +51,7 @@ const ProfilePhoto: FC<OwnProps> = ({
const videoRef = useRef<HTMLVideoElement>(null);
const lang = useLang();
const { isMobile } = useAppLayout();
const isDeleted = user && isDeletedUser(user);
const isRepliesChat = chat && isChatWithRepliesBot(chat.id);
@ -73,7 +75,7 @@ const ProfilePhoto: FC<OwnProps> = ({
const transitionClassNames = useMediaTransition(isFullMediaReady);
const isBlurredThumb = canHaveMedia && !isFullMediaReady && !avatarBlobUrl && currentPhoto?.thumbnail?.dataUri;
const blurredThumbCanvasRef = useCanvasBlur(
currentPhoto?.thumbnail?.dataUri, !isBlurredThumb, IS_SINGLE_COLUMN_LAYOUT && !IS_CANVAS_FILTER_SUPPORTED,
currentPhoto?.thumbnail?.dataUri, !isBlurredThumb, isMobile && !IS_CANVAS_FILTER_SUPPORTED,
);
const hasMedia = currentPhoto || avatarBlobUrl || isBlurredThumb;

View File

@ -2,9 +2,10 @@ import type { FC } from '../../lib/teact/teact';
import React, { useState, useCallback, memo } from '../../lib/teact/teact';
import { STICKER_SIZE_AUTH, STICKER_SIZE_AUTH_MOBILE, STICKER_SIZE_TWO_FA } from '../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { LOCAL_TGS_URLS } from './helpers/animatedAssets';
import useAppLayout from '../../hooks/useAppLayout';
import AnimatedSticker from './AnimatedSticker';
import './PasswordMonkey.scss';
@ -19,7 +20,6 @@ type OwnProps = {
const TRACKING_START_FRAME = 15;
const TRACKING_END_FRAME = 180;
const STICKER_SIZE = IS_SINGLE_COLUMN_LAYOUT ? STICKER_SIZE_AUTH_MOBILE : STICKER_SIZE_AUTH;
const TrackingMonkey: FC<OwnProps> = ({
code,
@ -29,17 +29,19 @@ const TrackingMonkey: FC<OwnProps> = ({
isBig,
}) => {
const [isFirstMonkeyLoaded, setIsFirstMonkeyLoaded] = useState(false);
const TRACKING_FRAMES_PER_SYMBOL = (TRACKING_END_FRAME - TRACKING_START_FRAME) / codeLength;
const { isMobile } = useAppLayout();
const trackningFramesPerSymbol = (TRACKING_END_FRAME - TRACKING_START_FRAME) / codeLength;
const stickerSize = isMobile ? STICKER_SIZE_AUTH_MOBILE : STICKER_SIZE_AUTH;
const handleFirstMonkeyLoad = useCallback(() => setIsFirstMonkeyLoaded(true), []);
function getTrackingFrames(): [number, number] {
const startFrame = (code && code.length > 1) || trackingDirection < 0
? TRACKING_START_FRAME + TRACKING_FRAMES_PER_SYMBOL * (code.length - 1)
? TRACKING_START_FRAME + trackningFramesPerSymbol * (code.length - 1)
: 0;
const endFrame = code.length === codeLength
? TRACKING_END_FRAME
: TRACKING_START_FRAME + TRACKING_FRAMES_PER_SYMBOL * code.length;
: TRACKING_START_FRAME + trackningFramesPerSymbol * code.length;
if (trackingDirection < 1) {
return [
@ -60,14 +62,14 @@ const TrackingMonkey: FC<OwnProps> = ({
<div className="monkey-preview" />
)}
<AnimatedSticker
size={isBig ? STICKER_SIZE_TWO_FA : STICKER_SIZE}
size={isBig ? STICKER_SIZE_TWO_FA : stickerSize}
className={isTracking ? 'hidden' : undefined}
tgsUrl={LOCAL_TGS_URLS.MonkeyIdle}
play={!isTracking}
onLoad={handleFirstMonkeyLoad}
/>
<AnimatedSticker
size={isBig ? STICKER_SIZE_TWO_FA : STICKER_SIZE}
size={isBig ? STICKER_SIZE_TWO_FA : stickerSize}
className={!isTracking ? 'hidden' : 'shown'}
tgsUrl={LOCAL_TGS_URLS.MonkeyTracking}
playSegment={isTracking ? getTrackingFrames() : undefined}

View File

@ -38,6 +38,7 @@ export type UiLoaderPage =
type OwnProps = {
page?: UiLoaderPage;
children: React.ReactNode;
isMobile?: boolean;
};
type StateProps = Pick<GlobalState, 'uiReadyState' | 'shouldSkipHistoryAnimations'> & {
@ -173,13 +174,13 @@ const UiLoader: FC<OwnProps & StateProps> = ({
};
export default withGlobal<OwnProps>(
(global): StateProps => {
(global, { isMobile }): StateProps => {
const theme = selectTheme(global);
return {
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
uiReadyState: global.uiReadyState,
isRightColumnShown: selectIsRightColumnShown(global),
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
leftColumnWidth: global.leftColumnWidth,
theme,
};

View File

@ -3,7 +3,7 @@ import type {
} from '../../../api/types';
import { STICKER_SIZE_INLINE_DESKTOP_FACTOR, STICKER_SIZE_INLINE_MOBILE_FACTOR } from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment';
import { IS_TOUCH_ENV } from '../../../util/environment';
import windowSize from '../../../util/windowSize';
import { getPhotoInlineDimensions, getVideoDimensions } from '../../../global/helpers';
@ -25,9 +25,9 @@ let cachedMaxWidthOwn: number | undefined;
let cachedMaxWidth: number | undefined;
let cachedMaxWidthNoAvatar: number | undefined;
function getMaxMessageWidthRem(fromOwnMessage: boolean, noAvatars?: boolean) {
function getMaxMessageWidthRem(fromOwnMessage: boolean, noAvatars?: boolean, isMobile?: boolean) {
const regularMaxWidth = fromOwnMessage ? MESSAGE_OWN_MAX_WIDTH_REM : MESSAGE_MAX_WIDTH_REM;
if (!IS_SINGLE_COLUMN_LAYOUT) {
if (!isMobile) {
return regularMaxWidth;
}
@ -63,9 +63,10 @@ export function getAvailableWidth(
asForwarded?: boolean,
isWebPageMedia?: boolean,
noAvatars?: boolean,
isMobile?: boolean,
) {
const extraPaddingRem = asForwarded && isWebPageMedia ? 2.25 : (asForwarded || isWebPageMedia ? 1.625 : 0);
const availableWidthRem = getMaxMessageWidthRem(fromOwnMessage, noAvatars) - extraPaddingRem;
const availableWidthRem = getMaxMessageWidthRem(fromOwnMessage, noAvatars, isMobile) - extraPaddingRem;
return availableWidthRem * REM;
}
@ -89,6 +90,7 @@ export function calculateDimensionsForMessageMedia({
isWebPageMedia,
isGif,
noAvatars,
isMobile,
}: {
width: number;
height: number;
@ -97,9 +99,10 @@ export function calculateDimensionsForMessageMedia({
isWebPageMedia?: boolean;
isGif?: boolean;
noAvatars?: boolean;
isMobile?: boolean;
}): ApiDimensions {
const aspectRatio = height / width;
const availableWidth = getAvailableWidth(fromOwnMessage, asForwarded, isWebPageMedia, noAvatars);
const availableWidth = getAvailableWidth(fromOwnMessage, asForwarded, isWebPageMedia, noAvatars, isMobile);
const availableHeight = getAvailableHeight(isGif, aspectRatio);
const mediaWidth = isGif ? Math.max(GIF_MIN_WIDTH, width) : width;
const mediaHeight = isGif ? height * (mediaWidth / width) : height;
@ -126,6 +129,7 @@ export function calculateInlineImageDimensions(
asForwarded?: boolean,
isWebPageMedia?: boolean,
noAvatars?: boolean,
isMobile?: boolean,
) {
const { width, height } = getPhotoInlineDimensions(photo) || DEFAULT_MEDIA_DIMENSIONS;
@ -136,6 +140,7 @@ export function calculateInlineImageDimensions(
asForwarded,
isWebPageMedia,
noAvatars,
isMobile,
});
}
@ -145,6 +150,7 @@ export function calculateVideoDimensions(
asForwarded?: boolean,
isWebPageMedia?: boolean,
noAvatars?: boolean,
isMobile?: boolean,
) {
const { width, height } = getVideoDimensions(video) || DEFAULT_MEDIA_DIMENSIONS;
@ -156,6 +162,7 @@ export function calculateVideoDimensions(
isWebPageMedia,
isGif: video.isGif,
noAvatars,
isMobile,
});
}
@ -180,7 +187,7 @@ export function getDocumentThumbnailDimensions(smaller?: boolean): ApiDimensions
};
}
export function getStickerDimensions(sticker: ApiSticker): ApiDimensions {
export function getStickerDimensions(sticker: ApiSticker, isMobile?: boolean): ApiDimensions {
const { width } = sticker;
let { height } = sticker;
@ -191,7 +198,7 @@ export function getStickerDimensions(sticker: ApiSticker): ApiDimensions {
const aspectRatio = (height && width) && height / width;
const baseWidth = REM * (
IS_SINGLE_COLUMN_LAYOUT
isMobile
? STICKER_SIZE_INLINE_MOBILE_FACTOR
: STICKER_SIZE_INLINE_DESKTOP_FACTOR
);

View File

@ -17,7 +17,6 @@ import type { AnimationLevel } from '../../../types';
import type { ChatAnimationTypes } from './hooks';
import { MAIN_THREAD_ID } from '../../../api/types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import {
isUserId,
getPrivateChatUserId,
@ -43,6 +42,7 @@ import useChatContextActions from '../../../hooks/useChatContextActions';
import useFlag from '../../../hooks/useFlag';
import useChatListEntry from './hooks/useChatListEntry';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useAppLayout from '../../../hooks/useAppLayout';
import ListItem from '../../ui/ListItem';
import Avatar from '../../common/Avatar';
@ -128,6 +128,7 @@ const Chat: FC<OwnProps & StateProps> = ({
openForumPanel,
} = getActions();
const { isMobile } = useAppLayout();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag();
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
@ -225,7 +226,7 @@ const Chat: FC<OwnProps & StateProps> = ({
ref={ref}
className={className}
style={`top: ${offsetTop}px`}
ripple={!isForum && !IS_SINGLE_COLUMN_LAYOUT}
ripple={!isForum && !isMobile}
contextActions={contextActions}
onClick={handleClick}
onDragEnter={handleDragEnter}
@ -245,7 +246,7 @@ const Chat: FC<OwnProps & StateProps> = ({
/>
<AvatarBadge chatId={chatId} />
{chat.isCallActive && chat.isCallNotEmpty && (
<ChatCallStatus isSelected={isSelected} isActive={animationLevel !== 0} />
<ChatCallStatus isMobile={isMobile} isSelected={isSelected} isActive={animationLevel !== 0} />
)}
</div>
<div className="info">
@ -255,7 +256,7 @@ const Chat: FC<OwnProps & StateProps> = ({
withEmojiStatus
isSavedMessages={chatId === user?.id && user?.isSelf}
observeIntersection={observeIntersection}
key={!IS_SINGLE_COLUMN_LAYOUT && isEmojiStatusColored ? `${isSelected}` : undefined}
key={!isMobile && isEmojiStatusColored ? `${isSelected}` : undefined}
/>
{isMuted && <i className="icon-muted" />}
<div className="separator" />

View File

@ -2,24 +2,24 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import './ChatCallStatus.scss';
type OwnProps = {
isSelected?: boolean;
isActive?: boolean;
isMobile?: boolean;
};
const ChatCallStatus: FC<OwnProps> = ({
isSelected,
isActive,
isMobile,
}) => {
return (
<div className={buildClassName(
'ChatCallStatus',
isActive && 'active',
isSelected && !IS_SINGLE_COLUMN_LAYOUT && 'selected',
isSelected && !isMobile && 'selected',
)}
>
<div className="indicator">

View File

@ -4,11 +4,11 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiUser, ApiUserStatus } from '../../../api/types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { filterUsersByName, sortUserIds } from '../../../global/helpers';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import InfiniteScroll from '../../ui/InfiniteScroll';
@ -42,6 +42,7 @@ const ContactList: FC<OwnProps & StateProps> = ({
} = getActions();
const lang = useLang();
const { isMobile } = useAppLayout();
useHistoryBack({
isActive,
@ -73,7 +74,7 @@ const ContactList: FC<OwnProps & StateProps> = ({
className="chat-item-clickable"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => handleClick(id)}
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
>
<PrivateChatInfo userId={id} forceShowSelf avatarSize="large" />
</ListItem>

View File

@ -6,9 +6,9 @@ import type { ApiChatFolder, ApiSticker } from '../../../api/types';
import { SettingsScreens } from '../../../types';
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { selectAnimatedEmoji, selectChatFolder } from '../../../global/selectors';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import Button from '../../ui/Button';
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
@ -33,6 +33,7 @@ const EmptyFolder: FC<OwnProps & StateProps> = ({
chatFolder, animatedEmoji, foldersDispatch, onScreenSelect,
}) => {
const lang = useLang();
const { isMobile } = useAppLayout();
const handleEditFolder = useCallback(() => {
foldersDispatch!({ type: 'editFolder', payload: chatFolder });
@ -50,7 +51,7 @@ const EmptyFolder: FC<OwnProps & StateProps> = ({
</p>
{chatFolder && foldersDispatch && onScreenSelect && (
<Button
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
fluid
pill
onClick={handleEditFolder}

View File

@ -4,12 +4,12 @@ import { getActions, withGlobal } from '../../../global';
import type { FC } from '../../../lib/teact/teact';
import type { ApiSticker } from '../../../api/types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { REM } from '../../common/helpers/mediaDimensions';
import { selectAnimatedEmoji, selectChat } from '../../../global/selectors';
import { getHasAdminRight } from '../../../global/helpers';
import { REM } from '../../common/helpers/mediaDimensions';
import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import Button from '../../ui/Button';
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
@ -31,7 +31,9 @@ const EmptyForum: FC<OwnProps & StateProps> = ({
chatId, animatedEmoji, canManageTopics,
}) => {
const { openCreateTopicPanel } = getActions();
const lang = useLang();
const { isMobile } = useAppLayout();
const handleCreateTopic = useCallback(() => {
openCreateTopicPanel({ chatId });
@ -48,7 +50,7 @@ const EmptyForum: FC<OwnProps & StateProps> = ({
</p>
{canManageTopics && (
<Button
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
fluid
onClick={handleCreateTopic}
size="smaller"

View File

@ -26,6 +26,7 @@ import useLang from '../../../hooks/useLang';
import usePrevious from '../../../hooks/usePrevious';
import useHistoryBack from '../../../hooks/useHistoryBack';
import { dispatchHeavyAnimationEvent } from '../../../hooks/useHeavyAnimationCheck';
import useAppLayout from '../../../hooks/useAppLayout';
import GroupChatInfo from '../../common/GroupChatInfo';
import Button from '../../ui/Button';
@ -73,6 +74,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const scrollTopHandlerRef = useRef<HTMLDivElement>(null);
const { isMobile } = useAppLayout();
useEffect(() => {
if (lastSyncTime && chat && !chat.topics) {
@ -223,6 +225,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
messageListType="thread"
canExpandActions={false}
withForumActions
isMobile={isMobile}
onTopicSearch={onTopicSearch}
/>
)}

View File

@ -19,7 +19,7 @@ import {
IS_TEST,
PRODUCTION_HOSTNAME,
} from '../../../config';
import { IS_PWA, IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { IS_PWA } from '../../../util/environment';
import buildClassName from '../../../util/buildClassName';
import { formatDateToString } from '../../../util/dateFormat';
import switchTheme from '../../../util/switchTheme';
@ -33,6 +33,7 @@ import { useHotkeys } from '../../../hooks/useHotkeys';
import { getPromptInstall } from '../../../util/installPrompt';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
import useLeftHeaderButtonRtlForumTransition from './hooks/useLeftHeaderButtonRtlForumTransition';
import useAppLayout from '../../../hooks/useAppLayout';
import DropdownMenu from '../../ui/DropdownMenu';
import MenuItem from '../../ui/MenuItem';
@ -119,6 +120,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
} = getActions();
const lang = useLang();
const { isMobile } = useAppLayout();
const hasMenu = content === LeftColumnContent.ChatList;
const clearedDateSearchParam = { date: undefined };
const clearedChatSearchParam = { id: undefined };
@ -168,7 +170,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
return ({ onTrigger, isOpen }) => (
<Button
round
ripple={hasMenu && !IS_SINGLE_COLUMN_LAYOUT}
ripple={hasMenu && !isMobile}
size="smaller"
color="translucent"
className={isOpen ? 'active' : ''}
@ -184,7 +186,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
/>
</Button>
);
}, [hasMenu, lang, onReset, shouldSkipTransition]);
}, [hasMenu, isMobile, lang, onReset, shouldSkipTransition]);
const handleSearchFocus = useCallback(() => {
if (!searchQuery) {
@ -431,7 +433,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
{hasPasscode && (
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
size="smaller"
color="translucent"
ariaLabel={`${lang('ShortcutsController.Others.LockByPasscode')} (Ctrl+Shift+L)`}

View File

@ -6,8 +6,8 @@ import type {
ApiChat, ApiUser, ApiMessage, ApiMessageOutgoingStatus,
} from '../../../api/types';
import type { AnimationLevel } from '../../../types';
import type { LangFn } from '../../../hooks/useLang';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import {
getPrivateChatUserId,
getMessageMediaHash,
@ -23,9 +23,9 @@ import { formatPastTimeShort } from '../../../util/dateFormat';
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
import useMedia from '../../../hooks/useMedia';
import type { LangFn } from '../../../hooks/useLang';
import useLang from '../../../hooks/useLang';
import useSelectWithEnter from '../../../hooks/useSelectWithEnter';
import useAppLayout from '../../../hooks/useAppLayout';
import Avatar from '../../common/Avatar';
import ListItem from '../../ui/ListItem';
@ -59,6 +59,7 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
}) => {
const { focusMessage } = getActions();
const { isMobile } = useAppLayout();
const mediaThumbnail = !getMessageSticker(message) ? getMessageMediaThumbDataUri(message) : undefined;
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'micro'));
const isRoundVideo = Boolean(getMessageRoundVideo(message));
@ -78,7 +79,7 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
return (
<ListItem
className="ChatMessage chat-item-clickable"
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
onClick={handleClick}
buttonRef={buttonRef}
>

View File

@ -8,8 +8,8 @@ import { LoadMoreDirection } from '../../../types';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers';
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import InfiniteScroll from '../../ui/InfiniteScroll';
import NothingFound from '../../common/NothingFound';
@ -54,6 +54,8 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
const { searchMessagesGlobal, openChat } = getActions();
const lang = useLang();
const { isMobile } = useAppLayout();
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
if (lastSyncTime && direction === LoadMoreDirection.Backwards) {
runThrottled(() => {
@ -70,11 +72,11 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
(id: number) => {
openChat({ id: searchChatId, threadId: id, shouldReplaceHistory: true });
if (!IS_SINGLE_COLUMN_LAYOUT) {
if (!isMobile) {
onReset();
}
},
[openChat, searchChatId, onReset],
[openChat, searchChatId, isMobile, onReset],
);
const foundMessages = useMemo(() => {

View File

@ -7,7 +7,6 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiChat, ApiMessage } from '../../../api/types';
import { LoadMoreDirection } from '../../../types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { unique } from '../../../util/iteratees';
import {
sortChatIds,
@ -15,9 +14,10 @@ import {
} from '../../../global/helpers';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers';
import useLang from '../../../hooks/useLang';
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
import useLang from '../../../hooks/useLang';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useAppLayout from '../../../hooks/useAppLayout';
import InfiniteScroll from '../../ui/InfiniteScroll';
import LeftSearchResultChat from './LeftSearchResultChat';
@ -72,6 +72,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
const lang = useLang();
const { isMobile } = useAppLayout();
const [shouldShowMoreLocal, setShouldShowMoreLocal] = useState<boolean>(false);
const [shouldShowMoreGlobal, setShouldShowMoreGlobal] = useState<boolean>(false);
@ -94,11 +95,11 @@ const ChatResults: FC<OwnProps & StateProps> = ({
addRecentlyFoundChatId({ id });
}
if (!IS_SINGLE_COLUMN_LAYOUT) {
if (!isMobile) {
onReset();
}
},
[currentUserId, openChat, addRecentlyFoundChatId, onReset],
[openChat, currentUserId, isMobile, addRecentlyFoundChatId, onReset],
);
const handlePickerItemClick = useCallback((id: string) => {

View File

@ -14,7 +14,7 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
import TabList from '../../ui/TabList';
import Transition from '../../ui/Transition';
import ChatResults from './ChatResults';
import UserChatResults from './ChatMessageResults';
import ChatMessageResults from './ChatMessageResults';
import MediaResults from './MediaResults';
import LinkResults from './LinkResults';
import FileResults from './FileResults';
@ -99,7 +99,7 @@ const LeftSearch: FC<OwnProps & StateProps> = ({
case GlobalSearchContent.ChatList:
if (chatId) {
return (
<UserChatResults
<ChatMessageResults
searchQuery={searchQuery}
dateSearchQuery={dateSearchQuery}
onReset={onReset}

View File

@ -6,9 +6,9 @@ import { getActions } from '../../../global';
import { SettingsScreens } from '../../../types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import useLang from '../../../hooks/useLang';
import useMultiClick from '../../../hooks/useMultiClick';
import useAppLayout from '../../../hooks/useAppLayout';
import DropdownMenu from '../../ui/DropdownMenu';
import MenuItem from '../../ui/MenuItem';
@ -35,6 +35,7 @@ const SettingsHeader: FC<OwnProps> = ({
openDeleteChatFolderModal,
} = getActions();
const { isMobile } = useAppLayout();
const [isSignOutDialogOpen, setIsSignOutDialogOpen] = useState(false);
const handleMultiClick = useMultiClick(5, () => {
@ -64,7 +65,7 @@ const SettingsHeader: FC<OwnProps> = ({
return ({ onTrigger, isOpen }) => (
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
size="smaller"
color="translucent"
className={isOpen ? 'active' : ''}
@ -74,7 +75,7 @@ const SettingsHeader: FC<OwnProps> = ({
<i className="icon-more" />
</Button>
);
}, []);
}, [isMobile]);
const lang = useLang();
@ -240,7 +241,7 @@ const SettingsHeader: FC<OwnProps> = ({
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
size="smaller"
color="translucent"
// eslint-disable-next-line react/jsx-no-bind

View File

@ -6,10 +6,11 @@ import { withGlobal } from '../../../../global';
import type { ApiSticker } from '../../../../api/types';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../../util/environment';
import { IS_TOUCH_ENV } from '../../../../util/environment';
import { selectAnimatedEmoji } from '../../../../global/selectors';
import useLang from '../../../../hooks/useLang';
import useHistoryBack from '../../../../hooks/useHistoryBack';
import useAppLayout from '../../../../hooks/useAppLayout';
import InputText from '../../../ui/InputText';
import Loading from '../../../ui/Loading';
@ -29,7 +30,6 @@ type StateProps = {
codeLength: number;
};
const FOCUS_DELAY_TIMEOUT_MS = IS_SINGLE_COLUMN_LAYOUT ? 550 : 400;
const ICON_SIZE = 160;
const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
@ -44,6 +44,8 @@ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
}) => {
// eslint-disable-next-line no-null/no-null
const inputRef = useRef<HTMLInputElement>(null);
const { isMobile } = useAppLayout();
const focusDelayTimeoutMs = isMobile ? 550 : 400;
const [value, setValue] = useState<string>('');
@ -51,9 +53,9 @@ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
if (!IS_TOUCH_ENV) {
setTimeout(() => {
inputRef.current!.focus();
}, FOCUS_DELAY_TIMEOUT_MS);
}, focusDelayTimeoutMs);
}
}, []);
}, [focusDelayTimeoutMs]);
const lang = useLang();

View File

@ -6,12 +6,13 @@ import { withGlobal } from '../../../../global';
import type { ApiSticker } from '../../../../api/types';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../../util/environment';
import { IS_TOUCH_ENV } from '../../../../util/environment';
import { selectAnimatedEmoji } from '../../../../global/selectors';
import renderText from '../../../common/helpers/renderText';
import useFlag from '../../../../hooks/useFlag';
import useLang from '../../../../hooks/useLang';
import useHistoryBack from '../../../../hooks/useHistoryBack';
import useAppLayout from '../../../../hooks/useAppLayout';
import Button from '../../../ui/Button';
import Modal from '../../../ui/Modal';
@ -35,7 +36,6 @@ type StateProps = {
animatedEmoji: ApiSticker;
};
const FOCUS_DELAY_TIMEOUT_MS = IS_SINGLE_COLUMN_LAYOUT ? 550 : 400;
const ICON_SIZE = 160;
const SettingsTwoFaSkippableForm: FC<OwnProps & StateProps> = ({
@ -52,7 +52,9 @@ const SettingsTwoFaSkippableForm: FC<OwnProps & StateProps> = ({
}) => {
// eslint-disable-next-line no-null/no-null
const inputRef = useRef<HTMLInputElement>(null);
const { isMobile } = useAppLayout();
const focusDelayTimeoutMs = isMobile ? 550 : 400;
const [value, setValue] = useState<string>('');
const [isConfirmShown, markIsConfirmShown, unmarkIsConfirmShown] = useFlag(false);
@ -60,9 +62,9 @@ const SettingsTwoFaSkippableForm: FC<OwnProps & StateProps> = ({
if (!IS_TOUCH_ENV) {
setTimeout(() => {
inputRef.current!.focus();
}, FOCUS_DELAY_TIMEOUT_MS);
}, focusDelayTimeoutMs);
}
}, []);
}, [focusDelayTimeoutMs]);
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (error && clearError) {

View File

@ -4,13 +4,13 @@ import { withGlobal } from '../../global';
import type { GlobalState } from '../../global/types';
import type { FC } from '../../lib/teact/teact';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { pick } from '../../util/iteratees';
import buildStyle from '../../util/buildStyle';
import useWindowSize from '../../hooks/useWindowSize';
import useOnChange from '../../hooks/useOnChange';
import useForceUpdate from '../../hooks/useForceUpdate';
import useAppLayout from '../../hooks/useAppLayout';
import styles from './ConfettiContainer.module.scss';
@ -37,7 +37,6 @@ interface Confetti {
}
const CONFETTI_FADEOUT_TIMEOUT = 10000;
const DEFAULT_CONFETTI_AMOUNT = IS_SINGLE_COLUMN_LAYOUT ? 50 : 100;
const DEFAULT_CONFETTI_SIZE = 10;
const CONFETTI_COLORS = ['#E8BC2C', '#D0049E', '#02CBFE', '#5723FD', '#FE8C27', '#6CB859'];
@ -48,12 +47,14 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
const isRafStartedRef = useRef(false);
const windowSize = useWindowSize();
const forceUpdate = useForceUpdate();
const { isMobile } = useAppLayout();
const defaultConfettiAmount = isMobile ? 50 : 100;
const {
lastConfettiTime, top, width, left, height,
} = confetti || {};
function generateConfetti(w: number, h: number, amount = DEFAULT_CONFETTI_AMOUNT) {
function generateConfetti(w: number, h: number, amount = defaultConfettiAmount) {
for (let i = 0; i < amount; i++) {
const leftSide = i % 2;
const pos = {

View File

@ -8,7 +8,6 @@ import { MAIN_THREAD_ID } from '../../api/types';
import { withGlobal } from '../../global';
import { selectChat } from '../../global/selectors';
import { getCanPostInChat } from '../../global/helpers';
import windowSize from '../../util/windowSize';
import useLang from '../../hooks/useLang';
import useSendMessageAction from '../../hooks/useSendMessageAction';
@ -68,17 +67,6 @@ const GameModal: FC<OwnProps & StateProps> = ({ openedGame, gameTitle, canPost }
return () => window.removeEventListener('message', handleMessage);
}, [handleMessage]);
// Prevent refresh when rotating device
useEffect(() => {
if (!isOpen) return undefined;
windowSize.disableRefresh();
return () => {
windowSize.enableRefresh();
};
}, [isOpen]);
return (
<Modal
className="GameModal"

View File

@ -2,12 +2,15 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact';
import { Bundles } from '../../util/moduleLoader';
import type { OwnProps } from './Main';
import useModuleLoader from '../../hooks/useModuleLoader';
const MainAsync: FC = () => {
const MainAsync: FC<OwnProps> = (props) => {
const Main = useModuleLoader(Bundles.Main, 'Main');
return Main ? <Main /> : undefined;
// eslint-disable-next-line react/jsx-props-no-spreading
return Main ? <Main {...props} /> : undefined;
};
export default memo(MainAsync);

View File

@ -107,6 +107,7 @@
#Main.left-column-open & {
transform: translate3d(0, 0, 0);
z-index: 2;
}
#Main.history-animation-disabled & {

View File

@ -18,6 +18,7 @@ import {
import { IS_ANDROID } from '../../util/environment';
import {
selectChatMessage,
selectCurrentMessageList,
selectIsForwardModalOpen,
selectIsMediaViewerOpen,
selectIsRightColumnShown,
@ -27,8 +28,8 @@ import {
import buildClassName from '../../util/buildClassName';
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
import { processDeepLink } from '../../util/deeplink';
import windowSize from '../../util/windowSize';
import { getAllNotificationsCount } from '../../util/folderManager';
import { parseInitialLocationHash, parseLocationHash } from '../../util/routing';
import { fastRaf } from '../../util/schedulers';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
@ -40,7 +41,7 @@ import useForceUpdate from '../../hooks/useForceUpdate';
import useShowTransition from '../../hooks/useShowTransition';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import useInterval from '../../hooks/useInterval';
import { parseInitialLocationHash, parseLocationHash } from '../../util/routing';
import useAppLayout from '../../hooks/useAppLayout';
import StickerSetModal from '../common/StickerSetModal.async';
import UnreadCount from '../common/UnreadCounter';
@ -78,10 +79,15 @@ import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
import './Main.scss';
export interface OwnProps {
isMobile?: boolean;
}
type StateProps = {
chat?: ApiChat;
lastSyncTime?: number;
isLeftColumnOpen: boolean;
isMiddleColumnOpen: boolean;
isRightColumnOpen: boolean;
isMediaViewerOpen: boolean;
isForwardModalOpen: boolean;
@ -129,9 +135,11 @@ let notificationInterval: number | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
let DEBUG_isLogged = false;
const Main: FC<StateProps> = ({
const Main: FC<OwnProps & StateProps> = ({
lastSyncTime,
isMobile,
isLeftColumnOpen,
isMiddleColumnOpen,
isRightColumnOpen,
isMediaViewerOpen,
isForwardModalOpen,
@ -198,6 +206,7 @@ const Main: FC<StateProps> = ({
clearReceipt,
checkAppVersion,
openChat,
toggleLeftColumn,
} = getActions();
if (DEBUG && !DEBUG_isLogged) {
@ -206,6 +215,15 @@ const Main: FC<StateProps> = ({
console.log('>>> RENDER MAIN');
}
// If you open the chat in the mobile version, switch to the desktop version, close the chat and
// switch back to the mobile version, you get a blank screen
const { isDesktop } = useAppLayout();
useEffect(() => {
if (!isMiddleColumnOpen && !isLeftColumnOpen && !isDesktop) {
toggleLeftColumn();
}
}, [isDesktop, isLeftColumnOpen, isMiddleColumnOpen, toggleLeftColumn]);
useInterval(checkAppVersion, APP_OUTDATED_TIMEOUT_MS, true);
// Initial API calls
@ -303,19 +321,6 @@ const Main: FC<StateProps> = ({
}
}, [lastSyncTime] as const);
// Prevent refresh by accidentally rotating device when listening to a voice chat
useEffect(() => {
if (!activeGroupCallId && !isPhoneCallActive) {
return undefined;
}
windowSize.disableRefresh();
return () => {
windowSize.enableRefresh();
};
}, [activeGroupCallId, isPhoneCallActive]);
const leftColumnTransition = useShowTransition(
isLeftColumnOpen, undefined, true, undefined, shouldSkipHistoryAnimations,
);
@ -442,8 +447,8 @@ const Main: FC<StateProps> = ({
return (
<div id="Main" className={className}>
<LeftColumn />
<MiddleColumn />
<RightColumn />
<MiddleColumn isMobile={isMobile} />
<RightColumn isMobile={isMobile} />
<MediaViewer isOpen={isMediaViewerOpen} />
<ForwardRecipientPicker isOpen={isForwardModalOpen} />
<DraftRecipientPicker requestedDraft={requestedDraft} />
@ -510,8 +515,8 @@ function updatePageTitle(nextTitle: string) {
}
}
export default memo(withGlobal(
(global): StateProps => {
export default memo(withGlobal<OwnProps>(
(global, { isMobile }): StateProps => {
const {
settings: {
byKey: {
@ -538,11 +543,13 @@ export default memo(withGlobal(
const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId);
const gameTitle = gameMessage?.content.game?.title;
const currentUser = global.currentUserId ? selectUser(global, global.currentUserId) : undefined;
const { chatId } = selectCurrentMessageList(global) || {};
return {
lastSyncTime,
isLeftColumnOpen: global.isLeftColumnShown,
isRightColumnOpen: selectIsRightColumnShown(global),
isMiddleColumnOpen: Boolean(chatId),
isRightColumnOpen: selectIsRightColumnShown(global, isMobile),
isMediaViewerOpen: selectIsMediaViewerOpen(global),
isForwardModalOpen: selectIsForwardModalOpen(global),
hasNotifications: Boolean(global.notifications.length),

View File

@ -9,8 +9,6 @@ import type { GlobalState } from '../../global/types';
import type { ThemeKey } from '../../types';
import type { PopupOptions, WebAppInboundEvent } from './hooks/useWebAppFrame';
import windowSize from '../../util/windowSize';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { TME_LINK_PREFIX } from '../../config';
import { selectCurrentChat, selectTheme, selectUser } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
@ -22,6 +20,7 @@ import useOnChange from '../../hooks/useOnChange';
import useWebAppFrame from './hooks/useWebAppFrame';
import usePrevious from '../../hooks/usePrevious';
import useFlag from '../../hooks/useFlag';
import useAppLayout from '../../hooks/useAppLayout';
import Modal from '../ui/Modal';
import Button from '../ui/Button';
@ -101,6 +100,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
const [confirmClose, setConfirmClose] = useState(false);
const [isCloseModalOpen, openCloseModal, closeCloseModal] = useFlag(false);
const [popupParams, setPopupParams] = useState<PopupOptions | undefined>();
const { isMobile } = useAppLayout();
const prevPopupParams = usePrevious(popupParams);
const renderingPopupParams = popupParams || prevPopupParams;
@ -268,16 +268,6 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
}, ANIMATION_WAIT);
}, [theme, sendTheme]);
// Prevent refresh when rotating device
useEffect(() => {
if (!isOpen) return undefined;
windowSize.disableRefresh();
return () => {
windowSize.enableRefresh();
};
}, [isOpen]);
useOnChange(([prevIsPaymentModalOpen]) => {
if (isPaymentModalOpen === prevIsPaymentModalOpen) return;
if (webApp?.slug && !isPaymentModalOpen && paymentStatus) {
@ -330,7 +320,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
return ({ onTrigger, isOpen: isMenuOpen }) => (
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
size="smaller"
color="translucent"
className={isMenuOpen ? 'active' : ''}
@ -340,7 +330,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
<i className="icon-more" />
</Button>
);
}, []);
}, [isMobile]);
const backButtonClassName = buildClassName(
'animated-close-icon',

View File

@ -27,10 +27,9 @@ import {
} from '../../global/selectors';
import { stopCurrentAudio } from '../../util/audioPlayer';
import captureEscKeyListener from '../../util/captureEscKeyListener';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
import { IS_TOUCH_ENV } from '../../util/environment';
import { ANIMATION_END_DELAY } from '../../config';
import { MEDIA_VIEWER_MEDIA_QUERY } from '../common/helpers/mediaDimensions';
import windowSize from '../../util/windowSize';
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
import { animateClosing, animateOpening } from './helpers/ghostAnimation';
import { renderMessageText } from '../common/helpers/renderMessageText';
@ -42,6 +41,7 @@ import { exitPictureInPictureIfNeeded } from '../../hooks/usePictureInPicture';
import useLang from '../../hooks/useLang';
import usePrevious from '../../hooks/usePrevious';
import { useMediaProps } from './hooks/useMediaProps';
import useAppLayout from '../../hooks/useAppLayout';
import ReportModal from '../common/ReportModal';
import Button from '../ui/Button';
@ -97,6 +97,7 @@ const MediaViewer: FC<StateProps> = ({
} = getActions();
const isOpen = Boolean(avatarOwner || mediaId);
const { isMobile } = useAppLayout();
/* Animation */
const animationKey = useRef<number>();
@ -163,14 +164,14 @@ const MediaViewer: FC<StateProps> = ({
}, [isVisible]);
useEffect(() => {
if (IS_SINGLE_COLUMN_LAYOUT) {
if (isMobile) {
document.body.classList.toggle('is-media-viewer-open', isOpen);
}
// Disable user selection if media viewer is open, to prevent accidental text selection
if (IS_TOUCH_ENV) {
document.body.classList.toggle('no-selection', isOpen);
}
}, [isOpen]);
}, [isMobile, isOpen]);
const forceUpdate = useForceUpdate();
useEffect(() => {
@ -220,7 +221,7 @@ const MediaViewer: FC<StateProps> = ({
const handleFooterClick = useCallback(() => {
handleClose();
if (IS_SINGLE_COLUMN_LAYOUT) {
if (isMobile) {
setTimeout(() => {
toggleChatInfo(false, { forceSyncOnIOs: true });
focusMessage({ chatId, threadId, mediaId });
@ -228,7 +229,7 @@ const MediaViewer: FC<StateProps> = ({
} else {
focusMessage({ chatId, threadId, mediaId });
}
}, [handleClose, chatId, threadId, focusMessage, toggleChatInfo, mediaId]);
}, [handleClose, isMobile, chatId, threadId, focusMessage, toggleChatInfo, mediaId]);
const handleForward = useCallback(() => {
openForwardMenu({
@ -259,18 +260,6 @@ const MediaViewer: FC<StateProps> = ({
}
}, [isGif, isVideo]);
// Prevent refresh when rotating device to watch a video
useEffect(() => {
if (!isOpen) {
return undefined;
}
windowSize.disableRefresh();
return () => {
windowSize.enableRefresh();
};
}, [isOpen]);
const getMediaId = useCallback((fromId?: number, direction?: number): number | undefined => {
if (fromId === undefined) return undefined;
const index = mediaIds.indexOf(fromId);
@ -317,7 +306,7 @@ const MediaViewer: FC<StateProps> = ({
noCloseTransition={shouldSkipHistoryAnimations}
>
<div className="media-viewer-head" dir={lang.isRtl ? 'rtl' : undefined}>
{IS_SINGLE_COLUMN_LAYOUT && (
{isMobile && (
<Button
className="media-viewer-close"
round

View File

@ -19,12 +19,12 @@ import {
selectCurrentMessageList,
selectIsChatProtected,
} from '../../global/selectors';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { getMessageMediaFormat, getMessageMediaHash, isUserId } from '../../global/helpers';
import useLang from '../../hooks/useLang';
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
import useFlag from '../../hooks/useFlag';
import useAppLayout from '../../hooks/useAppLayout';
import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu';
@ -87,6 +87,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
setZoomLevelChange,
}) => {
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false);
const { isMobile } = useAppLayout();
const {
downloadMessageMedia,
@ -201,7 +202,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
);
}
if (IS_SINGLE_COLUMN_LAYOUT) {
if (isMobile) {
const menuItems: MenuItemProps[] = [];
if (!message?.isForwardingAllowed && !isChatProtected) {
menuItems.push({

View File

@ -8,7 +8,7 @@ import type {
import type { AnimationLevel } from '../../types';
import { MediaViewerOrigin } from '../../types';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
import { IS_TOUCH_ENV } from '../../util/environment';
import {
selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectUser,
} from '../../global/selectors';
@ -17,6 +17,7 @@ import { renderMessageText } from '../common/helpers/renderMessageText';
import stopEvent from '../../util/stopEvent';
import buildClassName from '../../util/buildClassName';
import { useMediaProps } from './hooks/useMediaProps';
import useAppLayout from '../../hooks/useAppLayout';
import Spinner from '../ui/Spinner';
import MediaViewerFooter from './MediaViewerFooter';
@ -94,6 +95,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
});
const isOpen = Boolean(avatarOwner || mediaId);
const { isMobile } = useAppLayout();
const toggleControls = useCallback((isVisible) => {
setControlsVisible?.(isVisible);
@ -106,7 +108,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
{renderPhoto(
bestData,
calculateMediaViewerDimensions(dimensions, false),
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
!isMobile && !isProtected,
isProtected,
)}
</div>
@ -150,13 +152,13 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
{isPhoto && renderPhoto(
bestData,
message && calculateMediaViewerDimensions(dimensions!, hasFooter),
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
!isMobile && !isProtected,
isProtected,
)}
{isVideo && (!isActive ? renderVideoPreview(
bestImageData,
message && calculateMediaViewerDimensions(dimensions!, hasFooter, true),
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
!isMobile && !isProtected,
isProtected,
) : (
<VideoPlayer

View File

@ -3,10 +3,10 @@ import React, { useEffect, useState } from '../../lib/teact/teact';
import type { TextPart } from '../../types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { REM } from '../common/helpers/mediaDimensions';
import { throttle } from '../../util/schedulers';
import buildClassName from '../../util/buildClassName';
import { REM } from '../common/helpers/mediaDimensions';
import useAppLayout from '../../hooks/useAppLayout';
import './MediaViewerFooter.scss';
@ -24,6 +24,8 @@ const MediaViewerFooter: FC<OwnProps> = ({
text = '', isHidden, isForVideo, onClick, isProtected,
}) => {
const [isMultiline, setIsMultiline] = useState(false);
const { isMobile } = useAppLayout();
useEffect(() => {
const footerContent = document.querySelector('.MediaViewerFooter .media-text') as HTMLDivElement | null;
@ -61,7 +63,7 @@ const MediaViewerFooter: FC<OwnProps> = ({
return (
<div className={classNames} onClick={stopEvent}>
{Boolean(text) && (
<div className="media-viewer-footer-content" onClick={!IS_SINGLE_COLUMN_LAYOUT ? onClick : undefined}>
<div className="media-viewer-footer-content" onClick={!isMobile ? onClick : undefined}>
<p className={`media-text custom-scroll ${isMultiline ? 'multiline' : ''}`} dir="auto">{text}</p>
</div>
)}

View File

@ -5,7 +5,6 @@ import { getActions, withGlobal } from '../../global';
import type { ApiChat, ApiMessage, ApiUser } from '../../api/types';
import type { AnimationLevel } from '../../types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { getSenderTitle, isUserId } from '../../global/helpers';
import { formatMediaDateTime } from '../../util/dateFormat';
import renderText from '../common/helpers/renderText';
@ -16,6 +15,7 @@ import {
selectUser,
} from '../../global/selectors';
import useLang from '../../hooks/useLang';
import useAppLayout from '../../hooks/useAppLayout';
import Avatar from '../common/Avatar';
@ -49,10 +49,12 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
toggleChatInfo,
} = getActions();
const { isMobile } = useAppLayout();
const handleFocusMessage = useCallback(() => {
closeMediaViewer();
if (IS_SINGLE_COLUMN_LAYOUT) {
if (isMobile) {
setTimeout(() => {
toggleChatInfo(false, { forceSyncOnIOs: true });
focusMessage({ chatId, messageId });
@ -60,7 +62,7 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
} else {
focusMessage({ chatId, messageId });
}
}, [chatId, focusMessage, toggleChatInfo, messageId, closeMediaViewer]);
}, [chatId, isMobile, focusMessage, toggleChatInfo, messageId, closeMediaViewer]);
const lang = useLang();

View File

@ -6,16 +6,15 @@ import { getActions } from '../../global';
import type { ApiDimensions } from '../../api/types';
import { IS_IOS, IS_TOUCH_ENV, IS_YA_BROWSER } from '../../util/environment';
import safePlay from '../../util/safePlay';
import stopEvent from '../../util/stopEvent';
import useBuffering from '../../hooks/useBuffering';
import useFullscreen from '../../hooks/useFullscreen';
import usePictureInPicture from '../../hooks/usePictureInPicture';
import useShowTransition from '../../hooks/useShowTransition';
import useVideoCleanup from '../../hooks/useVideoCleanup';
import {
IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV, IS_YA_BROWSER,
} from '../../util/environment';
import safePlay from '../../util/safePlay';
import stopEvent from '../../util/stopEvent';
import useAppLayout from '../../hooks/useAppLayout';
import Button from '../ui/Button';
import ProgressSpinner from '../ui/ProgressSpinner';
@ -77,6 +76,7 @@ const VideoPlayer: FC<OwnProps> = ({
const [isPlaying, setIsPlaying] = useState(!IS_TOUCH_ENV || !IS_IOS);
const [currentTime, setCurrentTime] = useState(0);
const [isFullscreen, setFullscreen, exitFullscreen] = useFullscreen(videoRef, setIsPlaying);
const { isMobile } = useAppLayout();
const handleEnterFullscreen = useCallback(() => {
// Yandex browser doesn't support PIP when video is hidden
@ -159,7 +159,10 @@ const VideoPlayer: FC<OwnProps> = ({
}, [isPlaying]);
const handleClick = useCallback((e: React.MouseEvent<HTMLVideoElement, MouseEvent>) => {
if (isClickDisabled) return;
if (isClickDisabled) {
return;
}
if (shouldCloseOnClick) {
onClose(e);
} else {
@ -239,7 +242,7 @@ const VideoPlayer: FC<OwnProps> = ({
<div
onContextMenu={stopEvent}
onDoubleClick={!IS_TOUCH_ENV ? handleFullscreenChange : undefined}
onClick={!IS_SINGLE_COLUMN_LAYOUT ? togglePlayState : undefined}
onClick={!isMobile ? togglePlayState : undefined}
className="protector"
/>
)}
@ -255,7 +258,7 @@ const VideoPlayer: FC<OwnProps> = ({
style={videoStyle}
onPlay={() => setIsPlaying(true)}
onEnded={handleEnded}
onClick={!IS_SINGLE_COLUMN_LAYOUT && !isFullscreen ? handleClick : undefined}
onClick={!isMobile && !isFullscreen ? handleClick : undefined}
onDoubleClick={!IS_TOUCH_ENV ? handleFullscreenChange : undefined}
// eslint-disable-next-line react/jsx-props-no-spreading
{...bufferingHandlers}

View File

@ -2,15 +2,17 @@ import type { FC } from '../../lib/teact/teact';
import React, {
useEffect, useRef, useCallback, useMemo,
} from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName';
import useFlag from '../../hooks/useFlag';
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
import type { BufferedRange } from '../../hooks/useBuffering';
import { IS_IOS, IS_TOUCH_ENV } from '../../util/environment';
import buildClassName from '../../util/buildClassName';
import { formatMediaDuration } from '../../util/dateFormat';
import { formatFileSize } from '../../util/textFormat';
import useLang from '../../hooks/useLang';
import type { BufferedRange } from '../../hooks/useBuffering';
import { captureEvents } from '../../util/captureEvents';
import useLang from '../../hooks/useLang';
import useFlag from '../../hooks/useFlag';
import useAppLayout from '../../hooks/useAppLayout';
import Button from '../ui/Button';
import RangeSlider from '../ui/RangeSlider';
@ -89,6 +91,8 @@ const VideoPlayerControls: FC<OwnProps> = ({
const isSeekingRef = useRef<boolean>(false);
const isSeeking = isSeekingRef.current;
const { isMobile } = useAppLayout();
useEffect(() => {
if (!IS_TOUCH_ENV) return undefined;
let timeout: number | undefined;
@ -170,7 +174,7 @@ const VideoPlayerControls: FC<OwnProps> = ({
<Button
ariaLabel={lang('AccActionPlay')}
size="tiny"
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
color="translucent-white"
className="play"
round

View File

@ -1,6 +1,4 @@
import React, {
useCallback, useEffect, useMemo, useRef,
} from '../../lib/teact/teact';
import React, { useCallback, useMemo, useRef } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact';
@ -9,7 +7,7 @@ import type {
ApiAudio, ApiChat, ApiMessage, ApiUser,
} from '../../api/types';
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
import { IS_IOS, IS_TOUCH_ENV } from '../../util/environment';
import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../config';
import * as mediaLoader from '../../util/mediaLoader';
@ -20,10 +18,10 @@ import { selectChat, selectSender } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { makeTrackId } from '../../util/audioPlayer';
import { clearMediaSession } from '../../util/mediaSession';
import windowSize from '../../util/windowSize';
import useLang from '../../hooks/useLang';
import renderText from '../common/helpers/renderText';
import useLang from '../../hooks/useLang';
import useAppLayout from '../../hooks/useAppLayout';
import useAudioPlayer from '../../hooks/useAudioPlayer';
import useMessageMediaMetadata from '../../hooks/useMessageMediaMetadata';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
@ -80,6 +78,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const lang = useLang();
const { isMobile } = useAppLayout();
const { audio, voice, video } = getMessageContent(message);
const isVoice = Boolean(voice || video);
const shouldRenderPlaybackButton = isVoice || (audio?.duration || 0) > PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION;
@ -113,20 +112,6 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
true,
);
// Prevent refresh by accidentally rotating device when listening to a voice message
const isVoicePlaying = isVoice && isPlaying;
useEffect(() => {
if (!isVoicePlaying) {
return undefined;
}
windowSize.disableRefresh();
return () => {
windowSize.enableRefresh();
};
}, [isVoicePlaying]);
const {
isContextMenuOpen,
handleBeforeContextMenu, handleContextMenu,
@ -178,7 +163,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
color="translucent"
size="smaller"
ariaLabel="Playback Rate"
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
onClick={handlePlaybackClick}
onMouseDown={handleBeforeContextMenu}
onContextMenu={handleContextMenu}
@ -188,7 +173,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
</span>
</Button>
);
}, [handleBeforeContextMenu, handleContextMenu, handlePlaybackClick, playbackRate]);
}, [handleBeforeContextMenu, handleContextMenu, handlePlaybackClick, isMobile, playbackRate]);
const volumeIcon = useMemo(() => {
if (volume === 0 || isMuted) return 'icon-muted';
@ -210,7 +195,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
color="translucent"
size="smaller"
className="player-button"
@ -222,7 +207,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
</Button>
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
color="translucent"
size="smaller"
className={buildClassName('toggle-play', 'player-button', isPlaying ? 'pause' : 'play')}
@ -234,7 +219,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
</Button>
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
color="translucent"
size="smaller"
className="player-button"

View File

@ -13,9 +13,7 @@ import type { IAnchorPosition } from '../../types';
import { ManagementScreens } from '../../types';
import { ANIMATION_LEVEL_MIN } from '../../config';
import {
ARE_CALLS_SUPPORTED, IS_PWA, IS_SINGLE_COLUMN_LAYOUT,
} from '../../util/environment';
import { ARE_CALLS_SUPPORTED, IS_PWA } from '../../util/environment';
import {
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
} from '../../global/helpers';
@ -40,6 +38,7 @@ interface OwnProps {
messageListType: MessageListType;
canExpandActions: boolean;
withForumActions?: boolean;
isMobile?: boolean;
onTopicSearch?: NoneToVoidFunction;
}
@ -70,6 +69,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
chatId,
threadId,
noMenu,
isMobile,
isChannel,
canStartBot,
canRestartBot,
@ -149,7 +149,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
openLocalTextSearch();
if (IS_SINGLE_COLUMN_LAYOUT) {
if (isMobile) {
// iOS requires synchronous focus on user event.
const searchInput = document.querySelector<HTMLInputElement>('#MobileSearch input')!;
searchInput.focus();
@ -161,7 +161,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
} else {
setTimeout(setFocusInSearchInput, SEARCH_FOCUS_DELAY_MS);
}
}, [noAnimation, onTopicSearch, openLocalTextSearch, withForumActions]);
}, [isMobile, noAnimation, onTopicSearch, openLocalTextSearch, withForumActions]);
const handleAsMessagesClick = useCallback(() => {
openChat({ id: chatId, threadId: MAIN_THREAD_ID });
@ -186,7 +186,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
return (
<div className="HeaderActions">
{!IS_SINGLE_COLUMN_LAYOUT && (
{!isMobile && (
<>
{canExpandActions && !shouldSendJoinRequest && (canSubscribe || shouldJoinToSend) && (
<Button
@ -272,7 +272,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
ref={menuButtonRef}
className={isMenuOpen ? 'active' : ''}
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
size="smaller"
color="translucent"
disabled={noMenu}
@ -287,7 +287,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
threadId={threadId}
isOpen={isMenuOpen}
anchor={menuPosition}
withExtraActions={IS_SINGLE_COLUMN_LAYOUT || !canExpandActions}
withExtraActions={isMobile || !canExpandActions}
isChannel={isChannel}
canStartBot={canStartBot}
canRestartBot={canRestartBot}
@ -314,7 +314,9 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, messageListType }): StateProps => {
(global, {
chatId, threadId, messageListType, isMobile,
}): StateProps => {
const chat = selectChat(global, chatId);
const isChannel = Boolean(chat && isChatChannel(chat));
@ -328,7 +330,7 @@ export default memo(withGlobal<OwnProps>(
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID;
const isDiscussionThread = messageListType === 'thread' && threadId !== MAIN_THREAD_ID;
const isRightColumnShown = selectIsRightColumnShown(global);
const isRightColumnShown = selectIsRightColumnShown(global, isMobile);
const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
const canStartBot = !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));

View File

@ -9,7 +9,6 @@ import type { IAnchorPosition } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types';
import { REPLIES_USER_ID } from '../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { disableScrolling, enableScrolling } from '../../util/scrollLock';
import {
selectChat,
@ -34,6 +33,7 @@ import {
import useShowTransition from '../../hooks/useShowTransition';
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
import useLang from '../../hooks/useLang';
import useAppLayout from '../../hooks/useAppLayout';
import Portal from '../ui/Portal';
import Menu from '../ui/Menu';
@ -163,6 +163,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
openChat,
} = getActions();
const { isMobile } = useAppLayout();
const [isMenuOpen, setIsMenuOpen] = useState(true);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
@ -361,7 +362,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
{lang('lng_forum_topic_edit')}
</MenuItem>
)}
{IS_SINGLE_COLUMN_LAYOUT && !withForumActions && isForum && !isTopic && (
{isMobile && !withForumActions && isForum && !isTopic && (
<MenuItem
icon="forums"
onClick={handleViewAsTopicsClick}
@ -418,7 +419,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
{lang('AddContact')}
</MenuItem>
)}
{IS_SINGLE_COLUMN_LAYOUT && canCall && (
{isMobile && canCall && (
<MenuItem
icon="phone"
onClick={handleCall}
@ -434,7 +435,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
{lang('VideoCall')}
</MenuItem>
)}
{IS_SINGLE_COLUMN_LAYOUT && canSearch && (
{isMobile && canSearch && (
<MenuItem
icon="search"
onClick={handleSearch}

View File

@ -25,11 +25,7 @@ import {
ANIMATION_LEVEL_MIN,
SUPPORTED_IMAGE_CONTENT_TYPES,
} from '../../config';
import {
IS_SINGLE_COLUMN_LAYOUT,
IS_TABLET_COLUMN_LAYOUT,
MASK_IMAGE_DISABLED,
} from '../../util/environment';
import { MASK_IMAGE_DISABLED } from '../../util/environment';
import { DropAreaState } from './composer/DropArea';
import {
selectChat,
@ -53,6 +49,7 @@ import {
isChatSuperGroup,
isUserId,
} from '../../global/helpers';
import calculateMiddleFooterTransforms from './helpers/calculateMiddleFooterTransforms';
import captureEscKeyListener from '../../util/captureEscKeyListener';
import buildClassName from '../../util/buildClassName';
import useCustomBackground from '../../hooks/useCustomBackground';
@ -63,7 +60,7 @@ import useHistoryBack from '../../hooks/useHistoryBack';
import usePrevious from '../../hooks/usePrevious';
import useForceUpdate from '../../hooks/useForceUpdate';
import useOnChange from '../../hooks/useOnChange';
import calculateMiddleFooterTransforms from './helpers/calculateMiddleFooterTransforms';
import useAppLayout from '../../hooks/useAppLayout';
import Transition from '../ui/Transition';
import MiddleHeader from './MiddleHeader';
@ -82,6 +79,10 @@ import GiftPremiumModal from '../main/premium/GiftPremiumModal.async';
import './MiddleColumn.scss';
import styles from './MiddleColumn.module.scss';
interface OwnProps {
isMobile?: boolean;
}
type StateProps = {
chatId?: string;
threadId?: number;
@ -103,7 +104,7 @@ type StateProps = {
isLeftColumnShown?: boolean;
isRightColumnShown?: boolean;
isBackgroundBlurred?: boolean;
isMobileSearchActive?: boolean;
hasCurrentTextSearch?: boolean;
isSelectModeActive?: boolean;
isSeenByModalOpen: boolean;
isReactorListModalOpen: boolean;
@ -123,16 +124,15 @@ type StateProps = {
lastSyncTime?: number;
};
const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined;
function isImage(item: DataTransferItem) {
return item.kind === 'file' && item.type && SUPPORTED_IMAGE_CONTENT_TYPES.has(item.type);
}
const MiddleColumn: FC<StateProps> = ({
const MiddleColumn: FC<OwnProps & StateProps> = ({
chatId,
threadId,
messageListType,
isMobile,
chat,
replyingToId,
isPrivate,
@ -149,7 +149,7 @@ const MiddleColumn: FC<StateProps> = ({
isLeftColumnShown,
isRightColumnShown,
isBackgroundBlurred,
isMobileSearchActive,
hasCurrentTextSearch,
isSelectModeActive,
isSeenByModalOpen,
isReactorListModalOpen,
@ -184,6 +184,7 @@ const MiddleColumn: FC<StateProps> = ({
} = getActions();
const { width: windowWidth } = useWindowSize();
const { isTablet } = useAppLayout();
const lang = useLang();
const [dropAreaState, setDropAreaState] = useState(DropAreaState.None);
@ -191,6 +192,8 @@ const MiddleColumn: FC<StateProps> = ({
const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>();
const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false);
const isMobileSearchActive = isMobile && hasCurrentTextSearch;
const closeAnimationDuration = isMobile ? 450 + ANIMATION_END_DELAY : undefined;
const hasTools = hasPinnedOrAudioPlayer && (
windowWidth < MOBILE_SCREEN_MAX_WIDTH
|| (
@ -202,19 +205,19 @@ const MiddleColumn: FC<StateProps> = ({
)
);
const renderingChatId = usePrevDuringAnimation(chatId, CLOSE_ANIMATION_DURATION);
const renderingThreadId = usePrevDuringAnimation(threadId, CLOSE_ANIMATION_DURATION);
const renderingMessageListType = usePrevDuringAnimation(messageListType, CLOSE_ANIMATION_DURATION);
const renderingCanSubscribe = usePrevDuringAnimation(canSubscribe, CLOSE_ANIMATION_DURATION);
const renderingCanStartBot = usePrevDuringAnimation(canStartBot, CLOSE_ANIMATION_DURATION);
const renderingCanRestartBot = usePrevDuringAnimation(canRestartBot, CLOSE_ANIMATION_DURATION);
const renderingCanPost = usePrevDuringAnimation(canPost, CLOSE_ANIMATION_DURATION)
const renderingChatId = usePrevDuringAnimation(chatId, closeAnimationDuration);
const renderingThreadId = usePrevDuringAnimation(threadId, closeAnimationDuration);
const renderingMessageListType = usePrevDuringAnimation(messageListType, closeAnimationDuration);
const renderingCanSubscribe = usePrevDuringAnimation(canSubscribe, closeAnimationDuration);
const renderingCanStartBot = usePrevDuringAnimation(canStartBot, closeAnimationDuration);
const renderingCanRestartBot = usePrevDuringAnimation(canRestartBot, closeAnimationDuration);
const renderingCanPost = usePrevDuringAnimation(canPost, closeAnimationDuration)
&& !renderingCanRestartBot && !renderingCanStartBot && !renderingCanSubscribe;
const renderingHasTools = usePrevDuringAnimation(hasTools, CLOSE_ANIMATION_DURATION);
const renderingIsFabShown = usePrevDuringAnimation(isFabShown, CLOSE_ANIMATION_DURATION);
const renderingIsChannel = usePrevDuringAnimation(isChannel, CLOSE_ANIMATION_DURATION);
const renderingShouldJoinToSend = usePrevDuringAnimation(shouldJoinToSend, CLOSE_ANIMATION_DURATION);
const renderingShouldSendJoinRequest = usePrevDuringAnimation(shouldSendJoinRequest, CLOSE_ANIMATION_DURATION);
const renderingHasTools = usePrevDuringAnimation(hasTools, closeAnimationDuration);
const renderingIsFabShown = usePrevDuringAnimation(isFabShown, closeAnimationDuration);
const renderingIsChannel = usePrevDuringAnimation(isChannel, closeAnimationDuration);
const renderingShouldJoinToSend = usePrevDuringAnimation(shouldJoinToSend, closeAnimationDuration);
const renderingShouldSendJoinRequest = usePrevDuringAnimation(shouldSendJoinRequest, closeAnimationDuration);
const prevTransitionKey = usePrevious(currentTransitionKey);
@ -227,6 +230,7 @@ const MiddleColumn: FC<StateProps> = ({
currentTransitionKey,
prevTransitionKey,
chatId,
isMobile,
);
useEffect(() => {
@ -414,7 +418,7 @@ const MiddleColumn: FC<StateProps> = ({
--theme-background-color:
${backgroundColor || (theme === 'dark' ? DARK_THEME_BG_COLOR : LIGHT_THEME_BG_COLOR)};
`}
onClick={(IS_TABLET_COLUMN_LAYOUT && isLeftColumnShown) ? handleTabletFocus : undefined}
onClick={(isTablet && isLeftColumnShown) ? handleTabletFocus : undefined}
>
<div
className={bgClassName}
@ -429,6 +433,7 @@ const MiddleColumn: FC<StateProps> = ({
threadId={renderingThreadId}
messageListType={renderingMessageListType}
isReady={isReady}
isMobile={isMobile}
/>
<Transition
name={shouldSkipHistoryAnimations ? 'none' : animationLevel === ANIMATION_LEVEL_MAX ? 'slide' : 'fade'}
@ -459,6 +464,7 @@ const MiddleColumn: FC<StateProps> = ({
dropAreaState={dropAreaState}
onDropHide={handleHideDropArea}
isReady={isReady}
isMobile={isMobile}
/>
)}
{isPinnedMessageList && (
@ -484,7 +490,7 @@ const MiddleColumn: FC<StateProps> = ({
</div>
</div>
)}
{IS_SINGLE_COLUMN_LAYOUT
{isMobile
&& (renderingCanSubscribe || (renderingShouldJoinToSend && !renderingShouldSendJoinRequest)) && (
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
<Button
@ -498,7 +504,7 @@ const MiddleColumn: FC<StateProps> = ({
</Button>
</div>
)}
{IS_SINGLE_COLUMN_LAYOUT && renderingShouldSendJoinRequest && (
{isMobile && renderingShouldSendJoinRequest && (
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
@ -511,7 +517,7 @@ const MiddleColumn: FC<StateProps> = ({
</Button>
</div>
)}
{IS_SINGLE_COLUMN_LAYOUT && renderingCanStartBot && (
{isMobile && renderingCanStartBot && (
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
@ -524,7 +530,7 @@ const MiddleColumn: FC<StateProps> = ({
</Button>
</div>
)}
{IS_SINGLE_COLUMN_LAYOUT && renderingCanRestartBot && (
{isMobile && renderingCanRestartBot && (
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
@ -553,7 +559,7 @@ const MiddleColumn: FC<StateProps> = ({
withExtraShift={withExtraShift}
/>
</div>
{IS_SINGLE_COLUMN_LAYOUT && <MobileSearch isActive={Boolean(isMobileSearchActive)} />}
{isMobile && <MobileSearch isActive={Boolean(isMobileSearchActive)} />}
</>
)}
{chatId && (
@ -579,8 +585,8 @@ const MiddleColumn: FC<StateProps> = ({
);
};
export default memo(withGlobal(
(global): StateProps => {
export default memo(withGlobal<OwnProps>(
(global, { isMobile }): StateProps => {
const theme = selectTheme(global);
const {
isBlurred: isBackgroundBlurred, background: customBackground, backgroundColor, patternColor,
@ -598,9 +604,9 @@ export default memo(withGlobal(
backgroundColor,
patternColor,
isLeftColumnShown,
isRightColumnShown: selectIsRightColumnShown(global),
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isBackgroundBlurred,
isMobileSearchActive: Boolean(IS_SINGLE_COLUMN_LAYOUT && selectCurrentTextSearch(global)),
hasCurrentTextSearch: Boolean(selectCurrentTextSearch(global)),
isSelectModeActive: selectIsInSelectMode(global),
isSeenByModalOpen: Boolean(global.seenByModal),
isReactorListModalOpen: Boolean(global.reactorModal),
@ -681,8 +687,9 @@ function useIsReady(
currentTransitionKey?: number,
prevTransitionKey?: number,
chatId?: string,
isMobile?: boolean,
) {
const [isReady, setIsReady] = useState(!IS_SINGLE_COLUMN_LAYOUT);
const [isReady, setIsReady] = useState(!isMobile);
const forceUpdate = useForceUpdate();
const willSwitchMessageList = prevTransitionKey !== undefined && prevTransitionKey !== currentTransitionKey;

View File

@ -20,7 +20,6 @@ import {
SAFE_SCREEN_WIDTH_FOR_CHAT_INFO,
SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
} from '../../config';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../util/environment';
import {
getChatTitle, getMessageKey, getSenderTitle, isChatChannel, isChatSuperGroup, isUserId,
} from '../../global/helpers';
@ -50,6 +49,7 @@ import buildClassName from '../../util/buildClassName';
import useLang from '../../hooks/useLang';
import useConnectionStatus from '../../hooks/useConnectionStatus';
import usePrevious from '../../hooks/usePrevious';
import useAppLayout from '../../hooks/useAppLayout';
import PrivateChatInfo from '../common/PrivateChatInfo';
import GroupChatInfo from '../common/GroupChatInfo';
@ -73,6 +73,7 @@ type OwnProps = {
threadId: number;
messageListType: MessageListType;
isReady?: boolean;
isMobile?: boolean;
};
type StateProps = {
@ -101,6 +102,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
threadId,
messageListType,
isReady,
isMobile,
pinnedMessageIds,
messagesById,
canUnpin,
@ -133,6 +135,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
const lang = useLang();
const isBackButtonActive = useRef(true);
const { isTablet } = useAppLayout();
const [pinnedMessageIndex, setPinnedMessageIndex] = useState(0);
const pinnedMessageId = Array.isArray(pinnedMessageIds) ? pinnedMessageIds[pinnedMessageIndex] : pinnedMessageIds;
@ -160,7 +163,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
const { width: windowWidth } = useWindowSize();
const isLeftColumnHideable = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN;
const shouldShowCloseButton = IS_TABLET_COLUMN_LAYOUT && isLeftColumnShown;
const shouldShowCloseButton = isTablet && isLeftColumnShown;
// eslint-disable-next-line no-null/no-null
const componentRef = useRef<HTMLDivElement>(null);
@ -198,7 +201,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
// Workaround for missing UI when quickly clicking the Back button
isBackButtonActive.current = false;
if (IS_SINGLE_COLUMN_LAYOUT) {
if (isMobile) {
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
messageInput?.blur();
}
@ -210,7 +213,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
}
if (messageListType === 'thread' && currentTransitionKey === 0) {
if (IS_SINGLE_COLUMN_LAYOUT || shouldShowCloseButton) {
if (isMobile || shouldShowCloseButton) {
e.stopPropagation(); // Stop propagation to prevent chat re-opening on tablets
openChat({ id: undefined }, { forceOnHeavyAnimation: true });
} else {
@ -226,7 +229,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
setBackButtonActive();
}, [
messageListType, currentTransitionKey, isSelectModeActive, openPreviousChat, shouldShowCloseButton,
openChat, toggleLeftColumn, exitMessageSelectMode, setBackButtonActive,
openChat, toggleLeftColumn, exitMessageSelectMode, setBackButtonActive, isMobile,
]);
const canToolsCollideWithChatInfo = (
@ -386,7 +389,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
const isAudioPlayerRendered = Boolean(shouldRenderAudioPlayer && renderingAudioMessage);
const isPinnedMessagesFullWidth = isAudioPlayerRendered
|| (!IS_SINGLE_COLUMN_LAYOUT && hasButtonInHeader && windowWidth < MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES);
|| (!isMobile && hasButtonInHeader && windowWidth < MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES);
return (
<div className="MiddleHeader" ref={componentRef}>
@ -442,6 +445,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
chatId={chatId}
threadId={threadId}
messageListType={messageListType}
isMobile={isMobile}
canExpandActions={!isAudioPlayerRendered}
/>
</div>
@ -450,7 +454,9 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, messageListType }): StateProps => {
(global, {
chatId, threadId, messageListType, isMobile,
}): StateProps => {
const { isLeftColumnShown, lastSyncTime, shouldSkipHistoryAnimations } = global;
const chat = selectChat(global, chatId);
@ -484,7 +490,7 @@ export default memo(withGlobal<OwnProps>(
const state: StateProps = {
typingStatus,
isLeftColumnShown,
isRightColumnShown: selectIsRightColumnShown(global),
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isSelectModeActive: selectIsInSelectMode(global),
audioMessage,
chat,

View File

@ -14,7 +14,6 @@ import {
SUPPORTED_IMAGE_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES,
} from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
import getFilesFromDataTransferItems from './helpers/getFilesFromDataTransferItems';
import { getHtmlTextLength } from './helpers/getHtmlTextLength';
@ -117,6 +116,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
const hideTimeoutRef = useRef<number>();
const prevAttachments = usePrevious(attachments);
const renderingAttachments = attachments.length ? attachments : prevAttachments;
const { isMobile } = useAppLayout();
const [shouldSendCompressed, setShouldSendCompressed] = useState(
shouldSuggestCompression ?? attachmentSettings.shouldCompress,
@ -312,7 +312,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
return ({ onTrigger, isOpen: isMenuOpen }) => (
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
size="smaller"
color="translucent"
className={isMenuOpen ? 'active' : ''}
@ -322,7 +322,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
<i className="icon-more" />
</Button>
);
}, []);
}, [isMobile]);
const leftChars = useMemo(() => {
const captionLeftBeforeLimit = captionLimit - getHtmlTextLength(caption);

View File

@ -1,16 +1,17 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useCallback } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type { FC } from '../../../lib/teact/teact';
import type { ApiBotCommand } from '../../../api/types';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment';
import { IS_TOUCH_ENV } from '../../../util/environment';
import useMouseInside from '../../../hooks/useMouseInside';
import useAppLayout from '../../../hooks/useAppLayout';
import Menu from '../../ui/Menu';
import BotCommand from './BotCommand';
import './BotCommandMenu.scss';
import { getActions } from '../../../global';
export type OwnProps = {
isOpen: boolean;
@ -22,8 +23,9 @@ const BotCommandMenu: FC<OwnProps> = ({
isOpen, botCommands, onClose,
}) => {
const { sendBotCommand } = getActions();
const { isMobile } = useAppLayout();
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose, undefined, IS_SINGLE_COLUMN_LAYOUT);
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose, undefined, isMobile);
const handleClick = useCallback((botCommand: ApiBotCommand) => {
sendBotCommand({

View File

@ -30,7 +30,7 @@ import {
SEND_MESSAGE_ACTION_INTERVAL,
EDITABLE_INPUT_CSS_SELECTOR, MAX_UPLOAD_FILEPART_SIZE,
} from '../../../config';
import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
import { IS_VOICE_RECORDING_SUPPORTED, IS_IOS } from '../../../util/environment';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import {
selectChat,
@ -132,6 +132,7 @@ type OwnProps = {
messageListType: MessageListType;
dropAreaState: string;
isReady: boolean;
isMobile?: boolean;
onDropHide: NoneToVoidFunction;
};
@ -213,6 +214,7 @@ const Composer: FC<OwnProps & StateProps> = ({
shouldSchedule,
canScheduleUntilOnline,
isReady,
isMobile,
onDropHide,
editingMessage,
chatId,
@ -548,13 +550,16 @@ const Composer: FC<OwnProps & StateProps> = ({
closeMentionTooltip();
closeEmojiTooltip();
if (IS_SINGLE_COLUMN_LAYOUT) {
if (isMobile) {
// @optimization
setTimeout(() => closeSymbolMenu(), SENDING_ANIMATION_DURATION);
} else {
closeSymbolMenu();
}
}, [closeStickerTooltip, closeCustomEmojiTooltip, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu, setHtml]);
}, [
closeStickerTooltip, closeCustomEmojiTooltip, closeMentionTooltip, closeEmojiTooltip,
closeSymbolMenu, setHtml, isMobile,
]);
// Handle chat change (ref is used to avoid redundant effect calls)
const stopRecordingVoiceRef = useRef<typeof stopRecordingVoice>();
@ -1015,7 +1020,7 @@ const Composer: FC<OwnProps & StateProps> = ({
const handleSymbolMenuOpen = useCallback(() => {
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
if (!IS_SINGLE_COLUMN_LAYOUT || messageInput !== document.activeElement) {
if (!isMobile || messageInput !== document.activeElement) {
openSymbolMenu();
return;
}
@ -1025,12 +1030,12 @@ const Composer: FC<OwnProps & StateProps> = ({
closeBotCommandMenu();
openSymbolMenu();
}, MOBILE_KEYBOARD_HIDE_DELAY_MS);
}, [openSymbolMenu, closeBotCommandMenu]);
}, [openSymbolMenu, closeBotCommandMenu, isMobile]);
const handleSendAsMenuOpen = useCallback(() => {
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
if (!IS_SINGLE_COLUMN_LAYOUT || messageInput !== document.activeElement) {
if (!isMobile || messageInput !== document.activeElement) {
closeBotCommandMenu();
closeSymbolMenu();
openSendAsMenu();
@ -1043,17 +1048,17 @@ const Composer: FC<OwnProps & StateProps> = ({
closeSymbolMenu();
openSendAsMenu();
}, MOBILE_KEYBOARD_HIDE_DELAY_MS);
}, [closeBotCommandMenu, closeSymbolMenu, openSendAsMenu]);
}, [closeBotCommandMenu, closeSymbolMenu, openSendAsMenu, isMobile]);
const handleAllScheduledClick = useCallback(() => {
openChat({ id: chatId, threadId, type: 'scheduled' });
}, [openChat, chatId, threadId]);
useEffect(() => {
if (isRightColumnShown && IS_SINGLE_COLUMN_LAYOUT) {
if (isRightColumnShown && isMobile) {
closeSymbolMenu();
}
}, [isRightColumnShown, closeSymbolMenu]);
}, [isRightColumnShown, closeSymbolMenu, isMobile]);
useEffect(() => {
if (!isReady) return;
@ -1292,7 +1297,7 @@ const Composer: FC<OwnProps & StateProps> = ({
/>
</Button>
)}
{IS_SINGLE_COLUMN_LAYOUT ? (
{isMobile ? (
<Button
className={symbolMenuButtonClassName}
round
@ -1329,7 +1334,7 @@ const Composer: FC<OwnProps & StateProps> = ({
forcedPlaceholder={inlineBotHelp}
canAutoFocus={isReady && !attachments.length}
noFocusInterception={attachments.length > 0}
shouldSuppressFocus={IS_SINGLE_COLUMN_LAYOUT && isSymbolMenuOpen}
shouldSuppressFocus={isMobile && isSymbolMenuOpen}
shouldSuppressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen || isInlineBotTooltipOpen}
onUpdate={setHtml}
onSend={onSend}
@ -1479,7 +1484,9 @@ const Composer: FC<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, messageListType }): StateProps => {
(global, {
chatId, threadId, messageListType, isMobile,
}): StateProps => {
const chat = selectChat(global, chatId);
const chatBot = chatId !== REPLIES_USER_ID ? selectChatBot(global, chatId) : undefined;
const isChatWithBot = Boolean(chatBot);
@ -1521,7 +1528,7 @@ export default memo(withGlobal<OwnProps>(
isForCurrentMessageList,
canScheduleUntilOnline: selectCanScheduleUntilOnline(global, chatId),
isChannel: chat ? isChatChannel(chat) : undefined,
isRightColumnShown: selectIsRightColumnShown(global),
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isSelectModeActive: selectIsInSelectMode(global),
withScheduledButton: (
messageListType === 'thread'

View File

@ -1,15 +1,16 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useRef } from '../../../lib/teact/teact';
import type { FC } from '../../../lib/teact/teact';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { RECENT_SYMBOL_SET_ID } from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import buildClassName from '../../../util/buildClassName';
import windowSize from '../../../util/windowSize';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useMediaTransition from '../../../hooks/useMediaTransition';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import EmojiButton from './EmojiButton';
@ -38,8 +39,9 @@ const EmojiCategory: FC<OwnProps> = ({
const transitionClassNames = useMediaTransition(shouldRender);
const lang = useLang();
const { isMobile } = useAppLayout();
const emojisPerRow = IS_SINGLE_COLUMN_LAYOUT
const emojisPerRow = isMobile
? Math.floor((windowSize.get().width - MOBILE_CONTAINER_PADDING) / (EMOJI_SIZE + EMOJI_MARGIN))
: EMOJIS_PER_ROW_ON_DESKTOP;
const height = Math.ceil(category.emojis.length / emojisPerRow) * (EMOJI_SIZE + EMOJI_MARGIN);

View File

@ -1,22 +1,20 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
useState, useEffect, memo, useRef, useMemo, useCallback,
} from '../../../lib/teact/teact';
import { withGlobal } from '../../../global';
import type { FC } from '../../../lib/teact/teact';
import type { GlobalState } from '../../../global/types';
import { MENU_TRANSITION_DURATION, RECENT_SYMBOL_SET_ID } from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import type {
EmojiModule,
EmojiRawData,
EmojiData,
} from '../../../util/emoji';
import {
uncompressEmoji,
} from '../../../util/emoji';
import { MENU_TRANSITION_DURATION, RECENT_SYMBOL_SET_ID } from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/environment';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { uncompressEmoji } from '../../../util/emoji';
import fastSmoothScroll from '../../../util/fastSmoothScroll';
import { pick } from '../../../util/iteratees';
import buildClassName from '../../../util/buildClassName';
@ -25,6 +23,7 @@ import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import Button from '../../ui/Button';
import Loading from '../../ui/Loading';
@ -79,6 +78,7 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
const [categories, setCategories] = useState<EmojiCategoryData[]>();
const [emojis, setEmojis] = useState<AllEmojis>();
const [activeCategoryIndex, setActiveCategoryIndex] = useState(0);
const { isMobile } = useAppLayout();
const { observe: observeIntersection } = useIntersectionObserver({
rootRef: containerRef,
@ -105,7 +105,7 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
setActiveCategoryIndex(intersectingWithIndexes[Math.floor(intersectingWithIndexes.length / 2)].index);
});
useHorizontalScroll(headerRef.current, !IS_SINGLE_COLUMN_LAYOUT);
useHorizontalScroll(headerRef.current, !isMobile);
// Scroll header when active set updates
useEffect(() => {

View File

@ -8,13 +8,13 @@ import { getActions, withGlobal } from '../../../global';
import type { IAnchorPosition, ISettings } from '../../../types';
import { EDITABLE_INPUT_ID } from '../../../config';
import {
IS_ANDROID, IS_EMOJI_SUPPORTED, IS_IOS, IS_TOUCH_ENV,
} from '../../../util/environment';
import { selectIsInSelectMode, selectReplyingToId } from '../../../global/selectors';
import { debounce } from '../../../util/schedulers';
import focusEditableElement from '../../../util/focusEditableElement';
import buildClassName from '../../../util/buildClassName';
import {
IS_ANDROID, IS_EMOJI_SUPPORTED, IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV,
} from '../../../util/environment';
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
import { getIsDirectTextInputDisabled } from '../../../util/directInputManager';
import parseEmojiOnlyString from '../../../util/parseEmojiOnlyString';
@ -26,6 +26,7 @@ import useFlag from '../../../hooks/useFlag';
import { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck';
import useLang from '../../../hooks/useLang';
import useInputCustomEmojis from './hooks/useInputCustomEmojis';
import useAppLayout from '../../../hooks/useAppLayout';
import TextFormatter from './TextFormatter';
@ -62,7 +63,6 @@ type StateProps = {
messageSendKeyCombo?: ISettings['messageSendKeyCombo'];
};
const MAX_INPUT_HEIGHT = IS_SINGLE_COLUMN_LAYOUT ? 256 : 416;
const MAX_ATTACHMENT_MODAL_INPUT_HEIGHT = 160;
const TAB_INDEX_PRIORITY_TIMEOUT = 2000;
// Heuristics allowing the user to make a triple click
@ -134,14 +134,16 @@ const MessageInput: FC<OwnProps & StateProps> = ({
const [textFormatterAnchorPosition, setTextFormatterAnchorPosition] = useState<IAnchorPosition>();
const [selectedRange, setSelectedRange] = useState<Range>();
const [isTextFormatterDisabled, setIsTextFormatterDisabled] = useState<boolean>(false);
const { isMobile } = useAppLayout();
useInputCustomEmojis(html, inputRef, sharedCanvasRef, sharedCanvasHqRef, absoluteContainerRef);
const maxInputHeight = isMobile ? 256 : 416;
const updateInputHeight = useCallback((willSend = false) => {
const scroller = inputRef.current!.closest<HTMLDivElement>(`.${SCROLLER_CLASS}`)!;
const clone = scrollerCloneRef.current!;
const currentHeight = Number(scroller.style.height.replace('px', ''));
const maxHeight = isAttachmentModalInput ? MAX_ATTACHMENT_MODAL_INPUT_HEIGHT : MAX_INPUT_HEIGHT;
const maxHeight = isAttachmentModalInput ? MAX_ATTACHMENT_MODAL_INPUT_HEIGHT : maxInputHeight;
const newHeight = Math.min(clone.scrollHeight, maxHeight);
if (newHeight === currentHeight) {
return;
@ -163,7 +165,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
} else {
exec();
}
}, [isAttachmentModalInput]);
}, [isAttachmentModalInput, maxInputHeight]);
useEffect(() => {
if (!isAttachmentModalInput) return;
@ -391,7 +393,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
!chatId
|| editableInputId !== EDITABLE_INPUT_ID
|| noFocusInterception
|| (IS_TOUCH_ENV && IS_SINGLE_COLUMN_LAYOUT)
|| (IS_TOUCH_ENV && isMobile)
|| isSelectModeActive
) {
return undefined;
@ -438,7 +440,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
return () => {
document.removeEventListener('keydown', handleDocumentKeyDown, true);
};
}, [chatId, editableInputId, isSelectModeActive, noFocusInterception]);
}, [chatId, editableInputId, isMobile, isSelectModeActive, noFocusInterception]);
useEffect(() => {
const captureFirstTab = debounce((e: KeyboardEvent) => {

View File

@ -12,7 +12,6 @@ import {
DEFAULT_TOPIC_ICON_STICKER_ID,
EMOJI_SIZE_PICKER, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER,
} from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import buildClassName from '../../../util/buildClassName';
import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../../global/selectors';
@ -21,6 +20,7 @@ import useFlag from '../../../hooks/useFlag';
import useMediaTransition from '../../../hooks/useMediaTransition';
import useResizeObserver from '../../../hooks/useResizeObserver';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useAppLayout from '../../../hooks/useAppLayout';
import StickerButton from '../../common/StickerButton';
import ConfirmDialog from '../../ui/ConfirmDialog';
@ -46,10 +46,6 @@ type OwnProps = {
onStickerRemoveRecent?: (sticker: ApiSticker) => void;
};
const STICKER_MARGIN = IS_SINGLE_COLUMN_LAYOUT ? 8 : 16;
const EMOJI_MARGIN = IS_SINGLE_COLUMN_LAYOUT ? 8 : 10;
const CONTAINER_PADDING = IS_SINGLE_COLUMN_LAYOUT ? 8 : 0;
const ITEMS_PER_ROW_FALLBACK = 8;
const StickerSet: FC<OwnProps> = ({
@ -85,8 +81,9 @@ const StickerSet: FC<OwnProps> = ({
// eslint-disable-next-line no-null/no-null
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
const lang = useLang();
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
const { isMobile } = useAppLayout();
const [itemsPerRow, setItemsPerRow] = useState(ITEMS_PER_ROW_FALLBACK);
@ -94,6 +91,9 @@ const StickerSet: FC<OwnProps> = ({
const transitionClassNames = useMediaTransition(shouldRender);
const stickerMarginPx = isMobile ? 8 : 16;
const emojiMarginPx = isMobile ? 8 : 10;
const containerPaddingPx = isMobile ? 8 : 0;
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
const isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID;
const isEmoji = stickerSet.isEmoji;
@ -132,13 +132,13 @@ const StickerSet: FC<OwnProps> = ({
}, [onStickerSelect]);
const itemSize = isEmoji ? EMOJI_SIZE_PICKER : STICKER_SIZE_PICKER;
const margin = isEmoji ? EMOJI_MARGIN : STICKER_MARGIN;
const margin = isEmoji ? emojiMarginPx : stickerMarginPx;
const calculateItemsPerRow = useCallback((width: number) => {
if (!width) return ITEMS_PER_ROW_FALLBACK;
return Math.floor((width - CONTAINER_PADDING) / (itemSize + margin));
}, [itemSize, margin]);
return Math.floor((width - containerPaddingPx) / (itemSize + margin));
}, [containerPaddingPx, itemSize, margin]);
const handleResize = useCallback((entry: ResizeObserverEntry) => {
setItemsPerRow(calculateItemsPerRow(entry.contentRect.width));

View File

@ -7,7 +7,7 @@ import type { FC } from '../../../lib/teact/teact';
import type { ApiSticker, ApiVideo } from '../../../api/types';
import type { GlobalActions } from '../../../global/types';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment';
import { IS_TOUCH_ENV } from '../../../util/environment';
import { fastRaf } from '../../../util/schedulers';
import buildClassName from '../../../util/buildClassName';
import { selectIsCurrentUserPremium } from '../../../global/selectors';
@ -15,6 +15,7 @@ import { selectIsCurrentUserPremium } from '../../../global/selectors';
import useShowTransition from '../../../hooks/useShowTransition';
import useMouseInside from '../../../hooks/useMouseInside';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import Button from '../../ui/Button';
import Menu from '../../ui/Menu';
@ -86,8 +87,9 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
const [activeTab, setActiveTab] = useState<number>(0);
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
const [recentCustomEmojis, setRecentCustomEmojis] = useState<string[]>([]);
const { isMobile } = useAppLayout();
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose, undefined, IS_SINGLE_COLUMN_LAYOUT);
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose, undefined, isMobile);
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, onClose, false, false);
if (!isActivated && isOpen) {
@ -107,7 +109,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
}, [isCurrentUserPremium, lastSyncTime, loadFeaturedEmojiStickers, loadPremiumSetStickers]);
useLayoutEffect(() => {
if (!IS_SINGLE_COLUMN_LAYOUT) {
if (!isMobile) {
return undefined;
}
@ -126,7 +128,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
});
}
};
}, [isOpen]);
}, [isMobile, isOpen]);
const recentEmojisRef = useRef(recentEmojis);
recentEmojisRef.current = recentEmojis;
@ -239,7 +241,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
</Transition>
)}
</div>
{IS_SINGLE_COLUMN_LAYOUT && (
{isMobile && (
<Button
round
faded
@ -261,7 +263,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
</>
);
if (IS_SINGLE_COLUMN_LAYOUT) {
if (isMobile) {
if (!shouldRender) {
return undefined;
}

View File

@ -19,6 +19,7 @@ import { REM } from '../../../common/helpers/mediaDimensions';
import useResizeObserver from '../../../../hooks/useResizeObserver';
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
import useAppLayout from '../../../../hooks/useAppLayout';
const SIZE = 1.25 * REM;
@ -26,7 +27,7 @@ type CustomEmojiPlayer = {
play: () => void;
pause: () => void;
destroy: () => void;
updatePosition: (x: number, y: number) => void;
updatePosition: (x: number, y: number, isMobile?: boolean) => void;
};
export default function useInputCustomEmojis(
@ -38,6 +39,8 @@ export default function useInputCustomEmojis(
) {
const mapRef = useRef<Map<string, CustomEmojiPlayer>>(new Map());
const { isMobile } = useAppLayout();
const removeContainers = useCallback((ids: string[]) => {
ids.forEach((id) => {
const player = mapRef.current.get(id);
@ -74,7 +77,7 @@ export default function useInputCustomEmojis(
if (mapRef.current.has(id)) {
const player = mapRef.current.get(id)!;
player.updatePosition(x, y);
player.updatePosition(x, y, isMobile);
return;
}
@ -93,6 +96,7 @@ export default function useInputCustomEmojis(
mediaUrl,
isHq,
position: { x, y },
isMobile,
});
animation.play();
@ -100,7 +104,7 @@ export default function useInputCustomEmojis(
});
removeContainers(Array.from(removedContainers));
}, [absoluteContainerRef, inputRef, removeContainers, sharedCanvasHqRef, sharedCanvasRef]);
}, [absoluteContainerRef, inputRef, isMobile, removeContainers, sharedCanvasHqRef, sharedCanvasRef]);
useEffect(() => {
addCustomEmojiInputRenderCallback(synchronizeElements);
@ -152,6 +156,7 @@ function createPlayer({
mediaUrl,
position,
isHq,
isMobile,
} : {
customEmoji: ApiSticker;
sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
@ -161,6 +166,7 @@ function createPlayer({
mediaUrl: string;
position: { x: number; y: number };
isHq?: boolean;
isMobile?: boolean;
}): CustomEmojiPlayer {
if (customEmoji.isLottie) {
const lottie = RLottie.init(
@ -173,13 +179,16 @@ function createPlayer({
size: SIZE,
coords: position,
isLowPriority: !isHq,
isMobile,
},
);
return {
play: () => lottie.play(),
pause: () => lottie.pause(),
destroy: () => lottie.removeContainer(uniqueId),
updatePosition: (x: number, y: number) => lottie.setSharedCanvasCoords(uniqueId, { x, y }),
updatePosition: (x: number, y: number, isMobileNew?: boolean) => {
return lottie.setSharedCanvasCoords(uniqueId, { x, y }, isMobileNew);
},
};
}

View File

@ -3,13 +3,13 @@ import { getActions } from '../../../global';
import type { MessageListType } from '../../../global/types';
import { IS_ANDROID, IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { IS_ANDROID } from '../../../util/environment';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useBackgroundMode from '../../../hooks/useBackgroundMode';
import useAppLayout from '../../../hooks/useAppLayout';
const INTERSECTION_THROTTLE_FOR_READING = 150;
const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350;
const INTERSECTION_MARGIN_FOR_LOADING = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500;
export default function useMessageObservers(
type: MessageListType,
@ -18,6 +18,9 @@ export default function useMessageObservers(
) {
const { markMessageListRead, markMentionsRead, animateUnreadReaction } = getActions();
const { isMobile } = useAppLayout();
const INTERSECTION_MARGIN_FOR_LOADING = isMobile ? 300 : 500;
const {
observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading,
} = useIntersectionObserver({

View File

@ -26,12 +26,9 @@ import type {
import type {
AnimationLevel, FocusDirection, IAlbum, ISettings,
} from '../../../types';
import {
AudioOrigin,
} from '../../../types';
import {
MAIN_THREAD_ID,
} from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { AudioOrigin } from '../../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../util/environment';
import { EMOJI_STATUS_LOOP_LIMIT } from '../../../config';
@ -88,8 +85,6 @@ import {
isChatGroup,
} from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import {
calculateDimensionsForMessageMedia,
REM,
@ -100,9 +95,13 @@ import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimen
import { calculateAlbumLayout } from './helpers/calculateAlbumLayout';
import renderText from '../../common/helpers/renderText';
import calculateAuthorWidth from './helpers/calculateAuthorWidth';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { isAnimatingScroll } from '../../../util/fastSmoothScroll';
import { getServerTime } from '../../../util/serverTime';
import { isElementInViewport } from '../../../util/isElementInViewport';
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import { useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang';
import useShowTransition from '../../../hooks/useShowTransition';
@ -110,10 +109,8 @@ import useFlag from '../../../hooks/useFlag';
import useFocusMessage from './hooks/useFocusMessage';
import useOuterHandlers from './hooks/useOuterHandlers';
import useInnerHandlers from './hooks/useInnerHandlers';
import { getServerTime } from '../../../util/serverTime';
import { isElementInViewport } from '../../../util/isElementInViewport';
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
import useResizeObserver from '../../../hooks/useResizeObserver';
import useAppLayout from '../../../hooks/useAppLayout';
import Button from '../../ui/Button';
import Avatar from '../../common/Avatar';
@ -349,6 +346,7 @@ const Message: FC<OwnProps & StateProps> = ({
const [isTranscriptionHidden, setTranscriptionHidden] = useState(false);
const [hasActiveStickerEffect, startStickerEffect, stopStickerEffect] = useFlag();
const { isMobile } = useAppLayout();
useOnIntersect(bottomMarkerRef, observeIntersectionForBottom);
@ -626,19 +624,21 @@ const Message: FC<OwnProps & StateProps> = ({
let calculatedWidth;
let noMediaCorners = false;
const albumLayout = useMemo(() => {
return isAlbum ? calculateAlbumLayout(isOwn, Boolean(asForwarded), Boolean(noAvatars), album!) : undefined;
}, [isAlbum, isOwn, asForwarded, noAvatars, album]);
return isAlbum
? calculateAlbumLayout(isOwn, Boolean(asForwarded), Boolean(noAvatars), album!, isMobile)
: undefined;
}, [isAlbum, isOwn, asForwarded, noAvatars, album, isMobile]);
const extraPadding = asForwarded ? 28 : 0;
if (!isAlbum && (photo || video || invoice?.extendedMedia)) {
let width: number | undefined;
if (photo) {
width = calculateMediaDimensions(message, asForwarded, noAvatars).width;
width = calculateMediaDimensions(message, asForwarded, noAvatars, isMobile).width;
} else if (video) {
if (video.isRound) {
width = ROUND_VIDEO_DIMENSIONS_PX;
} else {
width = calculateMediaDimensions(message, asForwarded, noAvatars).width;
width = calculateMediaDimensions(message, asForwarded, noAvatars, isMobile).width;
}
} else if (invoice?.extendedMedia && (
invoice.extendedMedia.width && invoice.extendedMedia.height
@ -650,6 +650,7 @@ const Message: FC<OwnProps & StateProps> = ({
fromOwnMessage: isOwn,
asForwarded,
noAvatars,
isMobile,
}).width;
}

View File

@ -13,12 +13,12 @@ import { getMessageCopyOptions } from './helpers/copyOptions';
import { disableScrolling, enableScrolling } from '../../../util/scrollLock';
import { getUserFullName } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import renderText from '../../common/helpers/renderText';
import useFlag from '../../../hooks/useFlag';
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';
@ -170,6 +170,7 @@ const MessageContextMenu: FC<OwnProps> = ({
const messageId = !isSponsoredMessage ? message.id : '';
const [isReady, markIsReady, unmarkIsReady] = useFlag();
const { isMobile } = useAppLayout();
const handleAfterCopy = useCallback(() => {
showNotification({
@ -217,11 +218,11 @@ const MessageContextMenu: FC<OwnProps> = ({
);
const getLayout = useCallback(() => {
const extraHeightAudioPlayer = (IS_SINGLE_COLUMN_LAYOUT
const extraHeightAudioPlayer = (isMobile
&& (document.querySelector<HTMLElement>('.AudioPlayer-content'))?.offsetHeight) || 0;
const pinnedElement = document.querySelector<HTMLElement>('.HeaderPinnedMessage-wrapper');
const extraHeightPinned = (((IS_SINGLE_COLUMN_LAYOUT && !extraHeightAudioPlayer)
|| (!IS_SINGLE_COLUMN_LAYOUT && pinnedElement?.classList.contains('full-width')))
const extraHeightPinned = (((isMobile && !extraHeightAudioPlayer)
|| (!isMobile && pinnedElement?.classList.contains('full-width')))
&& pinnedElement?.offsetHeight) || 0;
return {
@ -230,7 +231,7 @@ const MessageContextMenu: FC<OwnProps> = ({
marginSides: withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined,
extraMarginTop: extraHeightPinned + extraHeightAudioPlayer,
};
}, [withReactions]);
}, [isMobile, withReactions]);
useEffect(() => {
if (!isOpen) {

View File

@ -30,6 +30,7 @@ import usePrevious from '../../../hooks/usePrevious';
import useMediaTransition from '../../../hooks/useMediaTransition';
import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevDeps';
import useFlag from '../../../hooks/useFlag';
import useAppLayout from '../../../hooks/useAppLayout';
import ProgressSpinner from '../../ui/ProgressSpinner';
import MediaSpoiler from '../../common/MediaSpoiler';
@ -83,6 +84,7 @@ const Photo: FC<OwnProps> = ({
const isIntersecting = useIsIntersecting(ref, observeIntersection);
const { isMobile } = useAppLayout();
const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad);
const shouldLoad = isLoadAllowed && isIntersecting;
const {
@ -161,7 +163,7 @@ const Photo: FC<OwnProps> = ({
}
}, [shouldAffectAppendix, fullMediaData, isOwn, isInSelectMode, isSelected, theme] as const);
const { width, height, isSmall } = dimensions || calculateMediaDimensions(message, asForwarded, noAvatars);
const { width, height, isSmall } = dimensions || calculateMediaDimensions(message, asForwarded, noAvatars, isMobile);
const className = buildClassName(
'media-inner',

View File

@ -55,7 +55,7 @@ const ReactionSelector: FC<OwnProps> = ({
if (!itemsScrollRef) return;
const deltaY = 'deltaY' in e ? e.deltaY : getTouchY(e);
if (deltaY) {
if (deltaY && e.cancelable) {
e.preventDefault();
}
};

View File

@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useRef } from '../../../lib/teact/teact'
import type { ApiMessage } from '../../../api/types';
import { ApiMediaFormat } from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { getStickerDimensions } from '../../common/helpers/mediaDimensions';
import { getMessageMediaHash } from '../../../global/helpers';
@ -10,11 +11,11 @@ import buildClassName from '../../../util/buildClassName';
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
import { getActions } from '../../../global';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useMedia from '../../../hooks/useMedia';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import StickerView from '../../common/StickerView';
import AnimatedSticker from '../../common/AnimatedSticker';
@ -42,6 +43,7 @@ const Sticker: FC<OwnProps> = ({
const { showNotification, openStickerSet } = getActions();
const lang = useLang();
const { isMobile } = useAppLayout();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
@ -101,7 +103,7 @@ const Sticker: FC<OwnProps> = ({
}, [hasEffect, isPlayingEffect, lang, onPlayEffect, openModal, showNotification, startPlayingEffect]);
const isMemojiSticker = 'isMissing' in stickerSetInfo;
const { width, height } = getStickerDimensions(sticker);
const { width, height } = getStickerDimensions(sticker, isMobile);
const className = buildClassName(
'Sticker media-inner',
isMemojiSticker && 'inactive',

View File

@ -4,6 +4,7 @@ import { getActions } from '../../../global';
import type { ApiMessage } from '../../../api/types';
import type { IMediaDimensions } from './helpers/calculateAlbumLayout';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { formatMediaDuration } from '../../../util/dateFormat';
import buildClassName from '../../../util/buildClassName';
@ -17,7 +18,6 @@ import {
getMessageWebPageVideo,
isOwnMessage,
} from '../../../global/helpers';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import * as mediaLoader from '../../../util/mediaLoader';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
@ -27,6 +27,7 @@ import usePrevious from '../../../hooks/usePrevious';
import useMediaTransition from '../../../hooks/useMediaTransition';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
import useFlag from '../../../hooks/useFlag';
import useAppLayout from '../../../hooks/useAppLayout';
import ProgressSpinner from '../../ui/ProgressSpinner';
import OptimizedVideo from '../../ui/OptimizedVideo';
@ -87,6 +88,7 @@ const Video: FC<OwnProps> = ({
wasIntersectedRef.current = true;
}
const { isMobile } = useAppLayout();
const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad);
const shouldLoad = Boolean(isLoadAllowed && isIntersectingForLoading && lastSyncTime);
const [isPlayAllowed, setIsPlayAllowed] = useState(canAutoPlay && !isSpoilerShown);
@ -147,7 +149,7 @@ const Video: FC<OwnProps> = ({
const isWebPageVideo = Boolean(getMessageWebPageVideo(message));
const {
width, height,
} = dimensions || calculateVideoDimensions(video, isOwn, asForwarded, isWebPageVideo, noAvatars);
} = dimensions || calculateVideoDimensions(video, isOwn, asForwarded, isWebPageVideo, noAvatars, isMobile);
const handleClick = useCallback(() => {
if (isUploading) {

View File

@ -10,6 +10,7 @@ import { calculateMediaDimensions } from './helpers/mediaDimensions';
import renderText from '../../common/helpers/renderText';
import trimText from '../../../util/trimText';
import buildClassName from '../../../util/buildClassName';
import useAppLayout from '../../../hooks/useAppLayout';
import SafeLink from '../../common/SafeLink';
import Photo from './Photo';
@ -51,6 +52,7 @@ const WebPage: FC<OwnProps> = ({
onCancelMediaTransfer,
}) => {
const webPage = getMessageWebPage(message);
const { isMobile } = useAppLayout();
const handleMediaClick = useCallback(() => {
onMediaClick!();
@ -73,7 +75,7 @@ const WebPage: FC<OwnProps> = ({
const isArticle = Boolean(truncatedDescription || title || siteName);
let isSquarePhoto = false;
if (isArticle && webPage?.photo && !webPage.video) {
const { width, height } = calculateMediaDimensions(message);
const { width, height } = calculateMediaDimensions(message, undefined, undefined, isMobile);
isSquarePhoto = width === height;
}
const isMediaInteractive = (photo || video) && onMediaClick && !isSquarePhoto;

View File

@ -46,10 +46,10 @@ export type IAlbumLayout = {
containerStyle: ApiDimensions;
};
function getRatios(messages: ApiMessage[]) {
function getRatios(messages: ApiMessage[], isMobile?: boolean) {
return messages.map(
(message) => {
const dimensions = calculateMediaDimensions(message) as ApiDimensions;
const dimensions = calculateMediaDimensions(message, undefined, undefined, isMobile) as ApiDimensions;
return dimensions.width / dimensions.height;
},
@ -96,14 +96,15 @@ export function calculateAlbumLayout(
asForwarded: boolean,
noAvatars: boolean,
album: IAlbum,
isMobile?: boolean,
): IAlbumLayout {
const spacing = 2;
const ratios = getRatios(album.messages);
const ratios = getRatios(album.messages, isMobile);
const proportions = getProportions(ratios);
const averageRatio = getAverageRatio(ratios);
const albumCount = ratios.length;
const forceCalc = ratios.some((ratio) => ratio > 2);
const maxWidth = getAvailableWidth(isOwn, asForwarded, false, noAvatars) - (asForwarded ? 2.5 : 0) * REM;
const maxWidth = getAvailableWidth(isOwn, asForwarded, false, noAvatars, isMobile) - (asForwarded ? 2.5 : 0) * REM;
const maxHeight = maxWidth;
let layout;

View File

@ -22,7 +22,9 @@ export function getMinMediaWidth(hasText?: boolean, hasCommentButton?: boolean)
: (hasCommentButton ? MIN_MEDIA_WIDTH_WITH_COMMENTS : MIN_MEDIA_WIDTH);
}
export function calculateMediaDimensions(message: ApiMessage, asForwarded?: boolean, noAvatars?: boolean) {
export function calculateMediaDimensions(
message: ApiMessage, asForwarded?: boolean, noAvatars?: boolean, isMobile?: boolean,
) {
const isOwn = isOwnMessage(message);
const photo = getMessagePhoto(message) || getMessageWebPagePhoto(message);
const video = getMessageVideo(message);
@ -30,8 +32,8 @@ export function calculateMediaDimensions(message: ApiMessage, asForwarded?: bool
const isWebPagePhoto = Boolean(getMessageWebPagePhoto(message));
const isWebPageVideo = Boolean(getMessageWebPageVideo(message));
const { width, height } = photo
? calculateInlineImageDimensions(photo, isOwn, asForwarded, isWebPagePhoto, noAvatars)
: calculateVideoDimensions(video!, isOwn, asForwarded, isWebPageVideo, noAvatars);
? calculateInlineImageDimensions(photo, isOwn, asForwarded, isWebPagePhoto, noAvatars, isMobile)
: calculateVideoDimensions(video!, isOwn, asForwarded, isWebPageVideo, noAvatars, isMobile);
const hasText = Boolean(getMessageText(message));
const minMediaWidth = getMinMediaWidth(hasText);

View File

@ -1,13 +1,16 @@
import type { ApiMessage } from '../../../../api/types';
import { IS_CANVAS_FILTER_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT } from '../../../../util/environment';
import { IS_CANVAS_FILTER_SUPPORTED } from '../../../../util/environment';
import { getMessageMediaThumbDataUri } from '../../../../global/helpers';
import useCanvasBlur from '../../../../hooks/useCanvasBlur';
import useAppLayout from '../../../../hooks/useAppLayout';
export default function useBlurredMediaThumbRef(message: ApiMessage, isDisabled?: boolean | string) {
const { isMobile } = useAppLayout();
return useCanvasBlur(
getMessageMediaThumbDataUri(message),
Boolean(isDisabled),
IS_SINGLE_COLUMN_LAYOUT && !IS_CANVAS_FILTER_SUPPORTED,
isMobile && !IS_CANVAS_FILTER_SUPPORTED,
);
}

View File

@ -72,6 +72,7 @@ type OwnProps = {
chatId: string;
topicId?: number;
profileState: ProfileState;
isMobile?: boolean;
onProfileStateChange: (state: ProfileState) => void;
};
@ -532,7 +533,7 @@ function buildInfiniteScrollItemSelector(resultType: string) {
}
export default memo(withGlobal<OwnProps>(
(global, { chatId, topicId }): StateProps => {
(global, { chatId, topicId, isMobile }): StateProps => {
const chat = selectChat(global, chatId);
const messagesById = selectChatMessages(global, chatId);
const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {};
@ -577,7 +578,7 @@ export default memo(withGlobal<OwnProps>(
canAddMembers,
canDeleteMembers,
currentUserId: global.currentUserId,
isRightColumnShown: selectIsRightColumnShown(global),
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isRestricted: chat?.isRestricted,
lastSyncTime: global.lastSyncTime,
activeDownloadIds,

View File

@ -38,6 +38,10 @@ import EditTopic from './EditTopic.async';
import './RightColumn.scss';
interface OwnProps {
isMobile?: boolean;
}
type StateProps = {
contentKey?: RightColumnContent;
chatId?: string;
@ -59,10 +63,11 @@ function blurSearchInput() {
}
}
const RightColumn: FC<StateProps> = ({
const RightColumn: FC<OwnProps & StateProps> = ({
contentKey,
chatId,
threadId,
isMobile,
isInsideTopic,
isChatSelected,
shouldSkipHistoryAnimations,
@ -274,6 +279,7 @@ const RightColumn: FC<StateProps> = ({
chatId={chatId!}
topicId={isInsideTopic ? threadId : undefined}
profileState={profileState}
isMobile={isMobile}
onProfileStateChange={setProfileState}
/>
);
@ -359,8 +365,8 @@ const RightColumn: FC<StateProps> = ({
);
};
export default memo(withGlobal(
(global): StateProps => {
export default memo(withGlobal<OwnProps>(
(global, { isMobile }): StateProps => {
const { chatId, threadId } = selectCurrentMessageList(global) || {};
const areActiveChatsLoaded = selectAreActiveChatsLoaded(global);
const nextManagementScreen = chatId ? global.management.byChatId[chatId]?.nextScreen : undefined;
@ -368,7 +374,7 @@ export default memo(withGlobal(
const isInsideTopic = isForum && Boolean(threadId && threadId !== MAIN_THREAD_ID);
return {
contentKey: selectRightColumnContentKey(global),
contentKey: selectRightColumnContentKey(global, isMobile),
chatId,
threadId,
isInsideTopic,

View File

@ -9,7 +9,6 @@ import { ManagementScreens, ProfileState } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types';
import { ANIMATION_END_DELAY } from '../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { debounce } from '../../util/schedulers';
import buildClassName from '../../util/buildClassName';
import {
@ -23,10 +22,11 @@ import {
import {
getCanAddContact, getCanManageTopic, isChatAdmin, isChatChannel, isUserBot, isUserId,
} from '../../global/helpers';
import { getDayStartAt } from '../../util/dateFormat';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useLang from '../../hooks/useLang';
import useFlag from '../../hooks/useFlag';
import { getDayStartAt } from '../../util/dateFormat';
import useAppLayout from '../../hooks/useAppLayout';
import SearchInput from '../ui/SearchInput';
import Button from '../ui/Button';
@ -161,6 +161,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
} = getActions();
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
const { isMobile } = useAppLayout();
const handleEditInviteClick = useCallback(() => {
setEditingExportedInvite({ chatId: chatId!, invite: currentInviteInfo! });
@ -490,7 +491,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
}
const isBackButton = (
IS_SINGLE_COLUMN_LAYOUT
isMobile
|| contentKey === HeaderContent.SharedMedia
|| contentKey === HeaderContent.MemberList
|| contentKey === HeaderContent.AddingMembers

View File

@ -1,25 +1,25 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useCallback, useMemo, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { FC } from '../../../lib/teact/teact';
import type { ApiChat, ApiExportedInvite } from '../../../api/types';
import { ManagementScreens } from '../../../types';
import { STICKER_SIZE_INVITES, TME_LINK_PREFIX } from '../../../config';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import { formatCountdown, MILLISECONDS_IN_DAY } from '../../../util/dateFormat';
import useInterval from '../../../hooks/useInterval';
import useForceUpdate from '../../../hooks/useForceUpdate';
import { getMainUsername, isChatChannel } from '../../../global/helpers';
import { selectChat } from '../../../global/selectors';
import { copyTextToClipboard } from '../../../util/clipboard';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { getServerTime } from '../../../util/serverTime';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useInterval from '../../../hooks/useInterval';
import useForceUpdate from '../../../hooks/useForceUpdate';
import useFlag from '../../../hooks/useFlag';
import { getMainUsername, isChatChannel } from '../../../global/helpers';
import useAppLayout from '../../../hooks/useAppLayout';
import ListItem from '../../ui/ListItem';
import NothingFound from '../../common/NothingFound';
@ -79,6 +79,7 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
const [revokingInvite, setRevokingInvite] = useState<ApiExportedInvite | undefined>();
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
const [deletingInvite, setDeletingInvite] = useState<ApiExportedInvite | undefined>();
const { isMobile } = useAppLayout();
useHistoryBack({
isActive,
@ -280,7 +281,7 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
return ({ onTrigger, isOpen }) => (
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
ripple={!isMobile}
size="smaller"
color="translucent"
className={isOpen ? 'active' : ''}
@ -290,7 +291,7 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
<i className="icon-more" />
</Button>
);
}, []);
}, [isMobile]);
return (
<div className="Management ManageInvites">

View File

@ -3,6 +3,7 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index';
import type { ApiMessage } from '../../../api/types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { FocusDirection } from '../../../types';
import type { GlobalState } from '../../types';
import {
ANIMATION_END_DELAY,
@ -45,8 +46,8 @@ import parseMessageInput from '../../../util/parseMessageInput';
import { getMessageSummaryText, getSenderTitle } from '../../helpers';
import * as langProvider from '../../../util/langProvider';
import { copyHtmlToClipboard } from '../../../util/clipboard';
import type { GlobalState } from '../../types';
import { renderMessageSummaryHtml } from '../../helpers/renderMessageSummaryHtml';
import { getIsMobile } from '../../../hooks/useAppLayout';
const FOCUS_DURATION = 1500;
const FOCUS_NO_HIGHLIGHT_DURATION = FAST_SMOOTH_MAX_DURATION + ANIMATION_END_DELAY;
@ -251,7 +252,7 @@ addActionHandler('closeAudioPlayer', (global) => {
addActionHandler('openPollResults', (global, actions, payload) => {
const { chatId, messageId } = payload!;
const shouldOpenInstantly = selectIsRightColumnShown(global);
const shouldOpenInstantly = selectIsRightColumnShown(global, getIsMobile());
if (!shouldOpenInstantly) {
window.setTimeout(() => {

View File

@ -4,13 +4,13 @@ import type { ApiError, ApiNotification } from '../../../api/types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { APP_VERSION, DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT } from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../../util/environment';
import getReadableErrorText from '../../../util/getReadableErrorText';
import {
selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectIsTrustedBot,
} from '../../selectors';
import generateIdFor from '../../../util/generateIdFor';
import { unique } from '../../../util/iteratees';
import { getIsMobile, getIsTablet } from '../../../hooks/useAppLayout';
export const APP_VERSION_URL = 'version.txt';
const MAX_STORED_EMOJIS = 8 * 4; // Represents four rows of recent emojis
@ -104,7 +104,7 @@ addActionHandler('closeManagement', (global) => {
});
addActionHandler('openChat', (global) => {
if (!IS_SINGLE_COLUMN_LAYOUT && !IS_TABLET_COLUMN_LAYOUT) {
if (!getIsMobile() && !getIsTablet()) {
return undefined;
}

View File

@ -23,7 +23,6 @@ import {
DEFAULT_PATTERN_COLOR,
DEFAULT_LIMITS,
} from '../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../util/environment';
import { isHeavyAnimating } from '../hooks/useHeavyAnimationCheck';
import { pick, pickTruthy, unique } from '../util/iteratees';
import {
@ -39,6 +38,7 @@ import { isUserId } from './helpers';
import { getOrderedIds } from '../util/folderManager';
import { clearGlobalForLockScreen } from './reducers';
import { encryptSession } from '../util/passcode';
import { getIsMobile } from '../hooks/useAppLayout';
const UPDATE_THROTTLE = 5000;
@ -130,7 +130,7 @@ function readCache(initialState: GlobalState): GlobalState {
...cached,
};
const parsedMessageList = !IS_SINGLE_COLUMN_LAYOUT ? parseLocationHash() : undefined;
const parsedMessageList = !getIsMobile() ? parseLocationHash() : undefined;
return {
...newState,

View File

@ -6,11 +6,11 @@ import {
MIN_LEFT_COLUMN_WIDTH,
SIDE_COLUMN_MAX_WIDTH,
} from '../../components/middle/helpers/calculateMiddleFooterTransforms';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import windowSize from '../../util/windowSize';
import { updateChat } from './chats';
import { isSameReaction, isReactionChosen } from '../helpers';
import { updateChatMessage } from './messages';
import { getIsMobile } from '../../hooks/useAppLayout';
function getLeftColumnWidth(windowWidth: number) {
if (windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN) {
@ -31,7 +31,7 @@ function getLeftColumnWidth(windowWidth: number) {
}
export function subtractXForEmojiInteraction(global: GlobalState, x: number) {
return x - ((global.isLeftColumnShown && !IS_SINGLE_COLUMN_LAYOUT)
return x - ((global.isLeftColumnShown && !getIsMobile())
? global.leftColumnWidth || getLeftColumnWidth(windowSize.get().width)
: 0);
}

View File

@ -1,7 +1,7 @@
import type { GlobalState } from '../types';
import { NewChatMembersProgress, RightColumnContent } from '../../types';
import { getSystemTheme, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { getSystemTheme } from '../../util/environment';
import {
selectCurrentMessageList, selectIsCreateTopicPanelOpen, selectIsEditTopicPanelOpen, selectIsPollResultsOpen,
} from './messages';
@ -15,14 +15,14 @@ export function selectIsMediaViewerOpen(global: GlobalState) {
return Boolean(mediaViewer.mediaId || mediaViewer.avatarOwnerId);
}
export function selectRightColumnContentKey(global: GlobalState) {
export function selectRightColumnContentKey(global: GlobalState, isMobile?: boolean) {
return selectIsEditTopicPanelOpen(global) ? (
RightColumnContent.EditTopic
) : selectIsCreateTopicPanelOpen(global) ? (
RightColumnContent.CreateTopic
) : selectIsPollResultsOpen(global) ? (
RightColumnContent.PollResults
) : !IS_SINGLE_COLUMN_LAYOUT && selectCurrentTextSearch(global) ? (
) : !isMobile && selectCurrentTextSearch(global) ? (
RightColumnContent.Search
) : selectCurrentManagement(global) ? (
RightColumnContent.Management
@ -41,8 +41,8 @@ export function selectRightColumnContentKey(global: GlobalState) {
) : undefined;
}
export function selectIsRightColumnShown(global: GlobalState) {
return selectRightColumnContentKey(global) !== undefined;
export function selectIsRightColumnShown(global: GlobalState, isMobile?: boolean) {
return selectRightColumnContentKey(global, isMobile) !== undefined;
}
export function selectTheme(global: GlobalState) {

75
src/hooks/useAppLayout.ts Normal file
View File

@ -0,0 +1,75 @@
import {
MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN,
MOBILE_SCREEN_LANDSCAPE_MAX_HEIGHT,
MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH,
MOBILE_SCREEN_MAX_WIDTH,
} from '../config';
import { useEffect } from '../lib/teact/teact';
import { IS_IOS } from '../util/environment';
import { createCallbackManager } from '../util/callbacks';
import { updateSizes } from '../util/windowSize';
import useForceUpdate from './useForceUpdate';
type MediaQueryCacheKey = 'mobile' | 'tablet' | 'landscape';
const mediaQueryCache = new Map<MediaQueryCacheKey, MediaQueryList>();
const callbacks = createCallbackManager();
let isMobile: boolean | undefined;
let isTablet: boolean | undefined;
let isLandscape: boolean | undefined;
export function getIsMobile() {
return isMobile;
}
export function getIsTablet() {
return isTablet;
}
function handleMediaQueryChange() {
isMobile = mediaQueryCache.get('mobile')?.matches || false;
isTablet = !isMobile && (mediaQueryCache.get('tablet')?.matches || false);
isLandscape = mediaQueryCache.get('landscape')?.matches || false;
updateSizes();
callbacks.runCallbacks();
}
function initMediaQueryCache() {
const mobileQuery = window.matchMedia(`(max-width: ${MOBILE_SCREEN_MAX_WIDTH}px), \
(max-width: ${MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH}px and max-height: ${MOBILE_SCREEN_LANDSCAPE_MAX_HEIGHT}px)`);
mediaQueryCache.set('mobile', mobileQuery);
mobileQuery.addEventListener('change', handleMediaQueryChange);
const tabletQuery = window.matchMedia(`(max-width: ${MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN}px)`);
mediaQueryCache.set('tablet', tabletQuery);
tabletQuery.addEventListener('change', handleMediaQueryChange);
const landscapeQuery = window.matchMedia(
IS_IOS
? '(orientation: landscape)'
// Source: https://web.archive.org/web/20160509220835/http://blog.abouthalf.com/development/orientation-media-query-challenges-in-android-browsers/
// Feature is marked as deprecated now, but it is still supported
// https://developer.mozilla.org/en-US/docs/Web/CSS/@media/device-aspect-ratio#browser_compatibility
: 'screen and (min-device-aspect-ratio: 1/1) and (orientation: landscape)',
);
mediaQueryCache.set('landscape', landscapeQuery);
landscapeQuery.addEventListener('change', handleMediaQueryChange);
}
initMediaQueryCache();
handleMediaQueryChange();
export default function useAppLayout() {
const forceUpdate = useForceUpdate();
useEffect(() => callbacks.addCallback(forceUpdate), [forceUpdate]);
return {
isMobile,
isTablet,
isLandscape,
isDesktop: !isMobile && !isTablet,
};
}

View File

@ -1,6 +1,6 @@
import type { RefObject } from 'react';
import { useEffect } from '../lib/teact/teact';
import { IS_SINGLE_COLUMN_LAYOUT } from '../util/environment';
import useAppLayout from './useAppLayout';
// Focus slows down animation, also it breaks transition layout in Chrome
const FOCUS_DELAY_MS = 500;
@ -11,9 +11,11 @@ export default function useInputFocusOnOpen(
isOpen?: boolean,
onClose?: NoneToVoidFunction,
) {
const { isMobile } = useAppLayout();
useEffect(() => {
if (isOpen) {
if (!IS_SINGLE_COLUMN_LAYOUT) {
if (!isMobile) {
setTimeout(() => {
requestAnimationFrame(() => {
if (inputRef.current?.isConnected) {
@ -31,5 +33,5 @@ export default function useInputFocusOnOpen(
setTimeout(onClose, MODAL_HIDE_DELAY_MS);
}
}
}, [inputRef, isOpen, onClose]);
}, [inputRef, isMobile, isOpen, onClose]);
}

View File

@ -1,9 +1,4 @@
import {
DPR,
IS_SINGLE_COLUMN_LAYOUT,
IS_SAFARI,
IS_ANDROID,
} from '../../util/environment';
import { DPR, IS_SAFARI, IS_ANDROID } from '../../util/environment';
import WorkerConnector from '../../util/WorkerConnector';
import { animate } from '../../util/animation';
import cycleRestrict from '../../util/cycleRestrict';
@ -14,6 +9,7 @@ interface Params {
size?: number;
quality?: number;
isLowPriority?: boolean;
isMobile?: boolean;
coords?: { x: number; y: number };
}
@ -24,7 +20,8 @@ type Frame =
| ImageBitmap;
const MAX_WORKERS = 4;
const HIGH_PRIORITY_QUALITY = IS_SINGLE_COLUMN_LAYOUT ? 0.75 : 1;
const HIGH_PRIORITY_QUALITY_MOBILE = 0.75;
const HIGH_PRIORITY_QUALITY_DESKTOP = 1;
const LOW_PRIORITY_QUALITY = IS_ANDROID ? 0.5 : 0.75;
const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24;
const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4;
@ -99,7 +96,7 @@ class RLottie {
instance = new RLottie(...args);
instancesById.set(id, instance);
} else {
instance.addContainer(container, canvas, onLoad, params?.coords);
instance.addContainer(container, canvas, onLoad, params?.coords, params?.isMobile);
}
return instance;
@ -116,7 +113,7 @@ class RLottie {
private onEnded?: (isDestroyed?: boolean) => void,
private onLoop?: () => void,
) {
this.addContainer(containerId, container, onLoad, params.coords);
this.addContainer(containerId, container, onLoad, params.coords, params.isMobile);
this.initConfig();
this.initRenderer();
}
@ -203,13 +200,18 @@ class RLottie {
this.params.noLoop = noLoop;
}
setSharedCanvasCoords(containerId: string, newCoords: Params['coords']) {
setIsMobile(isMobile?: Params['isMobile']) {
this.params.isMobile = isMobile;
}
setSharedCanvasCoords(containerId: string, newCoords: Params['coords'], isMobile?: Params['isMobile']) {
const containerInfo = this.containers.get(containerId)!;
const {
canvas, ctx,
} = containerInfo;
if (!canvas.dataset.isJustCleaned || canvas.dataset.isJustCleaned === 'false') {
this.setIsMobile(isMobile);
const sizeFactor = this.calcSizeFactor();
ensureCanvasSize(canvas, sizeFactor);
ctx.clearRect(0, 0, canvas.width, canvas.height);
@ -236,7 +238,9 @@ class RLottie {
container: HTMLDivElement | HTMLCanvasElement,
onLoad?: NoneToVoidFunction,
coords?: Params['coords'],
isMobile?: Params['isMobile'],
) {
this.setIsMobile(isMobile);
const sizeFactor = this.calcSizeFactor();
let imgSize: number;
@ -314,10 +318,11 @@ class RLottie {
const {
isLowPriority,
size,
isMobile,
// Reduced quality only looks acceptable on big enough images
quality = isLowPriority && (!size || size > LOW_PRIORITY_QUALITY_SIZE_THRESHOLD)
? LOW_PRIORITY_QUALITY
: HIGH_PRIORITY_QUALITY,
: (isMobile ? HIGH_PRIORITY_QUALITY_MOBILE : HIGH_PRIORITY_QUALITY_DESKTOP),
} = this.params;
// Reduced quality only looks acceptable on high DPR screens

View File

@ -1,8 +1,4 @@
import {
MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN,
MOBILE_SCREEN_MAX_WIDTH,
MOBILE_SCREEN_LANDSCAPE_MAX_HEIGHT,
MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH,
IS_TEST,
SUPPORTED_VIDEO_CONTENT_TYPES,
VIDEO_MOV_TYPE,
@ -53,14 +49,6 @@ export const IS_PWA = (
);
export const IS_TOUCH_ENV = window.matchMedia('(pointer: coarse)').matches;
// Keep in mind the landscape orientation
export const IS_SINGLE_COLUMN_LAYOUT = window.innerWidth <= MOBILE_SCREEN_MAX_WIDTH || (
window.innerWidth <= MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH && window.innerHeight <= MOBILE_SCREEN_LANDSCAPE_MAX_HEIGHT
);
// Special layout, 1 column while chat opened, 2 columns while collapsed
export const IS_TABLET_COLUMN_LAYOUT = !IS_SINGLE_COLUMN_LAYOUT && (
window.innerWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN
);
export const IS_VOICE_RECORDING_SUPPORTED = Boolean(
window.navigator.mediaDevices && 'getUserMedia' in window.navigator.mediaDevices && (
window.AudioContext || (window as any).webkitAudioContext

View File

@ -1,40 +1,19 @@
import { throttle } from './schedulers';
import {
MOBILE_SCREEN_LANDSCAPE_MAX_HEIGHT,
MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH,
MOBILE_SCREEN_MAX_WIDTH,
} from '../config';
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT } from './environment';
import { IS_IOS } from './environment';
type IDimensions = {
width: number;
height: number;
};
const IS_LANDSCAPE = IS_SINGLE_COLUMN_LAYOUT && isLandscape();
const WINDOW_RESIZE_THROTTLE_MS = 250;
const initialHeight = window.innerHeight;
let currentWindowSize = updateSizes();
let isRefreshDisabled = false;
function disableRefresh() {
isRefreshDisabled = true;
}
function enableRefresh() {
isRefreshDisabled = false;
}
const handleResize = throttle(() => {
currentWindowSize = updateSizes();
if (!isRefreshDisabled && (
isMobileScreen() !== IS_SINGLE_COLUMN_LAYOUT
|| (IS_SINGLE_COLUMN_LAYOUT && IS_LANDSCAPE !== isLandscape())
)) {
window.location.reload();
}
}, 250, true);
}, WINDOW_RESIZE_THROTTLE_MS, true);
window.addEventListener('orientationchange', handleResize);
if (IS_IOS) {
@ -60,29 +39,9 @@ export function updateSizes(): IDimensions {
};
}
function isMobileScreen() {
return currentWindowSize.width <= MOBILE_SCREEN_MAX_WIDTH || (
currentWindowSize.width <= MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH
&& currentWindowSize.height <= MOBILE_SCREEN_LANDSCAPE_MAX_HEIGHT
);
}
function isLandscape() {
if (IS_IOS) {
return window.matchMedia('(orientation: landscape)').matches;
}
// Source: https://web.archive.org/web/20160509220835/http://blog.abouthalf.com/development/orientation-media-query-challenges-in-android-browsers/
// Feature is marked as deprecated now, but it is still supported
// https://developer.mozilla.org/en-US/docs/Web/CSS/@media/device-aspect-ratio#browser_compatibility
return window.matchMedia('screen and (min-device-aspect-ratio: 1/1) and (orientation: landscape)').matches;
}
const windowSize = {
get: () => currentWindowSize,
getIsKeyboardVisible: () => initialHeight > currentWindowSize.height,
disableRefresh,
enableRefresh,
};
export default windowSize;