Left Column: Support resizing; Better support for large displays (#1484)

This commit is contained in:
Alexander Zinchuk 2021-10-13 14:38:34 +03:00
parent 03ea0cb152
commit 5180eb0d2e
16 changed files with 296 additions and 110 deletions

View File

@ -1 +1 @@
> 2%, last 2 edge versions
> 2%, last 2 edge versions, iOS >= 13.4, firefox >= 68, firefoxandroid >= 68, last 2 safari major versions

51
package-lock.json generated
View File

@ -5329,17 +5329,44 @@
"dev": true
},
"autoprefixer": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.1.tgz",
"integrity": "sha512-L8AmtKzdiRyYg7BUXJTzigmhbQRCXFKz6SA1Lqo0+AR2FBbQ4aTAPFSDlOutnFkjhiz8my4agGXog1xlMjPJ6A==",
"version": "10.3.7",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.7.tgz",
"integrity": "sha512-EmGpu0nnQVmMhX8ROoJ7Mx8mKYPlcUHuxkwrRYEYMz85lu7H09v8w6R1P0JPdn/hKU32GjpLBFEOuIlDWCRWvg==",
"dev": true,
"requires": {
"browserslist": "^4.16.6",
"caniuse-lite": "^1.0.30001243",
"colorette": "^1.2.2",
"browserslist": "^4.17.3",
"caniuse-lite": "^1.0.30001264",
"fraction.js": "^4.1.1",
"normalize-range": "^0.1.2",
"picocolors": "^0.2.1",
"postcss-value-parser": "^4.1.0"
},
"dependencies": {
"browserslist": {
"version": "4.17.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.3.tgz",
"integrity": "sha512-59IqHJV5VGdcJZ+GZ2hU5n4Kv3YiASzW6Xk5g9tf5a/MAzGeFwgGWU39fVzNIOVcgB3+Gp+kiQu0HEfTVU/3VQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001264",
"electron-to-chromium": "^1.3.857",
"escalade": "^3.1.1",
"node-releases": "^1.1.77",
"picocolors": "^0.2.1"
}
},
"electron-to-chromium": {
"version": "1.3.864",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.864.tgz",
"integrity": "sha512-v4rbad8GO6/yVI92WOeU9Wgxc4NA0n4f6P1FvZTY+jyY7JHEhw3bduYu60v3Q1h81Cg6eo4ApZrFPuycwd5hGw==",
"dev": true
},
"node-releases": {
"version": "1.1.77",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz",
"integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==",
"dev": true
}
}
},
"aws-sign2": {
@ -6085,9 +6112,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001249",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz",
"integrity": "sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw==",
"version": "1.0.30001265",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz",
"integrity": "sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw==",
"dev": true
},
"caseless": {
@ -15697,6 +15724,12 @@
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true
},
"picocolors": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
"integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
"dev": true
},
"picomatch": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.1.tgz",

View File

@ -53,7 +53,7 @@
"@typescript-eslint/eslint-plugin": "^4.29.1",
"@typescript-eslint/parser": "^4.29.1",
"@webpack-cli/serve": "^1.5.1",
"autoprefixer": "^10.3.1",
"autoprefixer": "^10.3.7",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"browserlist": "^1.0.1",

View File

