From 5180eb0d2ee7c9626aa4ae959532634827b2c58d Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 13 Oct 2021 14:38:34 +0300 Subject: [PATCH] Left Column: Support resizing; Better support for large displays (#1484) --- .browserslistrc | 2 +- package-lock.json | 51 ++++++-- package.json | 2 +- src/components/common/UiLoader.scss | 23 +++- src/components/common/UiLoader.tsx | 9 +- src/components/left/LeftColumn.scss | 4 - src/components/left/LeftColumn.tsx | 164 ++++++++++++++---------- src/components/main/Main.scss | 40 +++--- src/components/middle/MiddleColumn.scss | 1 + src/components/ui/ListItem.scss | 4 + src/global/cache.ts | 1 + src/global/types.ts | 3 +- src/hooks/useResize.ts | 64 +++++++++ src/modules/actions/ui/misc.ts | 16 +++ src/styles/_variables.scss | 1 + src/styles/index.scss | 21 ++- 16 files changed, 296 insertions(+), 110 deletions(-) create mode 100644 src/hooks/useResize.ts diff --git a/.browserslistrc b/.browserslistrc index bbb709d46..cf3f68d1b 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1 +1 @@ -> 2%, last 2 edge versions +> 2%, last 2 edge versions, iOS >= 13.4, firefox >= 68, firefoxandroid >= 68, last 2 safari major versions diff --git a/package-lock.json b/package-lock.json index 364fca929..4b0b8b837 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ebea0395f..0f7cb100e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/common/UiLoader.scss b/src/components/common/UiLoader.scss index 4149c76f9..1422c9d8e 100644 --- a/src/components/common/UiLoader.scss +++ b/src/components/common/UiLoader.scss @@ -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); diff --git a/src/components/common/UiLoader.tsx b/src/components/common/UiLoader.tsx index c606efda2..b34dc2896 100644 --- a/src/components/common/UiLoader.tsx +++ b/src/components/common/UiLoader.tsx @@ -32,6 +32,7 @@ type StateProps = Pick; @@ -83,6 +84,7 @@ const UiLoader: FC = ({ hasCustomBackgroundColor, isRightColumnShown, shouldSkipHistoryAnimations, + leftColumnWidth, setIsUiReady, }) => { const [isReady, markReady] = useFlag(); @@ -131,7 +133,11 @@ const UiLoader: FC = ({
{page === 'main' ? ( <> -
+
( hasCustomBackground: Boolean(background), hasCustomBackgroundColor: Boolean(backgroundColor), isRightColumnShown: selectIsRightColumnShown(global), + leftColumnWidth: global.leftColumnWidth, }; }, (setGlobal, actions): DispatchProps => pick(actions, ['setIsUiReady']), diff --git a/src/components/left/LeftColumn.scss b/src/components/left/LeftColumn.scss index 2de028c43..096e328ce 100644 --- a/src/components/left/LeftColumn.scss +++ b/src/components/left/LeftColumn.scss @@ -1,7 +1,3 @@ -#LeftColumn { - overflow: hidden; -} - #NewChat { height: 100%; } diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index 20802f293..7ac1fa504 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -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; enum ContentType { @@ -50,13 +52,18 @@ const LeftColumn: FC = ({ searchDate, activeChatFolder, shouldSkipHistoryAnimations, + leftColumnWidth, setGlobalSearchQuery, setGlobalSearchChatId, resetChatCreation, setGlobalSearchDate, loadPasswordInfo, clearTwoFaError, + setLeftColumnWidth, + resetLeftColumnWidth, }) => { + // eslint-disable-next-line no-null/no-null + const resizeRef = useRef(null); const [content, setContent] = useState(LeftColumnContent.ChatList); const [settingsScreen, setSettingsScreen] = useState(SettingsScreens.Main); const [contactsFilter, setContactsFilter] = useState(''); @@ -257,81 +264,95 @@ const LeftColumn: FC = ({ } }, [clearTwoFaError, loadPasswordInfo, settingsScreen]); + const { + initResize, resetResize, handleMouseUp, + } = useResize(resizeRef, setLeftColumnWidth, resetLeftColumnWidth, leftColumnWidth); + const handleSettingsScreenSelect = (screen: SettingsScreens) => { setContent(LeftColumnContent.Settings); setSettingsScreen(screen); }; return ( - - {(isActive) => { - switch (contentType) { - case ContentType.Archived: - return ( - - ); - case ContentType.Settings: - return ( - - ); - case ContentType.NewChannel: - return ( - - ); - case ContentType.NewGroup: - return ( - - ); - default: - return ( - - ); - } - }} - + + {(isActive) => { + switch (contentType) { + case ContentType.Archived: + return ( + + ); + case ContentType.Settings: + return ( + + ); + case ContentType.NewChannel: + return ( + + ); + case ContentType.NewGroup: + return ( + + ); + default: + return ( + + ); + } + }} + +
+
); }; @@ -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)); diff --git a/src/components/main/Main.scss b/src/components/main/Main.scss index 5063cf3dc..29b5eb2e0 100644 --- a/src/components/main/Main.scss +++ b/src/components/main/Main.scss @@ -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) { diff --git a/src/components/middle/MiddleColumn.scss b/src/components/middle/MiddleColumn.scss index f2968c7b5..19933b53f 100644 --- a/src/components/middle/MiddleColumn.scss +++ b/src/components/middle/MiddleColumn.scss @@ -76,6 +76,7 @@ height: 100%; position: relative; z-index: 1; + min-width: 0; @media (max-width: 600px) { overflow: hidden; diff --git a/src/components/ui/ListItem.scss b/src/components/ui/ListItem.scss index 9bce8cb65..de3ed9255 100644 --- a/src/components/ui/ListItem.scss +++ b/src/components/ui/ListItem.scss @@ -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); diff --git a/src/global/cache.ts b/src/global/cache.ts index 0d15b65b5..6c64aceff 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -164,6 +164,7 @@ function updateCache() { 'recentEmojis', 'push', 'shouldShowContextMenuHint', + 'leftColumnWidth', ]), isChatInfoShown: reduceShowChatInfo(global), users: reduceUsers(global), diff --git a/src/global/types.ts b/src/global/types.ts index 55a0c42dd..e0983e615 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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' | diff --git a/src/hooks/useResize.ts b/src/hooks/useResize.ts new file mode 100644 index 000000000..c8a6d2c5a --- /dev/null +++ b/src/hooks/useResize.ts @@ -0,0 +1,64 @@ +import { RefObject } from 'react'; +import { useState, useEffect } from '../lib/teact/teact'; +import useFlag from './useFlag'; + +export const useResize = ( + elementRef: RefObject, + onResize: (width: number) => void, + onReset: NoneToVoidFunction, + initialWidth?: number, +) => { + const [isActive, markIsActive, unmarkIsActive] = useFlag(); + const [initialMouseX, setInitialMouseX] = useState(); + const [initialElementWidth, setInitialElementWidth] = useState(); + + 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) => { + document.body.classList.add('no-selection', 'cursor-ew-resize'); + + setInitialMouseX(event.clientX); + setInitialElementWidth(elementRef.current!.offsetWidth); + markIsActive(); + }; + + const resetResize = (event: React.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 }; +}; diff --git a/src/modules/actions/ui/misc.ts b/src/modules/actions/ui/misc.ts index 657be19c6..f6eaae8a6 100644 --- a/src/modules/actions/ui/misc.ts +++ b/src/modules/actions/ui/misc.ts @@ -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) || {}; diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index cc74d9c01..1614a307c 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -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==); diff --git a/src/styles/index.scss b/src/styles/index.scss index cf8cc7f14..e7b7ce856 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -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