Connection Status: Indicate when syncing, different positions
This commit is contained in:
parent
b2d06ff289
commit
eb6e5f5e88
10
src/@types/global.d.ts
vendored
10
src/@types/global.d.ts
vendored
@ -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;
|
||||
|
||||
32
src/components/common/DotAnimation.scss
Normal file
32
src/components/common/DotAnimation.scss
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
23
src/components/common/DotAnimation.tsx
Normal file
23
src/components/common/DotAnimation.tsx
Normal file
@ -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<OwnProps> = ({ content, className }) => {
|
||||
const lang = useLang();
|
||||
return (
|
||||
<span className={buildClassName('DotAnimation', className)} dir={lang.isRtl ? 'rtl' : 'auto'}>
|
||||
{content}
|
||||
<span className="ellipsis" />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default DotAnimation;
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
typingStatus,
|
||||
avatarSize = 'medium',
|
||||
status,
|
||||
withDots,
|
||||
withMediaViewer,
|
||||
withUsername,
|
||||
withFullInfo,
|
||||
@ -86,9 +91,17 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
function renderStatusOrTyping() {
|
||||
if (status) {
|
||||
return withDots ? (
|
||||
<DotAnimation className="status" content={status} />
|
||||
) : (
|
||||
<span className="status" dir="auto">{status}</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (withUpdatingStatus && !areMessagesLoaded && !isRestricted) {
|
||||
return (
|
||||
<span className="status" dir="auto">{lang('Updating')}</span>
|
||||
<DotAnimation className="status" content={lang('Updating')} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
typingStatus,
|
||||
avatarSize = 'medium',
|
||||
status,
|
||||
withDots,
|
||||
withMediaViewer,
|
||||
withUsername,
|
||||
withFullInfo,
|
||||
@ -90,14 +93,16 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
function renderStatusOrTyping() {
|
||||
if (status) {
|
||||
return (
|
||||
return withDots ? (
|
||||
<DotAnimation className="status" content={status} />
|
||||
) : (
|
||||
<span className="status" dir="auto">{status}</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (withUpdatingStatus && !areMessagesLoaded) {
|
||||
return (
|
||||
<span className="status" dir="auto">{lang('Updating')}</span>
|
||||
<DotAnimation className="status" content={lang('Updating')} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({ 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 (
|
||||
<p className="typing-status" dir={lang.isRtl ? 'rtl' : 'auto'}>
|
||||
{typingUserName && (
|
||||
<span className="sender-name" dir="auto">{renderText(typingUserName)}</span>
|
||||
)}
|
||||
{/* fix for translation "username _is_ typing" */}
|
||||
{lang(typingStatus.action).replace('{user}', '').replace('{emoji}', typingStatus.emoji).trim()}
|
||||
<span className="ellipsis" />
|
||||
<DotAnimation content={content} />
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<GlobalState, 'connectionState'>;
|
||||
|
||||
const ConnectionState: FC<StateProps> = () => {
|
||||
const lang = useLang();
|
||||
|
||||
return (
|
||||
<div id="ConnectionState" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<Spinner color="black" />
|
||||
<div className="state-text">{lang('WaitingForNetwork')}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ConnectionState);
|
||||
66
src/components/left/ConnectionStatusOverlay.scss
Normal file
66
src/components/left/ConnectionStatusOverlay.scss
Normal file
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/components/left/ConnectionStatusOverlay.tsx
Normal file
44
src/components/left/ConnectionStatusOverlay.tsx
Normal file
@ -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<OwnProps> = ({
|
||||
connectionStatus,
|
||||
connectionStatusText,
|
||||
onClick,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
return (
|
||||
<div id="ConnectionStatusOverlay" dir={lang.isRtl ? 'rtl' : undefined} onClick={onClick}>
|
||||
<Spinner color="black" />
|
||||
<div className="state-text">
|
||||
<Transition activeKey={connectionStatus} name="slide-fade">
|
||||
{() => connectionStatusText}
|
||||
</Transition>
|
||||
</div>
|
||||
<Button
|
||||
round
|
||||
size="tiny"
|
||||
color="translucent-black"
|
||||
>
|
||||
<span className="icon-close" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ConnectionStatusOverlay);
|
||||
@ -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 {
|
||||
|
||||
@ -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<GlobalState, 'connectionState'>;
|
||||
|
||||
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<OwnProps & StateProps> = ({
|
||||
const LeftMain: FC<OwnProps> = ({
|
||||
content,
|
||||
searchQuery,
|
||||
searchDate,
|
||||
@ -59,13 +51,9 @@ const LeftMain: FC<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
onReset={onReset}
|
||||
shouldSkipTransition={shouldSkipTransition}
|
||||
/>
|
||||
<ShowTransition isOpen={isConnecting} isCustom className="connection-state-wrapper opacity-transition slow">
|
||||
{() => <ConnectionState />}
|
||||
</ShowTransition>
|
||||
<Transition
|
||||
name={shouldSkipTransition ? 'none' : 'zoom-fade'}
|
||||
renderCount={TRANSITION_RENDER_COUNT}
|
||||
activeKey={content}
|
||||
shouldCleanup
|
||||
cleanupExceptionKey={LeftColumnContent.ChatList}
|
||||
className={isConnecting ? 'pull-down' : undefined}
|
||||
>
|
||||
{(isActive) => {
|
||||
switch (content) {
|
||||
@ -220,6 +204,4 @@ function useAppOutdatedCheck() {
|
||||
return [shouldRender, transitionClassNames, handleUpdateClick] as const;
|
||||
}
|
||||
|
||||
export default withGlobal<OwnProps>(
|
||||
(global): StateProps => pick(global, ['connectionState']),
|
||||
)(LeftMain);
|
||||
export default memo(LeftMain);
|
||||
|
||||
@ -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<string, ApiChat>;
|
||||
};
|
||||
type StateProps =
|
||||
{
|
||||
searchQuery?: string;
|
||||
isLoading: boolean;
|
||||
currentUserId?: string;
|
||||
globalSearchChatId?: string;
|
||||
searchDate?: number;
|
||||
theme: ISettings['theme'];
|
||||
animationLevel: 0 | 1 | 2;
|
||||
chatsById?: Record<string, ApiChat>;
|
||||
isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized'];
|
||||
isMessageListOpen: boolean;
|
||||
}
|
||||
& Pick<GlobalState, 'connectionState' | 'isSyncing'>;
|
||||
|
||||
const ANIMATION_LEVEL_OPTIONS = [0, 1, 2];
|
||||
|
||||
@ -74,6 +82,10 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
theme,
|
||||
animationLevel,
|
||||
chatsById,
|
||||
connectionState,
|
||||
isSyncing,
|
||||
isConnectionStatusMinimized,
|
||||
isMessageListOpen,
|
||||
}) => {
|
||||
const {
|
||||
openChat,
|
||||
@ -105,6 +117,10 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
}, 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<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [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<OwnProps & StateProps> = ({
|
||||
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 && (
|
||||
<PickerSelectedItem
|
||||
@ -300,6 +323,19 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
)}
|
||||
</SearchInput>
|
||||
<ShowTransition
|
||||
isOpen={connectionStatusPosition === 'overlay'}
|
||||
isCustom
|
||||
className="connection-state-wrapper"
|
||||
>
|
||||
{() => (
|
||||
<ConnectionStatusOverlay
|
||||
connectionStatus={connectionStatus}
|
||||
connectionStatusText={connectionStatusText!}
|
||||
onClick={toggleConnectionStatus}
|
||||
/>
|
||||
)}
|
||||
</ShowTransition>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -310,9 +346,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
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<OwnProps>(
|
||||
searchDate: date,
|
||||
theme: selectTheme(global),
|
||||
animationLevel,
|
||||
connectionState,
|
||||
isSyncing,
|
||||
isConnectionStatusMinimized,
|
||||
isMessageListOpen: Boolean(selectCurrentMessageList(global)),
|
||||
};
|
||||
},
|
||||
)(LeftMainHeader));
|
||||
|
||||
@ -552,7 +552,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
onNotchToggle={onNotchToggle}
|
||||
/>
|
||||
) : (
|
||||
<Loading color="white" />
|
||||
<Loading color="white" backgroundColor="dark" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
@ -116,6 +110,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
shouldSkipHistoryAnimations,
|
||||
currentTransitionKey,
|
||||
connectionState,
|
||||
isSyncing,
|
||||
}) => {
|
||||
const {
|
||||
openChatWithInfo,
|
||||
@ -296,21 +291,9 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [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()}
|
||||
<h3>
|
||||
{lang('WaitingForNetwork')}
|
||||
</h3>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
messageListType === 'thread' && threadId === MAIN_THREAD_ID ? (
|
||||
renderMainThreadInfo()
|
||||
@ -348,6 +331,8 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
<PrivateChatInfo
|
||||
userId={chatId}
|
||||
typingStatus={typingStatus}
|
||||
status={connectionStatusText}
|
||||
withDots={Boolean(connectionStatusText)}
|
||||
withFullInfo={isChatWithBot}
|
||||
withMediaViewer
|
||||
withUpdatingStatus
|
||||
@ -357,10 +342,12 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
<GroupChatInfo
|
||||
chatId={chatId}
|
||||
typingStatus={typingStatus}
|
||||
noRtl
|
||||
status={connectionStatusText}
|
||||
withDots={Boolean(connectionStatusText)}
|
||||
withMediaViewer
|
||||
withFullInfo
|
||||
withUpdatingStatus
|
||||
noRtl
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -395,7 +382,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
<div className="MiddleHeader" ref={componentRef}>
|
||||
<Transition
|
||||
name={shouldSkipHistoryAnimations ? 'none' : 'slide-fade'}
|
||||
activeKey={isConnecting ? Infinity : currentTransitionKey}
|
||||
activeKey={currentTransitionKey}
|
||||
>
|
||||
{renderInfo}
|
||||
</Transition>
|
||||
@ -478,6 +465,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
shouldSkipHistoryAnimations,
|
||||
currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1),
|
||||
connectionState: global.connectionState,
|
||||
isSyncing: global.isSyncing,
|
||||
};
|
||||
|
||||
const messagesById = selectChatMessages(global, chatId);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
<span dir={isRtl ? 'auto' : undefined}>Please wait...</span>
|
||||
<Spinner color={isText ? 'blue' : 'white'} />
|
||||
</div>
|
||||
) : children }
|
||||
) : children}
|
||||
{!disabled && ripple && (
|
||||
<RippleEffect />
|
||||
)}
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Spinner {
|
||||
--spinner-size: 2.75rem;
|
||||
}
|
||||
|
||||
@ -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<OwnProps> = ({ color = 'blue' }) => {
|
||||
const Loading: FC<OwnProps> = ({ color = 'blue', backgroundColor, onClick }) => {
|
||||
return (
|
||||
<div className="Loading">
|
||||
<Spinner color={color} withBackground={color === 'white'} />
|
||||
<div className={buildClassName('Loading', onClick && 'interactive')} onClick={onClick}>
|
||||
<Spinner color={color} backgroundColor={backgroundColor} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
@ -42,6 +46,8 @@ const SearchInput: FC<OwnProps> = ({
|
||||
className,
|
||||
focused,
|
||||
isLoading,
|
||||
spinnerColor,
|
||||
spinnerBackgroundColor,
|
||||
placeholder,
|
||||
disabled,
|
||||
autoComplete,
|
||||
@ -51,6 +57,7 @@ const SearchInput: FC<OwnProps> = ({
|
||||
onReset,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onSpinnerClick,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
let inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -126,9 +133,11 @@ const SearchInput: FC<OwnProps> = ({
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<i className="icon-search" />
|
||||
{isLoading && (
|
||||
<Loading />
|
||||
)}
|
||||
<ShowTransition isOpen={Boolean(isLoading)} className="slow">
|
||||
{() => (
|
||||
<Loading color={spinnerColor} backgroundColor={spinnerBackgroundColor} onClick={onSpinnerClick} />
|
||||
)}
|
||||
</ShowTransition>
|
||||
{!isLoading && (value || canClose) && onReset && (
|
||||
<Button
|
||||
round
|
||||
|
||||
@ -34,7 +34,14 @@
|
||||
bottom: -0.125rem;
|
||||
right: -0.125rem;
|
||||
border-radius: 50%;
|
||||
background: rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
&.bg-dark::before {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
&.bg-light::before {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +60,7 @@
|
||||
&.blue {
|
||||
> div {
|
||||
background-image: var(--spinner-blue-data);
|
||||
|
||||
.theme-dark & {
|
||||
background-image: var(--spinner-dark-blue-data);
|
||||
}
|
||||
@ -76,12 +84,19 @@
|
||||
background-image: var(--spinner-gray-data);
|
||||
}
|
||||
}
|
||||
|
||||
&.yellow {
|
||||
> div {
|
||||
background-image: var(--spinner-yellow-data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
@ -5,14 +5,14 @@ import buildClassName from '../../util/buildClassName';
|
||||
import './Spinner.scss';
|
||||
|
||||
const Spinner: FC<{
|
||||
color?: 'blue' | 'white' | 'black' | 'green' | 'gray';
|
||||
withBackground?: boolean;
|
||||
color?: 'blue' | 'white' | 'black' | 'green' | 'gray' | 'yellow';
|
||||
backgroundColor?: 'light' | 'dark';
|
||||
}> = ({
|
||||
color = 'blue',
|
||||
withBackground,
|
||||
backgroundColor,
|
||||
}) => {
|
||||
return (
|
||||
<div className={buildClassName('Spinner', color, withBackground && 'with-background')}>
|
||||
<div className={buildClassName('Spinner', color, backgroundColor && 'with-background', `bg-${backgroundColor}`)}>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -168,6 +168,7 @@ export const INITIAL_STATE: GlobalState = {
|
||||
language: 'en',
|
||||
timeFormat: '24h',
|
||||
wasTimeFormatSetManually: false,
|
||||
isConnectionStatusMinimized: false,
|
||||
},
|
||||
themes: {
|
||||
light: {
|
||||
|
||||
@ -115,6 +115,7 @@ export type GlobalState = {
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
connectionState?: ApiUpdateConnectionStateType;
|
||||
currentUserId?: string;
|
||||
isSyncing?: boolean;
|
||||
lastSyncTime?: number;
|
||||
serverTimeOffset: number;
|
||||
leftColumnWidth?: number;
|
||||
|
||||
62
src/hooks/useConnectionStatus.ts
Normal file
62
src/hooks/useConnectionStatus.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { GlobalState } from '../global/types';
|
||||
|
||||
import useBrowserOnline from './useBrowserOnline';
|
||||
import { LangFn } from './useLang';
|
||||
|
||||
export enum ConnectionStatus {
|
||||
waitingForNetwork,
|
||||
syncing,
|
||||
online,
|
||||
}
|
||||
|
||||
type ConnectionStatusPosition =
|
||||
'overlay'
|
||||
| 'minimized'
|
||||
| 'middleHeader'
|
||||
| 'none';
|
||||
|
||||
export default function useConnectionStatus(
|
||||
lang: LangFn,
|
||||
connectionState: GlobalState['connectionState'],
|
||||
isSyncing: GlobalState['isSyncing'],
|
||||
hasMiddleHeader: boolean,
|
||||
isMinimized?: boolean,
|
||||
) {
|
||||
let status: ConnectionStatus;
|
||||
const isBrowserOnline = useBrowserOnline();
|
||||
if (!isBrowserOnline || connectionState === 'connectionStateConnecting') {
|
||||
status = ConnectionStatus.waitingForNetwork;
|
||||
} else if (isSyncing) {
|
||||
status = ConnectionStatus.syncing;
|
||||
} else {
|
||||
status = ConnectionStatus.online;
|
||||
}
|
||||
|
||||
let position: ConnectionStatusPosition;
|
||||
if (status === ConnectionStatus.online) {
|
||||
position = 'none';
|
||||
} else if (hasMiddleHeader) {
|
||||
position = 'middleHeader';
|
||||
} else if (isMinimized) {
|
||||
position = 'minimized';
|
||||
} else {
|
||||
position = 'overlay';
|
||||
}
|
||||
|
||||
let text: string | undefined;
|
||||
if (status === ConnectionStatus.waitingForNetwork) {
|
||||
text = lang('WaitingForNetwork');
|
||||
} else if (status === ConnectionStatus.syncing) {
|
||||
text = lang('Updating');
|
||||
}
|
||||
|
||||
if (position === 'middleHeader') {
|
||||
text = text!.toLowerCase().replace(/\.+$/, '');
|
||||
}
|
||||
|
||||
return {
|
||||
connectionStatus: status,
|
||||
connectionStatusPosition: position,
|
||||
connectionStatusText: text,
|
||||
};
|
||||
}
|
||||
@ -53,6 +53,8 @@ async function sync(afterSyncCallback: () => void) {
|
||||
console.log('>>> START SYNC');
|
||||
}
|
||||
|
||||
setGlobal({ ...getGlobal(), isSyncing: true });
|
||||
|
||||
await callApi('fetchCurrentUser');
|
||||
|
||||
// This fetches only active chats and clears archived chats, which will be fetched in `afterSync`
|
||||
@ -62,6 +64,7 @@ async function sync(afterSyncCallback: () => void) {
|
||||
setGlobal({
|
||||
...getGlobal(),
|
||||
lastSyncTime: Date.now(),
|
||||
isSyncing: false,
|
||||
});
|
||||
|
||||
if (DEBUG) {
|
||||
|
||||
@ -218,6 +218,7 @@ $color-message-reaction-own-hover: #b5e0a4;
|
||||
--spinner-black-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzJlMzkzOSIvPjwvc3ZnPg==);
|
||||
--spinner-green-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzRmYWU0ZSIvPjwvc3ZnPg==);
|
||||
--spinner-gray-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzcwNzU3OSIvPjwvc3ZnPg==);
|
||||
--spinner-yellow-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iI0ZERDc2NCIvPjwvc3ZnPg==);
|
||||
|
||||
--drag-target-border: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='%23DDDFE0' stroke-width='4' stroke-dasharray='9.1%2c 10.5' stroke-dashoffset='3' stroke-linecap='round'/%3e%3c/svg%3e");
|
||||
--drag-target-border-hovered: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='%2363A2E3' stroke-width='4' stroke-dasharray='9.1%2c 10.5' stroke-dashoffset='3' stroke-linecap='round'/%3e%3c/svg%3e");
|
||||
|
||||
@ -81,6 +81,7 @@ export interface ISettings extends NotifySettings, Record<string, any> {
|
||||
canChangeSensitive?: boolean;
|
||||
timeFormat: TimeFormat;
|
||||
wasTimeFormatSetManually: boolean;
|
||||
isConnectionStatusMinimized: boolean;
|
||||
}
|
||||
|
||||
export interface ApiPrivacySettings {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user