@ -11,16 +11,33 @@
right: 0;
margin: 0 auto;
width: 100%;
max-width: 1680px;
max-width: 1920px;
height: 100%;
z-index: var(--z-ui-loader-mask);
display: flex;
@media (min-width: 600px) {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 100%;
}
.left {
flex: 1;
background: var(--color-background);
min-width: 15.5rem;
max-width: 26.5rem;
min-width: 18rem;
width: 26.5rem;
max-width: 33vw;
@media (min-width: 926px) {
width: 25vw;
max-width: 40vw;
}
@media (min-width: 1276px) {
max-width: 33vw;
}
@media (min-width: 1680px) {
border-left: 1px solid var(--color-borders);

View File

@ -32,6 +32,7 @@ type StateProps = Pick<GlobalState, 'uiReadyState' | 'shouldSkipHistoryAnimation
hasCustomBackground?: boolean;
hasCustomBackgroundColor: boolean;
isRightColumnShown?: boolean;
leftColumnWidth?: number;
};
type DispatchProps = Pick<GlobalActions, 'setIsUiReady'>;
@ -83,6 +84,7 @@ const UiLoader: FC<OwnProps & StateProps & DispatchProps> = ({
hasCustomBackgroundColor,
isRightColumnShown,
shouldSkipHistoryAnimations,
leftColumnWidth,
setIsUiReady,
}) => {
const [isReady, markReady] = useFlag();
@ -131,7 +133,11 @@ const UiLoader: FC<OwnProps & StateProps & DispatchProps> = ({
<div className={buildClassName('mask', transitionClassNames)}>
{page === 'main' ? (
<>
<div className="left" />
<div
className="left"
// @ts-ignore teact feature
style={leftColumnWidth ? `width: ${leftColumnWidth}px` : undefined}
/>
<div
className={buildClassName(
'middle',
@ -162,6 +168,7 @@ export default withGlobal<OwnProps>(
hasCustomBackground: Boolean(background),
hasCustomBackgroundColor: Boolean(backgroundColor),
isRightColumnShown: selectIsRightColumnShown(global),
leftColumnWidth: global.leftColumnWidth,
};
},
(setGlobal, actions): DispatchProps => pick(actions, ['setIsUiReady']),

View File

@ -1,7 +1,3 @@
#LeftColumn {
overflow: hidden;
}
#NewChat {
height: 100%;
}

View File

@ -1,5 +1,5 @@
import React, {
FC, memo, useCallback, useEffect, useState,
FC, memo, useCallback, useEffect, useRef, useState,
} from '../../lib/teact/teact';
import { withGlobal } from '../../lib/teact/teactn';
@ -10,6 +10,7 @@ import { LAYERS_ANIMATION_NAME } from '../../util/environment';
import captureEscKeyListener from '../../util/captureEscKeyListener';
import { pick } from '../../util/iteratees';
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
import { useResize } from '../../hooks/useResize';
import Transition from '../ui/Transition';
import LeftMain from './main/LeftMain';
@ -24,11 +25,12 @@ type StateProps = {
searchDate?: number;
activeChatFolder: number;
shouldSkipHistoryAnimations?: boolean;
leftColumnWidth?: number;
};
type DispatchProps = Pick<GlobalActions, (
'setGlobalSearchQuery' | 'setGlobalSearchChatId' | 'resetChatCreation' | 'setGlobalSearchDate' |
'loadPasswordInfo' | 'clearTwoFaError'
'loadPasswordInfo' | 'clearTwoFaError' | 'setLeftColumnWidth' | 'resetLeftColumnWidth'
)>;
enum ContentType {
@ -50,13 +52,18 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
searchDate,
activeChatFolder,
shouldSkipHistoryAnimations,
leftColumnWidth,
setGlobalSearchQuery,
setGlobalSearchChatId,
resetChatCreation,
setGlobalSearchDate,
loadPasswordInfo,
clearTwoFaError,
setLeftColumnWidth,
resetLeftColumnWidth,
}) => {
// eslint-disable-next-line no-null/no-null
const resizeRef = useRef<HTMLDivElement>(null);
const [content, setContent] = useState<LeftColumnContent>(LeftColumnContent.ChatList);
const [settingsScreen, setSettingsScreen] = useState(SettingsScreens.Main);
const [contactsFilter, setContactsFilter] = useState<string>('');
@ -257,81 +264,95 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
}
}, [clearTwoFaError, loadPasswordInfo, settingsScreen]);
const {
initResize, resetResize, handleMouseUp,
} = useResize(resizeRef, setLeftColumnWidth, resetLeftColumnWidth, leftColumnWidth);
const handleSettingsScreenSelect = (screen: SettingsScreens) => {
setContent(LeftColumnContent.Settings);
setSettingsScreen(screen);
};
return (
<Transition
<div
id="LeftColumn"
name={shouldSkipHistoryAnimations ? 'none' : LAYERS_ANIMATION_NAME}
renderCount={RENDER_COUNT}
activeKey={contentType}
shouldCleanup
cleanupExceptionKey={ContentType.Main}
ref={resizeRef}
>
{(isActive) => {
switch (contentType) {
case ContentType.Archived:
return (
<ArchivedChats
isActive={isActive}
onReset={handleReset}
onContentChange={setContent}
/>
);
case ContentType.Settings:
return (
<Settings
isActive={isActive}
currentScreen={settingsScreen}
foldersState={foldersState}
foldersDispatch={foldersDispatch}
onScreenSelect={handleSettingsScreenSelect}
onReset={handleReset}
shouldSkipTransition={shouldSkipHistoryAnimations}
/>
);
case ContentType.NewChannel:
return (
<NewChat
key={lastResetTime}
isActive={isActive}
isChannel
content={content}
onContentChange={setContent}
onReset={handleReset}
/>
);
case ContentType.NewGroup:
return (
<NewChat
key={lastResetTime}
isActive={isActive}
content={content}
onContentChange={setContent}
onReset={handleReset}
/>
);
default:
return (
<LeftMain
content={content}
searchQuery={searchQuery}
searchDate={searchDate}
contactsFilter={contactsFilter}
foldersDispatch={foldersDispatch}
onContentChange={setContent}
onSearchQuery={handleSearchQuery}
onScreenSelect={handleSettingsScreenSelect}
onReset={handleReset}
shouldSkipTransition={shouldSkipHistoryAnimations}
/>
);
}
}}
</Transition>
<Transition
name={shouldSkipHistoryAnimations ? 'none' : LAYERS_ANIMATION_NAME}
renderCount={RENDER_COUNT}
activeKey={contentType}
shouldCleanup
cleanupExceptionKey={ContentType.Main}
>
{(isActive) => {
switch (contentType) {
case ContentType.Archived:
return (
<ArchivedChats
isActive={isActive}
onReset={handleReset}
onContentChange={setContent}
/>
);
case ContentType.Settings:
return (
<Settings
isActive={isActive}
currentScreen={settingsScreen}
foldersState={foldersState}
foldersDispatch={foldersDispatch}
onScreenSelect={handleSettingsScreenSelect}
onReset={handleReset}
shouldSkipTransition={shouldSkipHistoryAnimations}
/>
);
case ContentType.NewChannel:
return (
<NewChat
key={lastResetTime}
isActive={isActive}
isChannel
content={content}
onContentChange={setContent}
onReset={handleReset}
/>
);
case ContentType.NewGroup:
return (
<NewChat
key={lastResetTime}
isActive={isActive}
content={content}
onContentChange={setContent}
onReset={handleReset}
/>
);
default:
return (
<LeftMain
content={content}
searchQuery={searchQuery}
searchDate={searchDate}
contactsFilter={contactsFilter}
foldersDispatch={foldersDispatch}
onContentChange={setContent}
onSearchQuery={handleSearchQuery}
onScreenSelect={handleSettingsScreenSelect}
onReset={handleReset}
shouldSkipTransition={shouldSkipHistoryAnimations}
/>
);
}
}}
</Transition>
<div
className="resize-handle"
onMouseDown={initResize}
onMouseUp={handleMouseUp}
onDoubleClick={resetResize}
/>
</div>
);
};
@ -346,13 +367,14 @@ export default memo(withGlobal(
activeChatFolder,
},
shouldSkipHistoryAnimations,
leftColumnWidth,
} = global;
return {
searchQuery: query, searchDate: date, activeChatFolder, shouldSkipHistoryAnimations,
searchQuery: query, searchDate: date, activeChatFolder, shouldSkipHistoryAnimations, leftColumnWidth,
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
'setGlobalSearchQuery', 'setGlobalSearchChatId', 'resetChatCreation', 'setGlobalSearchDate',
'loadPasswordInfo', 'clearTwoFaError',
'loadPasswordInfo', 'clearTwoFaError', 'setLeftColumnWidth', 'resetLeftColumnWidth',
]),
)(LeftColumn));

