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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,13 @@ import type { FC } from '../../lib/teact/teact';
import React, { useCallback, memo } 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 { 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 { LOCAL_TGS_URLS } from './helpers/animatedAssets';
import AnimatedSticker from './AnimatedSticker'; import AnimatedSticker from './AnimatedSticker';
import useTimeout from '../../hooks/useTimeout'; import useTimeout from '../../hooks/useTimeout';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useAppLayout from '../../hooks/useAppLayout';
import './PasswordMonkey.scss'; import './PasswordMonkey.scss';
@ -21,11 +21,13 @@ const PEEK_MONKEY_SHOW_DELAY = 2000;
const SEGMENT_COVER_EYES: [number, number] = [0, 50]; const SEGMENT_COVER_EYES: [number, number] = [0, 50];
const SEGMENT_UNCOVER_EYE: [number, number] = [0, 20]; const SEGMENT_UNCOVER_EYE: [number, number] = [0, 20];
const SEGMENT_COVER_EYE: [number, number] = [20, 0]; 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 PasswordMonkey: FC<OwnProps> = ({ isPasswordVisible, isBig }) => {
const [isFirstMonkeyLoaded, markFirstMonkeyLoaded] = useFlag(false); const [isFirstMonkeyLoaded, markFirstMonkeyLoaded] = useFlag(false);
const [isPeekShown, markPeekShown] = 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); useTimeout(markPeekShown, PEEK_MONKEY_SHOW_DELAY);
const handleFirstMonkeyLoad = useCallback(markFirstMonkeyLoaded, [markFirstMonkeyLoaded]); const handleFirstMonkeyLoad = useCallback(markFirstMonkeyLoaded, [markFirstMonkeyLoaded]);
@ -36,7 +38,7 @@ const PasswordMonkey: FC<OwnProps> = ({ isPasswordVisible, isBig }) => {
<div className="monkey-preview" /> <div className="monkey-preview" />
)} )}
<AnimatedSticker <AnimatedSticker
size={isBig ? STICKER_SIZE_TWO_FA : STICKER_SIZE} size={isBig ? STICKER_SIZE_TWO_FA : stikerSize}
className={isPeekShown ? 'hidden' : 'shown'} className={isPeekShown ? 'hidden' : 'shown'}
tgsUrl={LOCAL_TGS_URLS.MonkeyClose} tgsUrl={LOCAL_TGS_URLS.MonkeyClose}
playSegment={SEGMENT_COVER_EYES} playSegment={SEGMENT_COVER_EYES}
@ -44,7 +46,7 @@ const PasswordMonkey: FC<OwnProps> = ({ isPasswordVisible, isBig }) => {
onLoad={handleFirstMonkeyLoad} onLoad={handleFirstMonkeyLoad}
/> />
<AnimatedSticker <AnimatedSticker
size={isBig ? STICKER_SIZE_TWO_FA : STICKER_SIZE} size={isBig ? STICKER_SIZE_TWO_FA : stikerSize}
className={isPeekShown ? 'shown' : 'hidden'} className={isPeekShown ? 'shown' : 'hidden'}
tgsUrl={LOCAL_TGS_URLS.MonkeyPeek} tgsUrl={LOCAL_TGS_URLS.MonkeyPeek}
playSegment={isPasswordVisible ? SEGMENT_UNCOVER_EYE : SEGMENT_COVER_EYE} 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 { FC, TeactNode } from '../../lib/teact/teact';
import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types'; 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 { import {
getChatAvatarHash, getChatAvatarHash,
getChatTitle, getChatTitle,
@ -21,6 +21,7 @@ import useLang from '../../hooks/useLang';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useMediaTransition from '../../hooks/useMediaTransition'; import useMediaTransition from '../../hooks/useMediaTransition';
import useCanvasBlur from '../../hooks/useCanvasBlur'; import useCanvasBlur from '../../hooks/useCanvasBlur';
import useAppLayout from '../../hooks/useAppLayout';
import Spinner from '../ui/Spinner'; import Spinner from '../ui/Spinner';
import OptimizedVideo from '../ui/OptimizedVideo'; import OptimizedVideo from '../ui/OptimizedVideo';
@ -50,6 +51,7 @@ const ProfilePhoto: FC<OwnProps> = ({
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const lang = useLang(); const lang = useLang();
const { isMobile } = useAppLayout();
const isDeleted = user && isDeletedUser(user); const isDeleted = user && isDeletedUser(user);
const isRepliesChat = chat && isChatWithRepliesBot(chat.id); const isRepliesChat = chat && isChatWithRepliesBot(chat.id);
@ -73,7 +75,7 @@ const ProfilePhoto: FC<OwnProps> = ({
const transitionClassNames = useMediaTransition(isFullMediaReady); const transitionClassNames = useMediaTransition(isFullMediaReady);
const isBlurredThumb = canHaveMedia && !isFullMediaReady && !avatarBlobUrl && currentPhoto?.thumbnail?.dataUri; const isBlurredThumb = canHaveMedia && !isFullMediaReady && !avatarBlobUrl && currentPhoto?.thumbnail?.dataUri;
const blurredThumbCanvasRef = useCanvasBlur( 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; 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 React, { useState, useCallback, memo } from '../../lib/teact/teact';
import { STICKER_SIZE_AUTH, STICKER_SIZE_AUTH_MOBILE, STICKER_SIZE_TWO_FA } from '../../config'; 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 { LOCAL_TGS_URLS } from './helpers/animatedAssets';
import useAppLayout from '../../hooks/useAppLayout';
import AnimatedSticker from './AnimatedSticker'; import AnimatedSticker from './AnimatedSticker';
import './PasswordMonkey.scss'; import './PasswordMonkey.scss';
@ -19,7 +20,6 @@ type OwnProps = {
const TRACKING_START_FRAME = 15; const TRACKING_START_FRAME = 15;
const TRACKING_END_FRAME = 180; const TRACKING_END_FRAME = 180;
const STICKER_SIZE = IS_SINGLE_COLUMN_LAYOUT ? STICKER_SIZE_AUTH_MOBILE : STICKER_SIZE_AUTH;
const TrackingMonkey: FC<OwnProps> = ({ const TrackingMonkey: FC<OwnProps> = ({
code, code,
@ -29,17 +29,19 @@ const TrackingMonkey: FC<OwnProps> = ({
isBig, isBig,
}) => { }) => {
const [isFirstMonkeyLoaded, setIsFirstMonkeyLoaded] = useState(false); 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), []); const handleFirstMonkeyLoad = useCallback(() => setIsFirstMonkeyLoaded(true), []);
function getTrackingFrames(): [number, number] { function getTrackingFrames(): [number, number] {
const startFrame = (code && code.length > 1) || trackingDirection < 0 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; : 0;
const endFrame = code.length === codeLength const endFrame = code.length === codeLength
? TRACKING_END_FRAME ? TRACKING_END_FRAME
: TRACKING_START_FRAME + TRACKING_FRAMES_PER_SYMBOL * code.length; : TRACKING_START_FRAME + trackningFramesPerSymbol * code.length;
if (trackingDirection < 1) { if (trackingDirection < 1) {
return [ return [
@ -60,14 +62,14 @@ const TrackingMonkey: FC<OwnProps> = ({
<div className="monkey-preview" /> <div className="monkey-preview" />
)} )}
<AnimatedSticker <AnimatedSticker
size={isBig ? STICKER_SIZE_TWO_FA : STICKER_SIZE} size={isBig ? STICKER_SIZE_TWO_FA : stickerSize}
className={isTracking ? 'hidden' : undefined} className={isTracking ? 'hidden' : undefined}
tgsUrl={LOCAL_TGS_URLS.MonkeyIdle} tgsUrl={LOCAL_TGS_URLS.MonkeyIdle}
play={!isTracking} play={!isTracking}
onLoad={handleFirstMonkeyLoad} onLoad={handleFirstMonkeyLoad}
/> />
<AnimatedSticker <AnimatedSticker
size={isBig ? STICKER_SIZE_TWO_FA : STICKER_SIZE} size={isBig ? STICKER_SIZE_TWO_FA : stickerSize}
className={!isTracking ? 'hidden' : 'shown'} className={!isTracking ? 'hidden' : 'shown'}
tgsUrl={LOCAL_TGS_URLS.MonkeyTracking} tgsUrl={LOCAL_TGS_URLS.MonkeyTracking}
playSegment={isTracking ? getTrackingFrames() : undefined} playSegment={isTracking ? getTrackingFrames() : undefined}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,9 +6,9 @@ import { getActions } from '../../../global';
import { SettingsScreens } from '../../../types'; import { SettingsScreens } from '../../../types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useMultiClick from '../../../hooks/useMultiClick'; import useMultiClick from '../../../hooks/useMultiClick';
import useAppLayout from '../../../hooks/useAppLayout';
import DropdownMenu from '../../ui/DropdownMenu'; import DropdownMenu from '../../ui/DropdownMenu';
import MenuItem from '../../ui/MenuItem'; import MenuItem from '../../ui/MenuItem';
@ -35,6 +35,7 @@ const SettingsHeader: FC<OwnProps> = ({
openDeleteChatFolderModal, openDeleteChatFolderModal,
} = getActions(); } = getActions();
const { isMobile } = useAppLayout();
const [isSignOutDialogOpen, setIsSignOutDialogOpen] = useState(false); const [isSignOutDialogOpen, setIsSignOutDialogOpen] = useState(false);
const handleMultiClick = useMultiClick(5, () => { const handleMultiClick = useMultiClick(5, () => {
@ -64,7 +65,7 @@ const SettingsHeader: FC<OwnProps> = ({
return ({ onTrigger, isOpen }) => ( return ({ onTrigger, isOpen }) => (
<Button <Button
round round
ripple={!IS_SINGLE_COLUMN_LAYOUT} ripple={!isMobile}
size="smaller" size="smaller"
color="translucent" color="translucent"
className={isOpen ? 'active' : ''} className={isOpen ? 'active' : ''}
@ -74,7 +75,7 @@ const SettingsHeader: FC<OwnProps> = ({
<i className="icon-more" /> <i className="icon-more" />
</Button> </Button>
); );
}, []); }, [isMobile]);
const lang = useLang(); const lang = useLang();
@ -240,7 +241,7 @@ const SettingsHeader: FC<OwnProps> = ({
<Button <Button
round round
ripple={!IS_SINGLE_COLUMN_LAYOUT} ripple={!isMobile}
size="smaller" size="smaller"
color="translucent" color="translucent"
// eslint-disable-next-line react/jsx-no-bind // 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 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 { selectAnimatedEmoji } from '../../../../global/selectors';
import useLang from '../../../../hooks/useLang'; import useLang from '../../../../hooks/useLang';
import useHistoryBack from '../../../../hooks/useHistoryBack'; import useHistoryBack from '../../../../hooks/useHistoryBack';
import useAppLayout from '../../../../hooks/useAppLayout';
import InputText from '../../../ui/InputText'; import InputText from '../../../ui/InputText';
import Loading from '../../../ui/Loading'; import Loading from '../../../ui/Loading';
@ -29,7 +30,6 @@ type StateProps = {
codeLength: number; codeLength: number;
}; };
const FOCUS_DELAY_TIMEOUT_MS = IS_SINGLE_COLUMN_LAYOUT ? 550 : 400;
const ICON_SIZE = 160; const ICON_SIZE = 160;
const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
@ -44,6 +44,8 @@ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const { isMobile } = useAppLayout();
const focusDelayTimeoutMs = isMobile ? 550 : 400;
const [value, setValue] = useState<string>(''); const [value, setValue] = useState<string>('');
@ -51,9 +53,9 @@ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
if (!IS_TOUCH_ENV) { if (!IS_TOUCH_ENV) {
setTimeout(() => { setTimeout(() => {
inputRef.current!.focus(); inputRef.current!.focus();
}, FOCUS_DELAY_TIMEOUT_MS); }, focusDelayTimeoutMs);
} }
}, []); }, [focusDelayTimeoutMs]);
const lang = useLang(); const lang = useLang();

View File

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

View File

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

View File

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

View File

@ -2,12 +2,15 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact'; import React, { memo } from '../../lib/teact/teact';
import { Bundles } from '../../util/moduleLoader'; import { Bundles } from '../../util/moduleLoader';
import type { OwnProps } from './Main';
import useModuleLoader from '../../hooks/useModuleLoader'; import useModuleLoader from '../../hooks/useModuleLoader';
const MainAsync: FC = () => { const MainAsync: FC<OwnProps> = (props) => {
const Main = useModuleLoader(Bundles.Main, 'Main'); 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); export default memo(MainAsync);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,9 +13,7 @@ import type { IAnchorPosition } from '../../types';
import { ManagementScreens } from '../../types'; import { ManagementScreens } from '../../types';
import { ANIMATION_LEVEL_MIN } from '../../config'; import { ANIMATION_LEVEL_MIN } from '../../config';
import { import { ARE_CALLS_SUPPORTED, IS_PWA } from '../../util/environment';
ARE_CALLS_SUPPORTED, IS_PWA, IS_SINGLE_COLUMN_LAYOUT,
} from '../../util/environment';
import { import {
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId, isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
} from '../../global/helpers'; } from '../../global/helpers';
@ -40,6 +38,7 @@ interface OwnProps {
messageListType: MessageListType; messageListType: MessageListType;
canExpandActions: boolean; canExpandActions: boolean;
withForumActions?: boolean; withForumActions?: boolean;
isMobile?: boolean;
onTopicSearch?: NoneToVoidFunction; onTopicSearch?: NoneToVoidFunction;
} }
@ -70,6 +69,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
chatId, chatId,
threadId, threadId,
noMenu, noMenu,
isMobile,
isChannel, isChannel,
canStartBot, canStartBot,
canRestartBot, canRestartBot,
@ -149,7 +149,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
openLocalTextSearch(); openLocalTextSearch();
if (IS_SINGLE_COLUMN_LAYOUT) { if (isMobile) {
// iOS requires synchronous focus on user event. // iOS requires synchronous focus on user event.
const searchInput = document.querySelector<HTMLInputElement>('#MobileSearch input')!; const searchInput = document.querySelector<HTMLInputElement>('#MobileSearch input')!;
searchInput.focus(); searchInput.focus();
@ -161,7 +161,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
} else { } else {
setTimeout(setFocusInSearchInput, SEARCH_FOCUS_DELAY_MS); setTimeout(setFocusInSearchInput, SEARCH_FOCUS_DELAY_MS);
} }
}, [noAnimation, onTopicSearch, openLocalTextSearch, withForumActions]); }, [isMobile, noAnimation, onTopicSearch, openLocalTextSearch, withForumActions]);
const handleAsMessagesClick = useCallback(() => { const handleAsMessagesClick = useCallback(() => {
openChat({ id: chatId, threadId: MAIN_THREAD_ID }); openChat({ id: chatId, threadId: MAIN_THREAD_ID });
@ -186,7 +186,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
return ( return (
<div className="HeaderActions"> <div className="HeaderActions">
{!IS_SINGLE_COLUMN_LAYOUT && ( {!isMobile && (
<> <>
{canExpandActions && !shouldSendJoinRequest && (canSubscribe || shouldJoinToSend) && ( {canExpandActions && !shouldSendJoinRequest && (canSubscribe || shouldJoinToSend) && (
<Button <Button
@ -272,7 +272,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
ref={menuButtonRef} ref={menuButtonRef}
className={isMenuOpen ? 'active' : ''} className={isMenuOpen ? 'active' : ''}
round round
ripple={!IS_SINGLE_COLUMN_LAYOUT} ripple={!isMobile}
size="smaller" size="smaller"
color="translucent" color="translucent"
disabled={noMenu} disabled={noMenu}
@ -287,7 +287,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
threadId={threadId} threadId={threadId}
isOpen={isMenuOpen} isOpen={isMenuOpen}
anchor={menuPosition} anchor={menuPosition}
withExtraActions={IS_SINGLE_COLUMN_LAYOUT || !canExpandActions} withExtraActions={isMobile || !canExpandActions}
isChannel={isChannel} isChannel={isChannel}
canStartBot={canStartBot} canStartBot={canStartBot}
canRestartBot={canRestartBot} canRestartBot={canRestartBot}
@ -314,7 +314,9 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
}; };
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, messageListType }): StateProps => { (global, {
chatId, threadId, messageListType, isMobile,
}): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const isChannel = Boolean(chat && isChatChannel(chat)); const isChannel = Boolean(chat && isChatChannel(chat));
@ -328,7 +330,7 @@ export default memo(withGlobal<OwnProps>(
const isChatWithSelf = selectIsChatWithSelf(global, chatId); const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID; const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID;
const isDiscussionThread = 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 canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
const canStartBot = !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId)); 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 { MAIN_THREAD_ID } from '../../api/types';
import { REPLIES_USER_ID } from '../../config'; import { REPLIES_USER_ID } from '../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { disableScrolling, enableScrolling } from '../../util/scrollLock'; import { disableScrolling, enableScrolling } from '../../util/scrollLock';
import { import {
selectChat, selectChat,
@ -34,6 +33,7 @@ import {
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation'; import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useAppLayout from '../../hooks/useAppLayout';
import Portal from '../ui/Portal'; import Portal from '../ui/Portal';
import Menu from '../ui/Menu'; import Menu from '../ui/Menu';
@ -163,6 +163,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
openChat, openChat,
} = getActions(); } = getActions();
const { isMobile } = useAppLayout();
const [isMenuOpen, setIsMenuOpen] = useState(true); const [isMenuOpen, setIsMenuOpen] = useState(true);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isReportModalOpen, setIsReportModalOpen] = useState(false); const [isReportModalOpen, setIsReportModalOpen] = useState(false);
@ -361,7 +362,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
{lang('lng_forum_topic_edit')} {lang('lng_forum_topic_edit')}
</MenuItem> </MenuItem>
)} )}
{IS_SINGLE_COLUMN_LAYOUT && !withForumActions && isForum && !isTopic && ( {isMobile && !withForumActions && isForum && !isTopic && (
<MenuItem <MenuItem
icon="forums" icon="forums"
onClick={handleViewAsTopicsClick} onClick={handleViewAsTopicsClick}
@ -418,7 +419,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
{lang('AddContact')} {lang('AddContact')}
</MenuItem> </MenuItem>
)} )}
{IS_SINGLE_COLUMN_LAYOUT && canCall && ( {isMobile && canCall && (
<MenuItem <MenuItem
icon="phone" icon="phone"
onClick={handleCall} onClick={handleCall}
@ -434,7 +435,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
{lang('VideoCall')} {lang('VideoCall')}
</MenuItem> </MenuItem>
)} )}
{IS_SINGLE_COLUMN_LAYOUT && canSearch && ( {isMobile && canSearch && (
<MenuItem <MenuItem
icon="search" icon="search"
onClick={handleSearch} onClick={handleSearch}

View File

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

View File

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

View File

@ -14,7 +14,6 @@ import {
SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_IMAGE_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES,
} from '../../../config'; } from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import captureEscKeyListener from '../../../util/captureEscKeyListener'; import captureEscKeyListener from '../../../util/captureEscKeyListener';
import getFilesFromDataTransferItems from './helpers/getFilesFromDataTransferItems'; import getFilesFromDataTransferItems from './helpers/getFilesFromDataTransferItems';
import { getHtmlTextLength } from './helpers/getHtmlTextLength'; import { getHtmlTextLength } from './helpers/getHtmlTextLength';
@ -117,6 +116,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
const hideTimeoutRef = useRef<number>(); const hideTimeoutRef = useRef<number>();
const prevAttachments = usePrevious(attachments); const prevAttachments = usePrevious(attachments);
const renderingAttachments = attachments.length ? attachments : prevAttachments; const renderingAttachments = attachments.length ? attachments : prevAttachments;
const { isMobile } = useAppLayout();
const [shouldSendCompressed, setShouldSendCompressed] = useState( const [shouldSendCompressed, setShouldSendCompressed] = useState(
shouldSuggestCompression ?? attachmentSettings.shouldCompress, shouldSuggestCompression ?? attachmentSettings.shouldCompress,
@ -312,7 +312,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
return ({ onTrigger, isOpen: isMenuOpen }) => ( return ({ onTrigger, isOpen: isMenuOpen }) => (
<Button <Button
round round
ripple={!IS_SINGLE_COLUMN_LAYOUT} ripple={!isMobile}
size="smaller" size="smaller"
color="translucent" color="translucent"
className={isMenuOpen ? 'active' : ''} className={isMenuOpen ? 'active' : ''}
@ -322,7 +322,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
<i className="icon-more" /> <i className="icon-more" />
</Button> </Button>
); );
}, []); }, [isMobile]);
const leftChars = useMemo(() => { const leftChars = useMemo(() => {
const captionLeftBeforeLimit = captionLimit - getHtmlTextLength(caption); 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 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 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 useMouseInside from '../../../hooks/useMouseInside';
import useAppLayout from '../../../hooks/useAppLayout';
import Menu from '../../ui/Menu'; import Menu from '../../ui/Menu';
import BotCommand from './BotCommand'; import BotCommand from './BotCommand';
import './BotCommandMenu.scss'; import './BotCommandMenu.scss';
import { getActions } from '../../../global';
export type OwnProps = { export type OwnProps = {
isOpen: boolean; isOpen: boolean;
@ -22,8 +23,9 @@ const BotCommandMenu: FC<OwnProps> = ({
isOpen, botCommands, onClose, isOpen, botCommands, onClose,
}) => { }) => {
const { sendBotCommand } = getActions(); 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) => { const handleClick = useCallback((botCommand: ApiBotCommand) => {
sendBotCommand({ sendBotCommand({

View File

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

View File

@ -1,15 +1,16 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useRef } 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 { RECENT_SYMBOL_SET_ID } from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import windowSize from '../../../util/windowSize'; import windowSize from '../../../util/windowSize';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { useOnIntersect } from '../../../hooks/useIntersectionObserver'; import { useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useMediaTransition from '../../../hooks/useMediaTransition'; import useMediaTransition from '../../../hooks/useMediaTransition';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import EmojiButton from './EmojiButton'; import EmojiButton from './EmojiButton';
@ -38,8 +39,9 @@ const EmojiCategory: FC<OwnProps> = ({
const transitionClassNames = useMediaTransition(shouldRender); const transitionClassNames = useMediaTransition(shouldRender);
const lang = useLang(); 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)) ? Math.floor((windowSize.get().width - MOBILE_CONTAINER_PADDING) / (EMOJI_SIZE + EMOJI_MARGIN))
: EMOJIS_PER_ROW_ON_DESKTOP; : EMOJIS_PER_ROW_ON_DESKTOP;
const height = Math.ceil(category.emojis.length / emojisPerRow) * (EMOJI_SIZE + EMOJI_MARGIN); 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, { import React, {
useState, useEffect, memo, useRef, useMemo, useCallback, useState, useEffect, memo, useRef, useMemo, useCallback,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { withGlobal } from '../../../global'; import { withGlobal } from '../../../global';
import type { FC } from '../../../lib/teact/teact';
import type { GlobalState } from '../../../global/types'; 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 { import type {
EmojiModule, EmojiModule,
EmojiRawData, EmojiRawData,
EmojiData, EmojiData,
} from '../../../util/emoji'; } from '../../../util/emoji';
import {
uncompressEmoji, import { MENU_TRANSITION_DURATION, RECENT_SYMBOL_SET_ID } from '../../../config';
} from '../../../util/emoji'; 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 fastSmoothScroll from '../../../util/fastSmoothScroll';
import { pick } from '../../../util/iteratees'; import { pick } from '../../../util/iteratees';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -25,6 +23,7 @@ import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll'; import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
@ -79,6 +78,7 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
const [categories, setCategories] = useState<EmojiCategoryData[]>(); const [categories, setCategories] = useState<EmojiCategoryData[]>();
const [emojis, setEmojis] = useState<AllEmojis>(); const [emojis, setEmojis] = useState<AllEmojis>();
const [activeCategoryIndex, setActiveCategoryIndex] = useState(0); const [activeCategoryIndex, setActiveCategoryIndex] = useState(0);
const { isMobile } = useAppLayout();
const { observe: observeIntersection } = useIntersectionObserver({ const { observe: observeIntersection } = useIntersectionObserver({
rootRef: containerRef, rootRef: containerRef,
@ -105,7 +105,7 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
setActiveCategoryIndex(intersectingWithIndexes[Math.floor(intersectingWithIndexes.length / 2)].index); 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 // Scroll header when active set updates
useEffect(() => { useEffect(() => {

View File

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

View File

@ -12,7 +12,6 @@ import {
DEFAULT_TOPIC_ICON_STICKER_ID, DEFAULT_TOPIC_ICON_STICKER_ID,
EMOJI_SIZE_PICKER, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER, EMOJI_SIZE_PICKER, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER,
} from '../../../config'; } from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../../global/selectors'; import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../../global/selectors';
@ -21,6 +20,7 @@ import useFlag from '../../../hooks/useFlag';
import useMediaTransition from '../../../hooks/useMediaTransition'; import useMediaTransition from '../../../hooks/useMediaTransition';
import useResizeObserver from '../../../hooks/useResizeObserver'; import useResizeObserver from '../../../hooks/useResizeObserver';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useAppLayout from '../../../hooks/useAppLayout';
import StickerButton from '../../common/StickerButton'; import StickerButton from '../../common/StickerButton';
import ConfirmDialog from '../../ui/ConfirmDialog'; import ConfirmDialog from '../../ui/ConfirmDialog';
@ -46,10 +46,6 @@ type OwnProps = {
onStickerRemoveRecent?: (sticker: ApiSticker) => void; 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 ITEMS_PER_ROW_FALLBACK = 8;
const StickerSet: FC<OwnProps> = ({ const StickerSet: FC<OwnProps> = ({
@ -85,8 +81,9 @@ const StickerSet: FC<OwnProps> = ({
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null); const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
const lang = useLang(); const lang = useLang();
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
const { isMobile } = useAppLayout();
const [itemsPerRow, setItemsPerRow] = useState(ITEMS_PER_ROW_FALLBACK); const [itemsPerRow, setItemsPerRow] = useState(ITEMS_PER_ROW_FALLBACK);
@ -94,6 +91,9 @@ const StickerSet: FC<OwnProps> = ({
const transitionClassNames = useMediaTransition(shouldRender); 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 isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
const isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID; const isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID;
const isEmoji = stickerSet.isEmoji; const isEmoji = stickerSet.isEmoji;
@ -132,13 +132,13 @@ const StickerSet: FC<OwnProps> = ({
}, [onStickerSelect]); }, [onStickerSelect]);
const itemSize = isEmoji ? EMOJI_SIZE_PICKER : STICKER_SIZE_PICKER; 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) => { const calculateItemsPerRow = useCallback((width: number) => {
if (!width) return ITEMS_PER_ROW_FALLBACK; if (!width) return ITEMS_PER_ROW_FALLBACK;
return Math.floor((width - CONTAINER_PADDING) / (itemSize + margin)); return Math.floor((width - containerPaddingPx) / (itemSize + margin));
}, [itemSize, margin]); }, [containerPaddingPx, itemSize, margin]);
const handleResize = useCallback((entry: ResizeObserverEntry) => { const handleResize = useCallback((entry: ResizeObserverEntry) => {
setItemsPerRow(calculateItemsPerRow(entry.contentRect.width)); 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 { ApiSticker, ApiVideo } from '../../../api/types';
import type { GlobalActions } from '../../../global/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 { fastRaf } from '../../../util/schedulers';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { selectIsCurrentUserPremium } from '../../../global/selectors'; import { selectIsCurrentUserPremium } from '../../../global/selectors';
@ -15,6 +15,7 @@ import { selectIsCurrentUserPremium } from '../../../global/selectors';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import useMouseInside from '../../../hooks/useMouseInside'; import useMouseInside from '../../../hooks/useMouseInside';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useAppLayout from '../../../hooks/useAppLayout';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Menu from '../../ui/Menu'; import Menu from '../../ui/Menu';
@ -86,8 +87,9 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
const [activeTab, setActiveTab] = useState<number>(0); const [activeTab, setActiveTab] = useState<number>(0);
const [recentEmojis, setRecentEmojis] = useState<string[]>([]); const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
const [recentCustomEmojis, setRecentCustomEmojis] = 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); const { shouldRender, transitionClassNames } = useShowTransition(isOpen, onClose, false, false);
if (!isActivated && isOpen) { if (!isActivated && isOpen) {
@ -107,7 +109,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
}, [isCurrentUserPremium, lastSyncTime, loadFeaturedEmojiStickers, loadPremiumSetStickers]); }, [isCurrentUserPremium, lastSyncTime, loadFeaturedEmojiStickers, loadPremiumSetStickers]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!IS_SINGLE_COLUMN_LAYOUT) { if (!isMobile) {
return undefined; return undefined;
} }
@ -126,7 +128,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
}); });
} }
}; };
}, [isOpen]); }, [isMobile, isOpen]);
const recentEmojisRef = useRef(recentEmojis); const recentEmojisRef = useRef(recentEmojis);
recentEmojisRef.current = recentEmojis; recentEmojisRef.current = recentEmojis;
@ -239,7 +241,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
</Transition> </Transition>
)} )}
</div> </div>
{IS_SINGLE_COLUMN_LAYOUT && ( {isMobile && (
<Button <Button
round round
faded faded
@ -261,7 +263,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
</> </>
); );
if (IS_SINGLE_COLUMN_LAYOUT) { if (isMobile) {
if (!shouldRender) { if (!shouldRender) {
return undefined; return undefined;
} }

View File

@ -19,6 +19,7 @@ import { REM } from '../../../common/helpers/mediaDimensions';
import useResizeObserver from '../../../../hooks/useResizeObserver'; import useResizeObserver from '../../../../hooks/useResizeObserver';
import useBackgroundMode from '../../../../hooks/useBackgroundMode'; import useBackgroundMode from '../../../../hooks/useBackgroundMode';
import useAppLayout from '../../../../hooks/useAppLayout';
const SIZE = 1.25 * REM; const SIZE = 1.25 * REM;
@ -26,7 +27,7 @@ type CustomEmojiPlayer = {
play: () => void; play: () => void;
pause: () => void; pause: () => void;
destroy: () => void; destroy: () => void;
updatePosition: (x: number, y: number) => void; updatePosition: (x: number, y: number, isMobile?: boolean) => void;
}; };
export default function useInputCustomEmojis( export default function useInputCustomEmojis(
@ -38,6 +39,8 @@ export default function useInputCustomEmojis(
) { ) {
const mapRef = useRef<Map<string, CustomEmojiPlayer>>(new Map()); const mapRef = useRef<Map<string, CustomEmojiPlayer>>(new Map());
const { isMobile } = useAppLayout();
const removeContainers = useCallback((ids: string[]) => { const removeContainers = useCallback((ids: string[]) => {
ids.forEach((id) => { ids.forEach((id) => {
const player = mapRef.current.get(id); const player = mapRef.current.get(id);
@ -74,7 +77,7 @@ export default function useInputCustomEmojis(
if (mapRef.current.has(id)) { if (mapRef.current.has(id)) {
const player = mapRef.current.get(id)!; const player = mapRef.current.get(id)!;
player.updatePosition(x, y); player.updatePosition(x, y, isMobile);
return; return;
} }
@ -93,6 +96,7 @@ export default function useInputCustomEmojis(
mediaUrl, mediaUrl,
isHq, isHq,
position: { x, y }, position: { x, y },
isMobile,
}); });
animation.play(); animation.play();
@ -100,7 +104,7 @@ export default function useInputCustomEmojis(
}); });
removeContainers(Array.from(removedContainers)); removeContainers(Array.from(removedContainers));
}, [absoluteContainerRef, inputRef, removeContainers, sharedCanvasHqRef, sharedCanvasRef]); }, [absoluteContainerRef, inputRef, isMobile, removeContainers, sharedCanvasHqRef, sharedCanvasRef]);
useEffect(() => { useEffect(() => {
addCustomEmojiInputRenderCallback(synchronizeElements); addCustomEmojiInputRenderCallback(synchronizeElements);
@ -152,6 +156,7 @@ function createPlayer({
mediaUrl, mediaUrl,
position, position,
isHq, isHq,
isMobile,
} : { } : {
customEmoji: ApiSticker; customEmoji: ApiSticker;
sharedCanvasRef: React.RefObject<HTMLCanvasElement>; sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
@ -161,6 +166,7 @@ function createPlayer({
mediaUrl: string; mediaUrl: string;
position: { x: number; y: number }; position: { x: number; y: number };
isHq?: boolean; isHq?: boolean;
isMobile?: boolean;
}): CustomEmojiPlayer { }): CustomEmojiPlayer {
if (customEmoji.isLottie) { if (customEmoji.isLottie) {
const lottie = RLottie.init( const lottie = RLottie.init(
@ -173,13 +179,16 @@ function createPlayer({
size: SIZE, size: SIZE,
coords: position, coords: position,
isLowPriority: !isHq, isLowPriority: !isHq,
isMobile,
}, },
); );
return { return {
play: () => lottie.play(), play: () => lottie.play(),
pause: () => lottie.pause(), pause: () => lottie.pause(),
destroy: () => lottie.removeContainer(uniqueId), 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 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 { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useBackgroundMode from '../../../hooks/useBackgroundMode'; import useBackgroundMode from '../../../hooks/useBackgroundMode';
import useAppLayout from '../../../hooks/useAppLayout';
const INTERSECTION_THROTTLE_FOR_READING = 150; const INTERSECTION_THROTTLE_FOR_READING = 150;
const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350; const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350;
const INTERSECTION_MARGIN_FOR_LOADING = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500;
export default function useMessageObservers( export default function useMessageObservers(
type: MessageListType, type: MessageListType,
@ -18,6 +18,9 @@ export default function useMessageObservers(
) { ) {
const { markMessageListRead, markMentionsRead, animateUnreadReaction } = getActions(); const { markMessageListRead, markMentionsRead, animateUnreadReaction } = getActions();
const { isMobile } = useAppLayout();
const INTERSECTION_MARGIN_FOR_LOADING = isMobile ? 300 : 500;
const { const {
observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading, observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading,
} = useIntersectionObserver({ } = useIntersectionObserver({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,16 @@
import type { ApiMessage } from '../../../../api/types'; 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 { getMessageMediaThumbDataUri } from '../../../../global/helpers';
import useCanvasBlur from '../../../../hooks/useCanvasBlur'; import useCanvasBlur from '../../../../hooks/useCanvasBlur';
import useAppLayout from '../../../../hooks/useAppLayout';
export default function useBlurredMediaThumbRef(message: ApiMessage, isDisabled?: boolean | string) { export default function useBlurredMediaThumbRef(message: ApiMessage, isDisabled?: boolean | string) {
const { isMobile } = useAppLayout();
return useCanvasBlur( return useCanvasBlur(
getMessageMediaThumbDataUri(message), getMessageMediaThumbDataUri(message),
Boolean(isDisabled), 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; chatId: string;
topicId?: number; topicId?: number;
profileState: ProfileState; profileState: ProfileState;
isMobile?: boolean;
onProfileStateChange: (state: ProfileState) => void; onProfileStateChange: (state: ProfileState) => void;
}; };
@ -532,7 +533,7 @@ function buildInfiniteScrollItemSelector(resultType: string) {
} }
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId, topicId }): StateProps => { (global, { chatId, topicId, isMobile }): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const messagesById = selectChatMessages(global, chatId); const messagesById = selectChatMessages(global, chatId);
const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {}; const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {};
@ -577,7 +578,7 @@ export default memo(withGlobal<OwnProps>(
canAddMembers, canAddMembers,
canDeleteMembers, canDeleteMembers,
currentUserId: global.currentUserId, currentUserId: global.currentUserId,
isRightColumnShown: selectIsRightColumnShown(global), isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isRestricted: chat?.isRestricted, isRestricted: chat?.isRestricted,
lastSyncTime: global.lastSyncTime, lastSyncTime: global.lastSyncTime,
activeDownloadIds, activeDownloadIds,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import type { GlobalState } from '../types'; import type { GlobalState } from '../types';
import { NewChatMembersProgress, RightColumnContent } from '../../types'; import { NewChatMembersProgress, RightColumnContent } from '../../types';
import { getSystemTheme, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; import { getSystemTheme } from '../../util/environment';
import { import {
selectCurrentMessageList, selectIsCreateTopicPanelOpen, selectIsEditTopicPanelOpen, selectIsPollResultsOpen, selectCurrentMessageList, selectIsCreateTopicPanelOpen, selectIsEditTopicPanelOpen, selectIsPollResultsOpen,
} from './messages'; } from './messages';
@ -15,14 +15,14 @@ export function selectIsMediaViewerOpen(global: GlobalState) {
return Boolean(mediaViewer.mediaId || mediaViewer.avatarOwnerId); return Boolean(mediaViewer.mediaId || mediaViewer.avatarOwnerId);
} }
export function selectRightColumnContentKey(global: GlobalState) { export function selectRightColumnContentKey(global: GlobalState, isMobile?: boolean) {
return selectIsEditTopicPanelOpen(global) ? ( return selectIsEditTopicPanelOpen(global) ? (
RightColumnContent.EditTopic RightColumnContent.EditTopic
) : selectIsCreateTopicPanelOpen(global) ? ( ) : selectIsCreateTopicPanelOpen(global) ? (
RightColumnContent.CreateTopic RightColumnContent.CreateTopic
) : selectIsPollResultsOpen(global) ? ( ) : selectIsPollResultsOpen(global) ? (
RightColumnContent.PollResults RightColumnContent.PollResults
) : !IS_SINGLE_COLUMN_LAYOUT && selectCurrentTextSearch(global) ? ( ) : !isMobile && selectCurrentTextSearch(global) ? (
RightColumnContent.Search RightColumnContent.Search
) : selectCurrentManagement(global) ? ( ) : selectCurrentManagement(global) ? (
RightColumnContent.Management RightColumnContent.Management
@ -41,8 +41,8 @@ export function selectRightColumnContentKey(global: GlobalState) {
) : undefined; ) : undefined;
} }
export function selectIsRightColumnShown(global: GlobalState) { export function selectIsRightColumnShown(global: GlobalState, isMobile?: boolean) {
return selectRightColumnContentKey(global) !== undefined; return selectRightColumnContentKey(global, isMobile) !== undefined;
} }
export function selectTheme(global: GlobalState) { 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 type { RefObject } from 'react';
import { useEffect } from '../lib/teact/teact'; 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 // Focus slows down animation, also it breaks transition layout in Chrome
const FOCUS_DELAY_MS = 500; const FOCUS_DELAY_MS = 500;
@ -11,9 +11,11 @@ export default function useInputFocusOnOpen(
isOpen?: boolean, isOpen?: boolean,
onClose?: NoneToVoidFunction, onClose?: NoneToVoidFunction,
) { ) {
const { isMobile } = useAppLayout();
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
if (!IS_SINGLE_COLUMN_LAYOUT) { if (!isMobile) {
setTimeout(() => { setTimeout(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (inputRef.current?.isConnected) { if (inputRef.current?.isConnected) {
@ -31,5 +33,5 @@ export default function useInputFocusOnOpen(
setTimeout(onClose, MODAL_HIDE_DELAY_MS); setTimeout(onClose, MODAL_HIDE_DELAY_MS);
} }
} }
}, [inputRef, isOpen, onClose]); }, [inputRef, isMobile, isOpen, onClose]);
} }

View File

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

View File

@ -1,8 +1,4 @@
import { import {
MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN,
MOBILE_SCREEN_MAX_WIDTH,
MOBILE_SCREEN_LANDSCAPE_MAX_HEIGHT,
MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH,
IS_TEST, IS_TEST,
SUPPORTED_VIDEO_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES,
VIDEO_MOV_TYPE, VIDEO_MOV_TYPE,
@ -53,14 +49,6 @@ export const IS_PWA = (
); );
export const IS_TOUCH_ENV = window.matchMedia('(pointer: coarse)').matches; 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( export const IS_VOICE_RECORDING_SUPPORTED = Boolean(
window.navigator.mediaDevices && 'getUserMedia' in window.navigator.mediaDevices && ( window.navigator.mediaDevices && 'getUserMedia' in window.navigator.mediaDevices && (
window.AudioContext || (window as any).webkitAudioContext window.AudioContext || (window as any).webkitAudioContext

View File

@ -1,40 +1,19 @@
import { throttle } from './schedulers'; import { throttle } from './schedulers';
import { import { IS_IOS } from './environment';
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';
type IDimensions = { type IDimensions = {
width: number; width: number;
height: number; height: number;
}; };
const IS_LANDSCAPE = IS_SINGLE_COLUMN_LAYOUT && isLandscape(); const WINDOW_RESIZE_THROTTLE_MS = 250;
const initialHeight = window.innerHeight; const initialHeight = window.innerHeight;
let currentWindowSize = updateSizes(); let currentWindowSize = updateSizes();
let isRefreshDisabled = false;
function disableRefresh() {
isRefreshDisabled = true;
}
function enableRefresh() {
isRefreshDisabled = false;
}
const handleResize = throttle(() => { const handleResize = throttle(() => {
currentWindowSize = updateSizes(); currentWindowSize = updateSizes();
}, WINDOW_RESIZE_THROTTLE_MS, true);
if (!isRefreshDisabled && (
isMobileScreen() !== IS_SINGLE_COLUMN_LAYOUT
|| (IS_SINGLE_COLUMN_LAYOUT && IS_LANDSCAPE !== isLandscape())
)) {
window.location.reload();
}
}, 250, true);
window.addEventListener('orientationchange', handleResize); window.addEventListener('orientationchange', handleResize);
if (IS_IOS) { 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 = { const windowSize = {
get: () => currentWindowSize, get: () => currentWindowSize,
getIsKeyboardVisible: () => initialHeight > currentWindowSize.height, getIsKeyboardVisible: () => initialHeight > currentWindowSize.height,
disableRefresh,
enableRefresh,
}; };
export default windowSize; export default windowSize;