From eb6e5f5e88948e60f4e7eafa39f62004a79cb6aa Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 25 Jan 2022 03:24:34 +0100 Subject: [PATCH] Connection Status: Indicate when syncing, different positions --- src/@types/global.d.ts | 10 ++- src/components/common/DotAnimation.scss | 32 +++++++++ src/components/common/DotAnimation.tsx | 23 ++++++ src/components/common/GroupChatInfo.tsx | 15 +++- src/components/common/PrivateChatInfo.tsx | 9 ++- src/components/common/TypingStatus.scss | 28 -------- src/components/common/TypingStatus.tsx | 10 ++- src/components/left/ConnectionState.scss | 27 ------- src/components/left/ConnectionState.tsx | 24 ------- .../left/ConnectionStatusOverlay.scss | 66 +++++++++++++++++ .../left/ConnectionStatusOverlay.tsx | 44 ++++++++++++ src/components/left/main/LeftMain.scss | 11 --- src/components/left/main/LeftMain.tsx | 24 +------ src/components/left/main/LeftMainHeader.tsx | 72 ++++++++++++++----- src/components/middle/MessageList.tsx | 2 +- src/components/middle/MiddleHeader.tsx | 70 ++++++++---------- src/components/ui/Button.scss | 14 ++++ src/components/ui/Button.tsx | 6 +- src/components/ui/Loading.scss | 4 ++ src/components/ui/Loading.tsx | 11 +-- src/components/ui/SearchInput.tsx | 15 +++- src/components/ui/Spinner.scss | 17 ++++- src/components/ui/Spinner.tsx | 8 +-- src/global/initial.ts | 1 + src/global/types.ts | 1 + src/hooks/useConnectionStatus.ts | 62 ++++++++++++++++ src/modules/actions/api/sync.ts | 3 + src/styles/_variables.scss | 1 + src/types/index.ts | 1 + 29 files changed, 419 insertions(+), 192 deletions(-) create mode 100644 src/components/common/DotAnimation.scss create mode 100644 src/components/common/DotAnimation.tsx delete mode 100644 src/components/left/ConnectionState.scss delete mode 100644 src/components/left/ConnectionState.tsx create mode 100644 src/components/left/ConnectionStatusOverlay.scss create mode 100644 src/components/left/ConnectionStatusOverlay.tsx create mode 100644 src/hooks/useConnectionStatus.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 10c506dc7..7e7bebc9e 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -55,7 +55,9 @@ declare module 'pako/dist/pako_inflate' { function inflate(...args: any[]): string; } -type WindowWithPerf = typeof window & { perf: AnyLiteral }; +type WindowWithPerf = + typeof window + & { perf: AnyLiteral }; interface TEncodedImage { result: Uint8ClampedArray; @@ -70,10 +72,12 @@ interface IWebpWorker extends Worker { interface Window { ClipboardItem?: any; - requestIdleCallback: (cb: AnyToVoidFunction, options:{ timeout?: number }) => void; + requestIdleCallback: (cb: AnyToVoidFunction, options: { timeout?: number }) => void; } -interface Clipboard { write?: any } +interface Clipboard { + write?: any; +} interface Document { mozFullScreenElement: any; diff --git a/src/components/common/DotAnimation.scss b/src/components/common/DotAnimation.scss new file mode 100644 index 000000000..35d8b48c0 --- /dev/null +++ b/src/components/common/DotAnimation.scss @@ -0,0 +1,32 @@ +.DotAnimation { + display: inline-flex; + align-items: baseline; + + .ellipsis { + display: flex; + width: 1rem; + overflow: hidden; + + &::after { + content: '...'; + animation: dot-animation 1s steps(4, start) infinite; + + html[lang=ar] &, + html[lang=fa] & { + animation-name: dot-animation-rtl; + } + } + } +} + +@keyframes dot-animation { + from { + transform: translateX(-1rem); + } +} + +@keyframes dot-animation-rtl { + from { + transform: translateX(1rem); + } +} diff --git a/src/components/common/DotAnimation.tsx b/src/components/common/DotAnimation.tsx new file mode 100644 index 000000000..3c6f297a7 --- /dev/null +++ b/src/components/common/DotAnimation.tsx @@ -0,0 +1,23 @@ +import React, { FC } from '../../lib/teact/teact'; + +import useLang from '../../hooks/useLang'; +import buildClassName from '../../util/buildClassName'; + +import './DotAnimation.scss'; + +type OwnProps = { + content: string; + className?: string; +}; + +const DotAnimation: FC = ({ content, className }) => { + const lang = useLang(); + return ( + + {content} + + + ); +}; + +export default DotAnimation; diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index f2d3f98d2..f6386abfb 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -20,11 +20,14 @@ import useLang, { LangFn } from '../../hooks/useLang'; import Avatar from './Avatar'; import VerifiedIcon from './VerifiedIcon'; import TypingStatus from './TypingStatus'; +import DotAnimation from './DotAnimation'; type OwnProps = { chatId: string; typingStatus?: ApiTypingStatus; avatarSize?: 'small' | 'medium' | 'large' | 'jumbo'; + status?: string; + withDots?: boolean; withMediaViewer?: boolean; withUsername?: boolean; withFullInfo?: boolean; @@ -44,6 +47,8 @@ type StateProps = const GroupChatInfo: FC = ({ typingStatus, avatarSize = 'medium', + status, + withDots, withMediaViewer, withUsername, withFullInfo, @@ -86,9 +91,17 @@ const GroupChatInfo: FC = ({ } function renderStatusOrTyping() { + if (status) { + return withDots ? ( + + ) : ( + {status} + ); + } + if (withUpdatingStatus && !areMessagesLoaded && !isRestricted) { return ( - {lang('Updating')} + ); } diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index efc702207..0ca19b317 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -16,6 +16,7 @@ import useLang from '../../hooks/useLang'; import Avatar from './Avatar'; import VerifiedIcon from './VerifiedIcon'; import TypingStatus from './TypingStatus'; +import DotAnimation from './DotAnimation'; type OwnProps = { userId: string; @@ -23,6 +24,7 @@ type OwnProps = { avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo'; forceShowSelf?: boolean; status?: string; + withDots?: boolean; withMediaViewer?: boolean; withUsername?: boolean; withFullInfo?: boolean; @@ -45,6 +47,7 @@ const PrivateChatInfo: FC = ({ typingStatus, avatarSize = 'medium', status, + withDots, withMediaViewer, withUsername, withFullInfo, @@ -90,14 +93,16 @@ const PrivateChatInfo: FC = ({ function renderStatusOrTyping() { if (status) { - return ( + return withDots ? ( + + ) : ( {status} ); } if (withUpdatingStatus && !areMessagesLoaded) { return ( - {lang('Updating')} + ); } diff --git a/src/components/common/TypingStatus.scss b/src/components/common/TypingStatus.scss index 4ec1cec77..98d1c804f 100644 --- a/src/components/common/TypingStatus.scss +++ b/src/components/common/TypingStatus.scss @@ -8,32 +8,4 @@ color: var(--color-text-secondary); } } - - .ellipsis { - display: flex; - width: 1rem; - overflow: hidden; - - &::after { - content: '...'; - animation: typing-animation 1s steps(4, start) infinite; - - html[lang=ar] &, - html[lang=fa] & { - animation-name: typing-animation-rtl; - } - } - } -} - -@keyframes typing-animation { - from { - transform: translateX(-1rem); - } -} - -@keyframes typing-animation-rtl { - from { - transform: translateX(1rem); - } } diff --git a/src/components/common/TypingStatus.tsx b/src/components/common/TypingStatus.tsx index e4a58f076..86601c337 100644 --- a/src/components/common/TypingStatus.tsx +++ b/src/components/common/TypingStatus.tsx @@ -8,6 +8,8 @@ import { getUserFirstOrLastName } from '../../modules/helpers'; import renderText from './helpers/renderText'; import useLang from '../../hooks/useLang'; +import DotAnimation from './DotAnimation'; + import './TypingStatus.scss'; type OwnProps = { @@ -21,15 +23,17 @@ type StateProps = { const TypingStatus: FC = ({ typingStatus, typingUser }) => { const lang = useLang(); const typingUserName = typingUser && !typingUser.isSelf && getUserFirstOrLastName(typingUser); + const content = lang(typingStatus.action) + // Fix for translation "{user} is typing" + .replace('{user}', '') + .replace('{emoji}', typingStatus.emoji).trim(); return (

{typingUserName && ( {renderText(typingUserName)} )} - {/* fix for translation "username _is_ typing" */} - {lang(typingStatus.action).replace('{user}', '').replace('{emoji}', typingStatus.emoji).trim()} - +

); }; diff --git a/src/components/left/ConnectionState.scss b/src/components/left/ConnectionState.scss deleted file mode 100644 index 86dfd6d7b..000000000 --- a/src/components/left/ConnectionState.scss +++ /dev/null @@ -1,27 +0,0 @@ -#ConnectionState { - flex: 0 0 auto; - display: flex; - align-items: center; - margin: 0 0.5rem 0.5rem; - padding: 0.75rem; - background: var(--color-yellow); - border-radius: var(--border-radius-default); - - > .Spinner { - --spinner-size: 1.75rem; - } - - > .state-text { - color: var(--color-text-lighter); - font-weight: 500; - line-height: 2rem; - margin-inline-start: 1.875rem; - white-space: nowrap; - } - - @media (max-width: 950px) { - > .state-text { - margin-inline-start: 1.25rem; - } - } -} diff --git a/src/components/left/ConnectionState.tsx b/src/components/left/ConnectionState.tsx deleted file mode 100644 index 2112944dc..000000000 --- a/src/components/left/ConnectionState.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { memo, FC } from '../../lib/teact/teact'; - -import { GlobalState } from '../../global/types'; - -import useLang from '../../hooks/useLang'; - -import Spinner from '../ui/Spinner'; - -import './ConnectionState.scss'; - -type StateProps = Pick; - -const ConnectionState: FC = () => { - const lang = useLang(); - - return ( -
- -
{lang('WaitingForNetwork')}
-
- ); -}; - -export default memo(ConnectionState); diff --git a/src/components/left/ConnectionStatusOverlay.scss b/src/components/left/ConnectionStatusOverlay.scss new file mode 100644 index 000000000..6c7baf574 --- /dev/null +++ b/src/components/left/ConnectionStatusOverlay.scss @@ -0,0 +1,66 @@ +.connection-state-wrapper { + position: absolute; + top: 0; + left: 0; + width: 100%; + + transition: transform 300ms ease, opacity 300ms ease; + opacity: 1; + + &:not(.open) { + transform: translateY(-3rem); + opacity: 0; + } + + &:not(.shown) { + display: none; + } +} + +#ConnectionStatusOverlay { + height: 2.9375rem; + flex: 0 0 auto; + display: flex; + align-items: center; + margin: 0.375rem 0.5rem; + padding: 0 0.75rem; + background: var(--color-yellow); + border-radius: var(--border-radius-default); + + &.interactive { + cursor: pointer; + } + + > .Spinner { + --spinner-size: 1.75rem; + } + + > .state-text { + flex: 1; + color: var(--color-text-lighter); + font-size: 0.9375rem; + font-weight: 500; + padding-bottom: 0.0625rem; + margin-inline-start: 1.875rem; + white-space: nowrap; + } + + @media (max-width: 950px) { + > .state-text { + margin-inline-start: 1.25rem; + } + } + + .Transition { + width: 100%; + // https://dfmcphee.com/flex-items-and-min-width-0/ + // https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size + min-width: 0; + + > div { + display: flex; + align-items: center; + width: 100%; + } + } +} diff --git a/src/components/left/ConnectionStatusOverlay.tsx b/src/components/left/ConnectionStatusOverlay.tsx new file mode 100644 index 000000000..5e513b68d --- /dev/null +++ b/src/components/left/ConnectionStatusOverlay.tsx @@ -0,0 +1,44 @@ +import React, { FC, memo } from '../../lib/teact/teact'; + +import useLang from '../../hooks/useLang'; +import { ConnectionStatus } from '../../hooks/useConnectionStatus'; + +import Transition from '../ui/Transition'; +import Spinner from '../ui/Spinner'; +import Button from '../ui/Button'; + +import './ConnectionStatusOverlay.scss'; + +type OwnProps = { + connectionStatus: ConnectionStatus; + connectionStatusText: string; + onClick?: NoneToVoidFunction; +}; + +const ConnectionStatusOverlay: FC = ({ + connectionStatus, + connectionStatusText, + onClick, +}) => { + const lang = useLang(); + + return ( +
+ +
+ + {() => connectionStatusText} + +
+ +
+ ); +}; + +export default memo(ConnectionStatusOverlay); diff --git a/src/components/left/main/LeftMain.scss b/src/components/left/main/LeftMain.scss index c24662df7..d55ecff5c 100644 --- a/src/components/left/main/LeftMain.scss +++ b/src/components/left/main/LeftMain.scss @@ -6,20 +6,9 @@ overflow: hidden; z-index: 1; - .connection-state-wrapper { - position: absolute; - top: 3.75rem; - width: 100%; - } - > .Transition { flex: 1; overflow: hidden; - transition: transform 300ms ease; - - &.pull-down { - transform: translateY(3.75rem); - } } .ChatFolders { diff --git a/src/components/left/main/LeftMain.tsx b/src/components/left/main/LeftMain.tsx index 9b05dd5c0..44addf5a2 100644 --- a/src/components/left/main/LeftMain.tsx +++ b/src/components/left/main/LeftMain.tsx @@ -1,28 +1,22 @@ import React, { - FC, useState, useRef, useCallback, useEffect, + FC, memo, useCallback, useEffect, useRef, useState, } from '../../../lib/teact/teact'; -import { withGlobal } from '../../../lib/teact/teactn'; -import { GlobalState } from '../../../global/types'; import { LeftColumnContent, SettingsScreens } from '../../../types'; import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer'; import { IS_TOUCH_ENV } from '../../../util/environment'; -import { pick } from '../../../util/iteratees'; import buildClassName from '../../../util/buildClassName'; -import useBrowserOnline from '../../../hooks/useBrowserOnline'; import useFlag from '../../../hooks/useFlag'; import useShowTransition from '../../../hooks/useShowTransition'; import useLang from '../../../hooks/useLang'; import Transition from '../../ui/Transition'; import LeftMainHeader from './LeftMainHeader'; -import ConnectionState from '../ConnectionState'; import ChatFolders from './ChatFolders'; import LeftSearch from '../search/LeftSearch.async'; import ContactList from './ContactList.async'; import NewChatButton from '../NewChatButton'; -import ShowTransition from '../../ui/ShowTransition'; import Button from '../../ui/Button'; import './LeftMain.scss'; @@ -40,15 +34,13 @@ type OwnProps = { onReset: () => void; }; -type StateProps = Pick; - const TRANSITION_RENDER_COUNT = Object.keys(LeftColumnContent).length / 2; const BUTTON_CLOSE_DELAY_MS = 250; const APP_OUTDATED_TIMEOUT = 3 * 24 * 60 * 60 * 1000; // 3 days let closeTimeout: number | undefined; -const LeftMain: FC = ({ +const LeftMain: FC = ({ content, searchQuery, searchDate, @@ -59,13 +51,9 @@ const LeftMain: FC = ({ onContentChange, onScreenSelect, onReset, - connectionState, }) => { const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV); - const isBrowserOnline = useBrowserOnline(); - const isConnecting = !isBrowserOnline || connectionState === 'connectionStateConnecting'; - const isMouseInside = useRef(false); const handleSelectSettings = useCallback(() => { @@ -149,16 +137,12 @@ const LeftMain: FC = ({ onReset={onReset} shouldSkipTransition={shouldSkipTransition} /> - - {() => } - {(isActive) => { switch (content) { @@ -220,6 +204,4 @@ function useAppOutdatedCheck() { return [shouldRender, transitionClassNames, handleUpdateClick] as const; } -export default withGlobal( - (global): StateProps => pick(global, ['connectionState']), -)(LeftMain); +export default memo(LeftMain); diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index 333922bd1..b1c73c9d6 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -1,10 +1,11 @@ import React, { - FC, useCallback, useMemo, memo, + FC, memo, useCallback, useMemo, } from '../../../lib/teact/teact'; import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; -import { LeftColumnContent, ISettings } from '../../../types'; +import { ISettings, LeftColumnContent } from '../../../types'; import { ApiChat } from '../../../api/types'; +import { GlobalState } from '../../../global/types'; import { ANIMATION_LEVEL_MAX, APP_NAME, APP_VERSION, FEEDBACK_URL, @@ -15,10 +16,11 @@ import { formatDateToString } from '../../../util/dateFormat'; import switchTheme from '../../../util/switchTheme'; import { setPermanentWebVersion } from '../../../util/permanentWebVersion'; import { clearWebsync } from '../../../util/websync'; -import { selectTheme } from '../../../modules/selectors'; +import { selectCurrentMessageList, selectTheme } from '../../../modules/selectors'; import { isChatArchived } from '../../../modules/helpers'; import useLang from '../../../hooks/useLang'; import { disableHistoryBack } from '../../../hooks/useHistoryBack'; +import useConnectionStatus from '../../../hooks/useConnectionStatus'; import DropdownMenu from '../../ui/DropdownMenu'; import MenuItem from '../../ui/MenuItem'; @@ -26,6 +28,8 @@ import Button from '../../ui/Button'; import SearchInput from '../../ui/SearchInput'; import PickerSelectedItem from '../../common/PickerSelectedItem'; import Switcher from '../../ui/Switcher'; +import ShowTransition from '../../ui/ShowTransition'; +import ConnectionStatusOverlay from '../ConnectionStatusOverlay'; import './LeftMainHeader.scss'; @@ -40,16 +44,20 @@ type OwnProps = { onReset: () => void; }; -type StateProps = { - searchQuery?: string; - isLoading: boolean; - currentUserId?: string; - globalSearchChatId?: string; - searchDate?: number; - theme: ISettings['theme']; - animationLevel: 0 | 1 | 2; - chatsById?: Record; -}; +type StateProps = + { + searchQuery?: string; + isLoading: boolean; + currentUserId?: string; + globalSearchChatId?: string; + searchDate?: number; + theme: ISettings['theme']; + animationLevel: 0 | 1 | 2; + chatsById?: Record; + isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized']; + isMessageListOpen: boolean; + } + & Pick; const ANIMATION_LEVEL_OPTIONS = [0, 1, 2]; @@ -74,6 +82,10 @@ const LeftMainHeader: FC = ({ theme, animationLevel, chatsById, + connectionState, + isSyncing, + isConnectionStatusMinimized, + isMessageListOpen, }) => { const { openChat, @@ -105,6 +117,10 @@ const LeftMainHeader: FC = ({ }, 0); }, [hasMenu, chatsById]); + const { connectionStatus, connectionStatusText, connectionStatusPosition } = useConnectionStatus( + lang, connectionState, isSyncing, isMessageListOpen, isConnectionStatusMinimized, + ); + const withOtherVersions = window.location.hostname === PRODUCTION_HOSTNAME; const MainButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { @@ -134,6 +150,10 @@ const LeftMainHeader: FC = ({ } }, [searchQuery, onSearchQuery]); + const toggleConnectionStatus = useCallback(() => { + setSettingOption({ isConnectionStatusMinimized: !isConnectionStatusMinimized }); + }, [isConnectionStatusMinimized, setSettingOption]); + const handleSelectSaved = useCallback(() => { openChat({ id: currentUserId, shouldReplaceHistory: true }); }, [currentUserId, openChat]); @@ -272,13 +292,16 @@ const LeftMainHeader: FC = ({ className={globalSearchChatId || searchDate ? 'with-picker-item' : ''} value={contactsFilter || searchQuery} focused={isSearchFocused} - isLoading={isLoading} + isLoading={isLoading || connectionStatusPosition === 'minimized'} + spinnerColor={connectionStatusPosition === 'minimized' ? 'yellow' : undefined} + spinnerBackgroundColor={connectionStatusPosition === 'minimized' && theme === 'light' ? 'light' : undefined} placeholder={searchInputPlaceholder} autoComplete="off" canClose={Boolean(globalSearchChatId || searchDate)} onChange={onSearchQuery} onReset={onReset} onFocus={handleSearchFocus} + onSpinnerClick={connectionStatusPosition === 'minimized' ? toggleConnectionStatus : undefined} > {selectedSearchDate && ( = ({ /> )} + + {() => ( + + )} + ); @@ -310,9 +346,9 @@ export default memo(withGlobal( const { query: searchQuery, fetchingStatus, chatId, date, } = global.globalSearch; - const { currentUserId } = global; + const { currentUserId, connectionState, isSyncing } = global; const { byId: chatsById } = global.chats; - const { animationLevel } = global.settings.byKey; + const { isConnectionStatusMinimized, animationLevel } = global.settings.byKey; return { searchQuery, @@ -323,6 +359,10 @@ export default memo(withGlobal( searchDate: date, theme: selectTheme(global), animationLevel, + connectionState, + isSyncing, + isConnectionStatusMinimized, + isMessageListOpen: Boolean(selectCurrentMessageList(global)), }; }, )(LeftMainHeader)); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 6de7081d4..6824dd3d4 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -552,7 +552,7 @@ const MessageList: FC = ({ onNotchToggle={onNotchToggle} /> ) : ( - + )} ); diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index dd84b052c..729d41c91 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -1,48 +1,41 @@ import React, { - FC, useCallback, useMemo, memo, useEffect, useRef, useState, + FC, memo, useCallback, useEffect, useMemo, useRef, useState, } from '../../lib/teact/teact'; import { getDispatch, getGlobal, withGlobal } from '../../lib/teact/teactn'; import cycleRestrict from '../../util/cycleRestrict'; -import { MessageListType } from '../../global/types'; +import { GlobalState, MessageListType } from '../../global/types'; import { - ApiMessage, - ApiChat, - ApiUser, - ApiTypingStatus, - MAIN_THREAD_ID, ApiUpdateConnectionStateType, + ApiChat, ApiMessage, ApiTypingStatus, ApiUser, MAIN_THREAD_ID, } from '../../api/types'; import { - MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN, - MOBILE_SCREEN_MAX_WIDTH, EDITABLE_INPUT_ID, + MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, - SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, + MOBILE_SCREEN_MAX_WIDTH, SAFE_SCREEN_WIDTH_FOR_CHAT_INFO, + SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, } from '../../config'; import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../util/environment'; import { - isUserId, - getMessageKey, - getChatTitle, - getSenderTitle, + getChatTitle, getMessageKey, getSenderTitle, isUserId, } from '../../modules/helpers'; import { + selectAllowedMessageActions, selectChat, selectChatMessage, - selectAllowedMessageActions, - selectIsRightColumnShown, - selectThreadTopMessageId, - selectThreadInfo, selectChatMessages, - selectPinnedIds, - selectIsChatWithSelf, - selectForwardedSender, - selectScheduledIds, - selectIsInSelectMode, - selectIsChatWithBot, selectCountNotMutedUnread, + selectForwardedSender, + selectIsChatWithBot, + selectIsChatWithSelf, + selectIsInSelectMode, + selectIsRightColumnShown, + selectPinnedIds, + selectScheduledIds, + selectThreadInfo, + selectThreadTopMessageId, } from '../../modules/selectors'; import useEnsureMessage from '../../hooks/useEnsureMessage'; import useWindowSize from '../../hooks/useWindowSize'; @@ -51,7 +44,7 @@ import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import { formatIntegerCompact } from '../../util/textFormat'; import buildClassName from '../../util/buildClassName'; import useLang from '../../hooks/useLang'; -import useBrowserOnline from '../../hooks/useBrowserOnline'; +import useConnectionStatus from '../../hooks/useConnectionStatus'; import PrivateChatInfo from '../common/PrivateChatInfo'; import GroupChatInfo from '../common/GroupChatInfo'; @@ -91,7 +84,8 @@ type StateProps = { lastSyncTime?: number; shouldSkipHistoryAnimations?: boolean; currentTransitionKey: number; - connectionState?: ApiUpdateConnectionStateType; + connectionState?: GlobalState['connectionState']; + isSyncing?: GlobalState['isSyncing']; }; const MiddleHeader: FC = ({ @@ -116,6 +110,7 @@ const MiddleHeader: FC = ({ shouldSkipHistoryAnimations, currentTransitionKey, connectionState, + isSyncing, }) => { const { openChatWithInfo, @@ -296,21 +291,9 @@ const MiddleHeader: FC = ({ } }, [shouldUseStackedToolsClass, canRevealTools, canToolsCollideWithChatInfo, isRightColumnShown]); - const isBrowserOnline = useBrowserOnline(); - const isConnecting = (!isBrowserOnline || connectionState === 'connectionStateConnecting') - && (IS_SINGLE_COLUMN_LAYOUT || (IS_TABLET_COLUMN_LAYOUT && !shouldShowCloseButton)); + const { connectionStatusText } = useConnectionStatus(lang, connectionState, isSyncing, true); function renderInfo() { - if (isConnecting) { - return ( - <> - {renderBackButton()} -

- {lang('WaitingForNetwork')} -

- - ); - } return ( messageListType === 'thread' && threadId === MAIN_THREAD_ID ? ( renderMainThreadInfo() @@ -348,6 +331,8 @@ const MiddleHeader: FC = ({ = ({ )} @@ -395,7 +382,7 @@ const MiddleHeader: FC = ({
{renderInfo} @@ -478,6 +465,7 @@ export default memo(withGlobal( shouldSkipHistoryAnimations, currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1), connectionState: global.connectionState, + isSyncing: global.isSyncing, }; const messagesById = selectChatMessages(global, chatId); diff --git a/src/components/ui/Button.scss b/src/components/ui/Button.scss index 8df61dfdb..bb533784d 100644 --- a/src/components/ui/Button.scss +++ b/src/components/ui/Button.scss @@ -200,6 +200,20 @@ } } + &.translucent-black { + background-color: transparent; + color: rgba(0, 0, 0, 0.8); + --ripple-color: rgba(0, 0, 0, 0.08); + + @include active-styles() { + background-color: rgba(0, 0, 0, 0.08); + } + + @include no-ripple-styles() { + background-color: rgba(0, 0, 0, 0.16); + } + } + &.dark { background-color: rgba(0, 0, 0, 0.75); color: white; diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index b9cb58193..2c53fc2a0 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -16,7 +16,9 @@ export type OwnProps = { type?: 'button' | 'submit' | 'reset'; children: any; size?: 'default' | 'smaller' | 'tiny'; - color?: 'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'dark'; + color?: ( + 'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black' | 'dark' + ); backgroundImage?: string; className?: string; round?: boolean; @@ -159,7 +161,7 @@ const Button: FC = ({ Please wait...
- ) : children } + ) : children} {!disabled && ripple && ( )} diff --git a/src/components/ui/Loading.scss b/src/components/ui/Loading.scss index 9e551fec3..31312d67c 100644 --- a/src/components/ui/Loading.scss +++ b/src/components/ui/Loading.scss @@ -4,6 +4,10 @@ align-items: center; justify-content: center; + &.interactive { + cursor: pointer; + } + .Spinner { --spinner-size: 2.75rem; } diff --git a/src/components/ui/Loading.tsx b/src/components/ui/Loading.tsx index 84b500e87..6f2f00d88 100644 --- a/src/components/ui/Loading.tsx +++ b/src/components/ui/Loading.tsx @@ -1,17 +1,20 @@ import React, { FC, memo } from '../../lib/teact/teact'; import Spinner from './Spinner'; +import buildClassName from '../../util/buildClassName'; import './Loading.scss'; type OwnProps = { - color?: 'blue' | 'white' | 'black'; + color?: 'blue' | 'white' | 'black' | 'yellow'; + backgroundColor?: 'light' | 'dark'; + onClick?: NoneToVoidFunction; }; -const Loading: FC = ({ color = 'blue' }) => { +const Loading: FC = ({ color = 'blue', backgroundColor, onClick }) => { return ( -
- +
+
); }; diff --git a/src/components/ui/SearchInput.tsx b/src/components/ui/SearchInput.tsx index 04c392866..8cd2cbaf5 100644 --- a/src/components/ui/SearchInput.tsx +++ b/src/components/ui/SearchInput.tsx @@ -10,6 +10,7 @@ import useInputFocusOnOpen from '../../hooks/useInputFocusOnOpen'; import Loading from './Loading'; import Button from './Button'; +import ShowTransition from './ShowTransition'; import './SearchInput.scss'; @@ -22,6 +23,8 @@ type OwnProps = { value?: string; focused?: boolean; isLoading?: boolean; + spinnerColor?: 'yellow'; + spinnerBackgroundColor?: 'light'; placeholder?: string; disabled?: boolean; autoComplete?: string; @@ -31,6 +34,7 @@ type OwnProps = { onReset?: NoneToVoidFunction; onFocus?: NoneToVoidFunction; onBlur?: NoneToVoidFunction; + onSpinnerClick?: NoneToVoidFunction; }; const SearchInput: FC = ({ @@ -42,6 +46,8 @@ const SearchInput: FC = ({ className, focused, isLoading, + spinnerColor, + spinnerBackgroundColor, placeholder, disabled, autoComplete, @@ -51,6 +57,7 @@ const SearchInput: FC = ({ onReset, onFocus, onBlur, + onSpinnerClick, }) => { // eslint-disable-next-line no-null/no-null let inputRef = useRef(null); @@ -126,9 +133,11 @@ const SearchInput: FC = ({ onKeyDown={handleKeyDown} /> - {isLoading && ( - - )} + + {() => ( + + )} + {!isLoading && (value || canClose) && onReset && (