View File

@ -1,5 +1,4 @@
#Main {
display: flex;
height: 100%;
text-align: left;
overflow: hidden;
@ -11,13 +10,25 @@
@media (max-width: 600px) {
height: calc(var(--vh, 1vh) * 100);
}
@media (min-width: 926px) {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 100%;
}
}
#LeftColumn {
flex: 1;
min-width: 18rem;
min-width: 12rem;
width: 25vw;
max-width: 26.5rem;
height: 100%;
position: relative;
& > div {
height: 100%;
overflow: hidden;
}
@media (max-width: 600px) {
height: calc(var(--vh, 1vh) * 100);
@ -27,8 +38,12 @@
border-left: 1px solid var(--color-borders);
}
@media (max-width: 1275px) {
flex: 2;
@media (min-width: 926px) {
max-width: 40vw;
}
@media (min-width: 1276px) {
max-width: 33vw;
}
@media (max-width: 925px) {
@ -36,7 +51,7 @@
left: 0;
top: 0;
height: calc(var(--vh, 1vh) * 100);
width: 26.5rem;
width: 26.5rem !important;
transform: translate3d(-5rem, 0, 0);
transition: transform var(--layer-transition);
@ -92,7 +107,7 @@
@media (max-width: 600px) {
max-width: none;
width: 100vw;
width: 100vw !important;
transform: translate3d(-20vw, 0, 0);
@supports (left: env(safe-area-inset-left)) {
@ -125,21 +140,10 @@
}
#MiddleColumn {
flex: 3;
border-left: 1px solid var(--color-borders);
max-width: 75vw;
@media (max-width: 1275px) {
max-width: calc(100vw - 26.5rem);
}
@media (max-width: 66.25rem) {
max-width: 60vw;
}
@media (min-width: 1680px) {
border-right: 1px solid var(--color-borders);
max-width: calc(1680px - 26.5rem);
}
@media (max-width: 925px) {

View File

@ -76,6 +76,7 @@
height: 100%;
position: relative;
z-index: 1;
min-width: 0;
@media (max-width: 600px) {
overflow: hidden;

View File

@ -76,6 +76,10 @@
.ListItem-button {
cursor: pointer;
body.cursor-ew-resize & {
cursor: ew-resize !important;
}
@media (hover: hover) {
&:hover, &:focus {
--background-color: var(--color-chat-hover);

View File

@ -164,6 +164,7 @@ function updateCache() {
'recentEmojis',
'push',
'shouldShowContextMenuHint',
'leftColumnWidth',
]),
isChatInfoShown: reduceShowChatInfo(global),
users: reduceUsers(global),

View File

@ -83,6 +83,7 @@ export type GlobalState = {
currentUserId?: number;
lastSyncTime?: number;
serverTimeOffset: number;
leftColumnWidth?: number;
// TODO Move to `auth`.
isLoggingOut?: boolean;
@ -447,7 +448,7 @@ export type ActionTypes = (
// ui
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
'setNewChatMembersDialogState' | 'disableHistoryAnimations' |
'setNewChatMembersDialogState' | 'disableHistoryAnimations' | 'setLeftColumnWidth' | 'resetLeftColumnWidth' |
// auth
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' |
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |

64
src/hooks/useResize.ts Normal file
View File

@ -0,0 +1,64 @@
import { RefObject } from 'react';
import { useState, useEffect } from '../lib/teact/teact';
import useFlag from './useFlag';
export const useResize = (
elementRef: RefObject<HTMLElement>,
onResize: (width: number) => void,
onReset: NoneToVoidFunction,
initialWidth?: number,
) => {
const [isActive, markIsActive, unmarkIsActive] = useFlag();
const [initialMouseX, setInitialMouseX] = useState<number>();
const [initialElementWidth, setInitialElementWidth] = useState<number>();
useEffect(() => {
if (!elementRef.current || !initialWidth) {
return;
}
elementRef.current.style.width = `${initialWidth}px`;
}, [elementRef, initialWidth]);
const handleMouseUp = () => {
document.body.classList.remove('no-selection', 'cursor-ew-resize');
};
const initResize = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
document.body.classList.add('no-selection', 'cursor-ew-resize');
setInitialMouseX(event.clientX);
setInitialElementWidth(elementRef.current!.offsetWidth);
markIsActive();
};
const resetResize = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
event.preventDefault();
elementRef.current!.style.width = '';
onReset();
};
useEffect(() => {
if (!isActive) return;
const handleMouseMove = (event: MouseEvent) => {
const newWidth = Math.ceil(initialElementWidth + event.clientX - initialMouseX);
elementRef.current!.style.width = `${newWidth}px`;
};
const stopDrag = () => {
handleMouseUp();
document.removeEventListener('mousemove', handleMouseMove, false);
document.removeEventListener('mouseup', stopDrag, false);
document.removeEventListener('blur', stopDrag, false);
onResize(elementRef.current!.offsetWidth);
unmarkIsActive();
};
document.addEventListener('mousemove', handleMouseMove, false);
document.addEventListener('mouseup', stopDrag, false);
document.addEventListener('blur', stopDrag, false);
}, [initialElementWidth, initialMouseX, elementRef, onResize, isActive, unmarkIsActive]);
return { initResize, resetResize, handleMouseUp };
};

View File

@ -17,6 +17,22 @@ addReducer('toggleChatInfo', (global) => {
};
});
addReducer('setLeftColumnWidth', (global, actions, payload) => {
const leftColumnWidth = payload;
return {
...global,
leftColumnWidth,
};
});
addReducer('resetLeftColumnWidth', (global) => {
return {
...global,
leftColumnWidth: undefined,
};
});
addReducer('toggleManagement', (global): GlobalState | undefined => {
const { chatId } = selectCurrentMessageList(global) || {};

View File

@ -194,6 +194,7 @@ $color-user-8: #faa774;
--z-sticky-date: 9;
--z-register-add-avatar: 5;
--z-media-viewer-head: 3;
--z-resize-handle: 2;
--z-below: -1;
--spinner-white-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iI2ZmZmZmZiIvPjwvc3ZnPg==);

View File

@ -49,9 +49,13 @@ body.cursor-grabbing, body.cursor-grabbing * {
cursor: grabbing !important;
}
body.cursor-ew-resize {
cursor: ew-resize !important;
}
#root {
height: 100%;
max-width: 1680px;
max-width: 1920px;
margin: 0 auto;
@media (max-width: 600px) {
height: calc(var(--vh, 1vh) * 100);
@ -75,6 +79,21 @@ body.cursor-grabbing, body.cursor-grabbing * {
-webkit-user-select: none !important;
}
.resize-handle {
display: none;
position: absolute;
top: 0;
right: -.1875rem;
bottom: 0;
width: .1875rem;
z-index: var(--z-resize-handle);
cursor: ew-resize;
@media (min-width: 926px) {
display: block;
}
}
/*
See the article for more information on this visually-hidden pattern.
https://snook.ca/archives/html_and_css/hiding-content-for-accessibility