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;
|
function inflate(...args: any[]): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WindowWithPerf = typeof window & { perf: AnyLiteral };
|
type WindowWithPerf =
|
||||||
|
typeof window
|
||||||
|
& { perf: AnyLiteral };
|
||||||
|
|
||||||
interface TEncodedImage {
|
interface TEncodedImage {
|
||||||
result: Uint8ClampedArray;
|
result: Uint8ClampedArray;
|
||||||
@ -70,10 +72,12 @@ interface IWebpWorker extends Worker {
|
|||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
ClipboardItem?: any;
|
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 {
|
interface Document {
|
||||||
mozFullScreenElement: any;
|
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 Avatar from './Avatar';
|
||||||
import VerifiedIcon from './VerifiedIcon';
|
import VerifiedIcon from './VerifiedIcon';
|
||||||
import TypingStatus from './TypingStatus';
|
import TypingStatus from './TypingStatus';
|
||||||
|
import DotAnimation from './DotAnimation';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
typingStatus?: ApiTypingStatus;
|
typingStatus?: ApiTypingStatus;
|
||||||
avatarSize?: 'small' | 'medium' | 'large' | 'jumbo';
|
avatarSize?: 'small' | 'medium' | 'large' | 'jumbo';
|
||||||
|
status?: string;
|
||||||
|
withDots?: boolean;
|
||||||
withMediaViewer?: boolean;
|
withMediaViewer?: boolean;
|
||||||
withUsername?: boolean;
|
withUsername?: boolean;
|
||||||
withFullInfo?: boolean;
|
withFullInfo?: boolean;
|
||||||
@ -44,6 +47,8 @@ type StateProps =
|
|||||||
const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||||
typingStatus,
|
typingStatus,
|
||||||
avatarSize = 'medium',
|
avatarSize = 'medium',
|
||||||
|
status,
|
||||||
|
withDots,
|
||||||
withMediaViewer,
|
withMediaViewer,
|
||||||
withUsername,
|
withUsername,
|
||||||
withFullInfo,
|
withFullInfo,
|
||||||
@ -86,9 +91,17 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderStatusOrTyping() {
|
function renderStatusOrTyping() {
|
||||||
|
if (status) {
|
||||||
|
return withDots ? (
|
||||||
|
<DotAnimation className="status" content={status} />
|
||||||
|
) : (
|
||||||
|
<span className="status" dir="auto">{status}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (withUpdatingStatus && !areMessagesLoaded && !isRestricted) {
|
if (withUpdatingStatus && !areMessagesLoaded && !isRestricted) {
|
||||||
return (
|
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 Avatar from './Avatar';
|
||||||
import VerifiedIcon from './VerifiedIcon';
|
import VerifiedIcon from './VerifiedIcon';
|
||||||
import TypingStatus from './TypingStatus';
|
import TypingStatus from './TypingStatus';
|
||||||
|
import DotAnimation from './DotAnimation';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -23,6 +24,7 @@ type OwnProps = {
|
|||||||
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
|
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
|
||||||
forceShowSelf?: boolean;
|
forceShowSelf?: boolean;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
withDots?: boolean;
|
||||||
withMediaViewer?: boolean;
|
withMediaViewer?: boolean;
|
||||||
withUsername?: boolean;
|
withUsername?: boolean;
|
||||||
withFullInfo?: boolean;
|
withFullInfo?: boolean;
|
||||||
@ -45,6 +47,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
typingStatus,
|
typingStatus,
|
||||||
avatarSize = 'medium',
|
avatarSize = 'medium',
|
||||||
status,
|
status,
|
||||||
|
withDots,
|
||||||
withMediaViewer,
|
withMediaViewer,
|
||||||
withUsername,
|
withUsername,
|
||||||
withFullInfo,
|
withFullInfo,
|
||||||
@ -90,14 +93,16 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
function renderStatusOrTyping() {
|
function renderStatusOrTyping() {
|
||||||
if (status) {
|
if (status) {
|
||||||
return (
|
return withDots ? (
|
||||||
|
<DotAnimation className="status" content={status} />
|
||||||
|
) : (
|
||||||
<span className="status" dir="auto">{status}</span>
|
<span className="status" dir="auto">{status}</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withUpdatingStatus && !areMessagesLoaded) {
|
if (withUpdatingStatus && !areMessagesLoaded) {
|
||||||
return (
|
return (
|
||||||
<span className="status" dir="auto">{lang('Updating')}</span>
|
<DotAnimation className="status" content={lang('Updating')} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,32 +8,4 @@
|
|||||||
color: var(--color-text-secondary);
|
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 renderText from './helpers/renderText';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
|
import DotAnimation from './DotAnimation';
|
||||||
|
|
||||||
import './TypingStatus.scss';
|
import './TypingStatus.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
@ -21,15 +23,17 @@ type StateProps = {
|
|||||||
const TypingStatus: FC<OwnProps & StateProps> = ({ typingStatus, typingUser }) => {
|
const TypingStatus: FC<OwnProps & StateProps> = ({ typingStatus, typingUser }) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const typingUserName = typingUser && !typingUser.isSelf && getUserFirstOrLastName(typingUser);
|
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 (
|
return (
|
||||||
<p className="typing-status" dir={lang.isRtl ? 'rtl' : 'auto'}>
|
<p className="typing-status" dir={lang.isRtl ? 'rtl' : 'auto'}>
|
||||||
{typingUserName && (
|
{typingUserName && (
|
||||||
<span className="sender-name" dir="auto">{renderText(typingUserName)}</span>
|
<span className="sender-name" dir="auto">{renderText(typingUserName)}</span>
|
||||||
)}
|
)}
|
||||||
{/* fix for translation "username _is_ typing" */}
|
<DotAnimation content={content} />
|
||||||
{lang(typingStatus.action).replace('{user}', '').replace('{emoji}', typingStatus.emoji).trim()}
|
|
||||||
<span className="ellipsis" />
|
|
||||||
</p>
|
</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;
|
overflow: hidden;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
.connection-state-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 3.75rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .Transition {
|
> .Transition {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: transform 300ms ease;
|
|
||||||
|
|
||||||
&.pull-down {
|
|
||||||
transform: translateY(3.75rem);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ChatFolders {
|
.ChatFolders {
|
||||||
|
|||||||
@ -1,28 +1,22 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, useState, useRef, useCallback, useEffect,
|
FC, memo, useCallback, useEffect, useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../lib/teact/teactn';
|
|
||||||
|
|
||||||
import { GlobalState } from '../../../global/types';
|
|
||||||
import { LeftColumnContent, SettingsScreens } from '../../../types';
|
import { LeftColumnContent, SettingsScreens } from '../../../types';
|
||||||
import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||||
|
|
||||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
import { pick } from '../../../util/iteratees';
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import useBrowserOnline from '../../../hooks/useBrowserOnline';
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import Transition from '../../ui/Transition';
|
import Transition from '../../ui/Transition';
|
||||||
import LeftMainHeader from './LeftMainHeader';
|
import LeftMainHeader from './LeftMainHeader';
|
||||||
import ConnectionState from '../ConnectionState';
|
|
||||||
import ChatFolders from './ChatFolders';
|
import ChatFolders from './ChatFolders';
|
||||||
import LeftSearch from '../search/LeftSearch.async';
|
import LeftSearch from '../search/LeftSearch.async';
|
||||||
import ContactList from './ContactList.async';
|
import ContactList from './ContactList.async';
|
||||||
import NewChatButton from '../NewChatButton';
|
import NewChatButton from '../NewChatButton';
|
||||||
import ShowTransition from '../../ui/ShowTransition';
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
|
|
||||||
import './LeftMain.scss';
|
import './LeftMain.scss';
|
||||||
@ -40,15 +34,13 @@ type OwnProps = {
|
|||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, 'connectionState'>;
|
|
||||||
|
|
||||||
const TRANSITION_RENDER_COUNT = Object.keys(LeftColumnContent).length / 2;
|
const TRANSITION_RENDER_COUNT = Object.keys(LeftColumnContent).length / 2;
|
||||||
const BUTTON_CLOSE_DELAY_MS = 250;
|
const BUTTON_CLOSE_DELAY_MS = 250;
|
||||||
const APP_OUTDATED_TIMEOUT = 3 * 24 * 60 * 60 * 1000; // 3 days
|
const APP_OUTDATED_TIMEOUT = 3 * 24 * 60 * 60 * 1000; // 3 days
|
||||||
|
|
||||||
let closeTimeout: number | undefined;
|
let closeTimeout: number | undefined;
|
||||||
|
|
||||||
const LeftMain: FC<OwnProps & StateProps> = ({
|
const LeftMain: FC<OwnProps> = ({
|
||||||
content,
|
content,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
searchDate,
|
searchDate,
|
||||||
@ -59,13 +51,9 @@ const LeftMain: FC<OwnProps & StateProps> = ({
|
|||||||
onContentChange,
|
onContentChange,
|
||||||
onScreenSelect,
|
onScreenSelect,
|
||||||
onReset,
|
onReset,
|
||||||
connectionState,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV);
|
const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV);
|
||||||
|
|
||||||
const isBrowserOnline = useBrowserOnline();
|
|
||||||
const isConnecting = !isBrowserOnline || connectionState === 'connectionStateConnecting';
|
|
||||||
|
|
||||||
const isMouseInside = useRef(false);
|
const isMouseInside = useRef(false);
|
||||||
|
|
||||||
const handleSelectSettings = useCallback(() => {
|
const handleSelectSettings = useCallback(() => {
|
||||||
@ -149,16 +137,12 @@ const LeftMain: FC<OwnProps & StateProps> = ({
|
|||||||
onReset={onReset}
|
onReset={onReset}
|
||||||
shouldSkipTransition={shouldSkipTransition}
|
shouldSkipTransition={shouldSkipTransition}
|
||||||
/>
|
/>
|
||||||
<ShowTransition isOpen={isConnecting} isCustom className="connection-state-wrapper opacity-transition slow">
|
|
||||||
{() => <ConnectionState />}
|
|
||||||
</ShowTransition>
|
|
||||||
<Transition
|
<Transition
|
||||||
name={shouldSkipTransition ? 'none' : 'zoom-fade'}
|
name={shouldSkipTransition ? 'none' : 'zoom-fade'}
|
||||||
renderCount={TRANSITION_RENDER_COUNT}
|
renderCount={TRANSITION_RENDER_COUNT}
|
||||||
activeKey={content}
|
activeKey={content}
|
||||||
shouldCleanup
|
shouldCleanup
|
||||||
cleanupExceptionKey={LeftColumnContent.ChatList}
|
cleanupExceptionKey={LeftColumnContent.ChatList}
|
||||||
className={isConnecting ? 'pull-down' : undefined}
|
|
||||||
>
|
>
|
||||||
{(isActive) => {
|
{(isActive) => {
|
||||||
switch (content) {
|
switch (content) {
|
||||||
@ -220,6 +204,4 @@ function useAppOutdatedCheck() {
|
|||||||
return [shouldRender, transitionClassNames, handleUpdateClick] as const;
|
return [shouldRender, transitionClassNames, handleUpdateClick] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withGlobal<OwnProps>(
|
export default memo(LeftMain);
|
||||||
(global): StateProps => pick(global, ['connectionState']),
|
|
||||||
)(LeftMain);
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, useCallback, useMemo, memo,
|
FC, memo, useCallback, useMemo,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { LeftColumnContent, ISettings } from '../../../types';
|
import { ISettings, LeftColumnContent } from '../../../types';
|
||||||
import { ApiChat } from '../../../api/types';
|
import { ApiChat } from '../../../api/types';
|
||||||
|
import { GlobalState } from '../../../global/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ANIMATION_LEVEL_MAX, APP_NAME, APP_VERSION, FEEDBACK_URL,
|
ANIMATION_LEVEL_MAX, APP_NAME, APP_VERSION, FEEDBACK_URL,
|
||||||
@ -15,10 +16,11 @@ import { formatDateToString } from '../../../util/dateFormat';
|
|||||||
import switchTheme from '../../../util/switchTheme';
|
import switchTheme from '../../../util/switchTheme';
|
||||||
import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
|
import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
|
||||||
import { clearWebsync } from '../../../util/websync';
|
import { clearWebsync } from '../../../util/websync';
|
||||||
import { selectTheme } from '../../../modules/selectors';
|
import { selectCurrentMessageList, selectTheme } from '../../../modules/selectors';
|
||||||
import { isChatArchived } from '../../../modules/helpers';
|
import { isChatArchived } from '../../../modules/helpers';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import { disableHistoryBack } from '../../../hooks/useHistoryBack';
|
import { disableHistoryBack } from '../../../hooks/useHistoryBack';
|
||||||
|
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
||||||
|
|
||||||
import DropdownMenu from '../../ui/DropdownMenu';
|
import DropdownMenu from '../../ui/DropdownMenu';
|
||||||
import MenuItem from '../../ui/MenuItem';
|
import MenuItem from '../../ui/MenuItem';
|
||||||
@ -26,6 +28,8 @@ import Button from '../../ui/Button';
|
|||||||
import SearchInput from '../../ui/SearchInput';
|
import SearchInput from '../../ui/SearchInput';
|
||||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||||
import Switcher from '../../ui/Switcher';
|
import Switcher from '../../ui/Switcher';
|
||||||
|
import ShowTransition from '../../ui/ShowTransition';
|
||||||
|
import ConnectionStatusOverlay from '../ConnectionStatusOverlay';
|
||||||
|
|
||||||
import './LeftMainHeader.scss';
|
import './LeftMainHeader.scss';
|
||||||
|
|
||||||
@ -40,16 +44,20 @@ type OwnProps = {
|
|||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps =
|
||||||
searchQuery?: string;
|
{
|
||||||
isLoading: boolean;
|
searchQuery?: string;
|
||||||
currentUserId?: string;
|
isLoading: boolean;
|
||||||
globalSearchChatId?: string;
|
currentUserId?: string;
|
||||||
searchDate?: number;
|
globalSearchChatId?: string;
|
||||||
theme: ISettings['theme'];
|
searchDate?: number;
|
||||||
animationLevel: 0 | 1 | 2;
|
theme: ISettings['theme'];
|
||||||
chatsById?: Record<string, ApiChat>;
|
animationLevel: 0 | 1 | 2;
|
||||||
};
|
chatsById?: Record<string, ApiChat>;
|
||||||
|
isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized'];
|
||||||
|
isMessageListOpen: boolean;
|
||||||
|
}
|
||||||
|
& Pick<GlobalState, 'connectionState' | 'isSyncing'>;
|
||||||
|
|
||||||
const ANIMATION_LEVEL_OPTIONS = [0, 1, 2];
|
const ANIMATION_LEVEL_OPTIONS = [0, 1, 2];
|
||||||
|
|
||||||
@ -74,6 +82,10 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
theme,
|
theme,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
chatsById,
|
chatsById,
|
||||||
|
connectionState,
|
||||||
|
isSyncing,
|
||||||
|
isConnectionStatusMinimized,
|
||||||
|
isMessageListOpen,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
openChat,
|
openChat,
|
||||||
@ -105,6 +117,10 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, [hasMenu, chatsById]);
|
}, [hasMenu, chatsById]);
|
||||||
|
|
||||||
|
const { connectionStatus, connectionStatusText, connectionStatusPosition } = useConnectionStatus(
|
||||||
|
lang, connectionState, isSyncing, isMessageListOpen, isConnectionStatusMinimized,
|
||||||
|
);
|
||||||
|
|
||||||
const withOtherVersions = window.location.hostname === PRODUCTION_HOSTNAME;
|
const withOtherVersions = window.location.hostname === PRODUCTION_HOSTNAME;
|
||||||
|
|
||||||
const MainButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
const MainButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||||
@ -134,6 +150,10 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [searchQuery, onSearchQuery]);
|
}, [searchQuery, onSearchQuery]);
|
||||||
|
|
||||||
|
const toggleConnectionStatus = useCallback(() => {
|
||||||
|
setSettingOption({ isConnectionStatusMinimized: !isConnectionStatusMinimized });
|
||||||
|
}, [isConnectionStatusMinimized, setSettingOption]);
|
||||||
|
|
||||||
const handleSelectSaved = useCallback(() => {
|
const handleSelectSaved = useCallback(() => {
|
||||||
openChat({ id: currentUserId, shouldReplaceHistory: true });
|
openChat({ id: currentUserId, shouldReplaceHistory: true });
|
||||||
}, [currentUserId, openChat]);
|
}, [currentUserId, openChat]);
|
||||||
@ -272,13 +292,16 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
className={globalSearchChatId || searchDate ? 'with-picker-item' : ''}
|
className={globalSearchChatId || searchDate ? 'with-picker-item' : ''}
|
||||||
value={contactsFilter || searchQuery}
|
value={contactsFilter || searchQuery}
|
||||||
focused={isSearchFocused}
|
focused={isSearchFocused}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading || connectionStatusPosition === 'minimized'}
|
||||||
|
spinnerColor={connectionStatusPosition === 'minimized' ? 'yellow' : undefined}
|
||||||
|
spinnerBackgroundColor={connectionStatusPosition === 'minimized' && theme === 'light' ? 'light' : undefined}
|
||||||
placeholder={searchInputPlaceholder}
|
placeholder={searchInputPlaceholder}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
canClose={Boolean(globalSearchChatId || searchDate)}
|
canClose={Boolean(globalSearchChatId || searchDate)}
|
||||||
onChange={onSearchQuery}
|
onChange={onSearchQuery}
|
||||||
onReset={onReset}
|
onReset={onReset}
|
||||||
onFocus={handleSearchFocus}
|
onFocus={handleSearchFocus}
|
||||||
|
onSpinnerClick={connectionStatusPosition === 'minimized' ? toggleConnectionStatus : undefined}
|
||||||
>
|
>
|
||||||
{selectedSearchDate && (
|
{selectedSearchDate && (
|
||||||
<PickerSelectedItem
|
<PickerSelectedItem
|
||||||
@ -300,6 +323,19 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SearchInput>
|
</SearchInput>
|
||||||
|
<ShowTransition
|
||||||
|
isOpen={connectionStatusPosition === 'overlay'}
|
||||||
|
isCustom
|
||||||
|
className="connection-state-wrapper"
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<ConnectionStatusOverlay
|
||||||
|
connectionStatus={connectionStatus}
|
||||||
|
connectionStatusText={connectionStatusText!}
|
||||||
|
onClick={toggleConnectionStatus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ShowTransition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -310,9 +346,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const {
|
const {
|
||||||
query: searchQuery, fetchingStatus, chatId, date,
|
query: searchQuery, fetchingStatus, chatId, date,
|
||||||
} = global.globalSearch;
|
} = global.globalSearch;
|
||||||
const { currentUserId } = global;
|
const { currentUserId, connectionState, isSyncing } = global;
|
||||||
const { byId: chatsById } = global.chats;
|
const { byId: chatsById } = global.chats;
|
||||||
const { animationLevel } = global.settings.byKey;
|
const { isConnectionStatusMinimized, animationLevel } = global.settings.byKey;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@ -323,6 +359,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
searchDate: date,
|
searchDate: date,
|
||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
animationLevel,
|
animationLevel,
|
||||||
|
connectionState,
|
||||||
|
isSyncing,
|
||||||
|
isConnectionStatusMinimized,
|
||||||
|
isMessageListOpen: Boolean(selectCurrentMessageList(global)),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(LeftMainHeader));
|
)(LeftMainHeader));
|
||||||
|
|||||||
@ -552,7 +552,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
onNotchToggle={onNotchToggle}
|
onNotchToggle={onNotchToggle}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Loading color="white" />
|
<Loading color="white" backgroundColor="dark" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,48 +1,41 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, useCallback, useMemo, memo, useEffect, useRef, useState,
|
FC, memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getDispatch, getGlobal, withGlobal } from '../../lib/teact/teactn';
|
import { getDispatch, getGlobal, withGlobal } from '../../lib/teact/teactn';
|
||||||
import cycleRestrict from '../../util/cycleRestrict';
|
import cycleRestrict from '../../util/cycleRestrict';
|
||||||
|
|
||||||
import { MessageListType } from '../../global/types';
|
import { GlobalState, MessageListType } from '../../global/types';
|
||||||
import {
|
import {
|
||||||
ApiMessage,
|
ApiChat, ApiMessage, ApiTypingStatus, ApiUser, MAIN_THREAD_ID,
|
||||||
ApiChat,
|
|
||||||
ApiUser,
|
|
||||||
ApiTypingStatus,
|
|
||||||
MAIN_THREAD_ID, ApiUpdateConnectionStateType,
|
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN,
|
|
||||||
MOBILE_SCREEN_MAX_WIDTH,
|
|
||||||
EDITABLE_INPUT_ID,
|
EDITABLE_INPUT_ID,
|
||||||
|
MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN,
|
||||||
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_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_CHAT_INFO,
|
||||||
|
SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../util/environment';
|
||||||
import {
|
import {
|
||||||
isUserId,
|
getChatTitle, getMessageKey, getSenderTitle, isUserId,
|
||||||
getMessageKey,
|
|
||||||
getChatTitle,
|
|
||||||
getSenderTitle,
|
|
||||||
} from '../../modules/helpers';
|
} from '../../modules/helpers';
|
||||||
import {
|
import {
|
||||||
|
selectAllowedMessageActions,
|
||||||
selectChat,
|
selectChat,
|
||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
selectAllowedMessageActions,
|
|
||||||
selectIsRightColumnShown,
|
|
||||||
selectThreadTopMessageId,
|
|
||||||
selectThreadInfo,
|
|
||||||
selectChatMessages,
|
selectChatMessages,
|
||||||
selectPinnedIds,
|
|
||||||
selectIsChatWithSelf,
|
|
||||||
selectForwardedSender,
|
|
||||||
selectScheduledIds,
|
|
||||||
selectIsInSelectMode,
|
|
||||||
selectIsChatWithBot,
|
|
||||||
selectCountNotMutedUnread,
|
selectCountNotMutedUnread,
|
||||||
|
selectForwardedSender,
|
||||||
|
selectIsChatWithBot,
|
||||||
|
selectIsChatWithSelf,
|
||||||
|
selectIsInSelectMode,
|
||||||
|
selectIsRightColumnShown,
|
||||||
|
selectPinnedIds,
|
||||||
|
selectScheduledIds,
|
||||||
|
selectThreadInfo,
|
||||||
|
selectThreadTopMessageId,
|
||||||
} from '../../modules/selectors';
|
} from '../../modules/selectors';
|
||||||
import useEnsureMessage from '../../hooks/useEnsureMessage';
|
import useEnsureMessage from '../../hooks/useEnsureMessage';
|
||||||
import useWindowSize from '../../hooks/useWindowSize';
|
import useWindowSize from '../../hooks/useWindowSize';
|
||||||
@ -51,7 +44,7 @@ import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
|||||||
import { formatIntegerCompact } from '../../util/textFormat';
|
import { formatIntegerCompact } from '../../util/textFormat';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useBrowserOnline from '../../hooks/useBrowserOnline';
|
import useConnectionStatus from '../../hooks/useConnectionStatus';
|
||||||
|
|
||||||
import PrivateChatInfo from '../common/PrivateChatInfo';
|
import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||||
import GroupChatInfo from '../common/GroupChatInfo';
|
import GroupChatInfo from '../common/GroupChatInfo';
|
||||||
@ -91,7 +84,8 @@ type StateProps = {
|
|||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
currentTransitionKey: number;
|
currentTransitionKey: number;
|
||||||
connectionState?: ApiUpdateConnectionStateType;
|
connectionState?: GlobalState['connectionState'];
|
||||||
|
isSyncing?: GlobalState['isSyncing'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const MiddleHeader: FC<OwnProps & StateProps> = ({
|
const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||||
@ -116,6 +110,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
currentTransitionKey,
|
currentTransitionKey,
|
||||||
connectionState,
|
connectionState,
|
||||||
|
isSyncing,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
openChatWithInfo,
|
openChatWithInfo,
|
||||||
@ -296,21 +291,9 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [shouldUseStackedToolsClass, canRevealTools, canToolsCollideWithChatInfo, isRightColumnShown]);
|
}, [shouldUseStackedToolsClass, canRevealTools, canToolsCollideWithChatInfo, isRightColumnShown]);
|
||||||
|
|
||||||
const isBrowserOnline = useBrowserOnline();
|
const { connectionStatusText } = useConnectionStatus(lang, connectionState, isSyncing, true);
|
||||||
const isConnecting = (!isBrowserOnline || connectionState === 'connectionStateConnecting')
|
|
||||||
&& (IS_SINGLE_COLUMN_LAYOUT || (IS_TABLET_COLUMN_LAYOUT && !shouldShowCloseButton));
|
|
||||||
|
|
||||||
function renderInfo() {
|
function renderInfo() {
|
||||||
if (isConnecting) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{renderBackButton()}
|
|
||||||
<h3>
|
|
||||||
{lang('WaitingForNetwork')}
|
|
||||||
</h3>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
messageListType === 'thread' && threadId === MAIN_THREAD_ID ? (
|
messageListType === 'thread' && threadId === MAIN_THREAD_ID ? (
|
||||||
renderMainThreadInfo()
|
renderMainThreadInfo()
|
||||||
@ -348,6 +331,8 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
<PrivateChatInfo
|
<PrivateChatInfo
|
||||||
userId={chatId}
|
userId={chatId}
|
||||||
typingStatus={typingStatus}
|
typingStatus={typingStatus}
|
||||||
|
status={connectionStatusText}
|
||||||
|
withDots={Boolean(connectionStatusText)}
|
||||||
withFullInfo={isChatWithBot}
|
withFullInfo={isChatWithBot}
|
||||||
withMediaViewer
|
withMediaViewer
|
||||||
withUpdatingStatus
|
withUpdatingStatus
|
||||||
@ -357,10 +342,12 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
<GroupChatInfo
|
<GroupChatInfo
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
typingStatus={typingStatus}
|
typingStatus={typingStatus}
|
||||||
noRtl
|
status={connectionStatusText}
|
||||||
|
withDots={Boolean(connectionStatusText)}
|
||||||
withMediaViewer
|
withMediaViewer
|
||||||
withFullInfo
|
withFullInfo
|
||||||
withUpdatingStatus
|
withUpdatingStatus
|
||||||
|
noRtl
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -395,7 +382,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="MiddleHeader" ref={componentRef}>
|
<div className="MiddleHeader" ref={componentRef}>
|
||||||
<Transition
|
<Transition
|
||||||
name={shouldSkipHistoryAnimations ? 'none' : 'slide-fade'}
|
name={shouldSkipHistoryAnimations ? 'none' : 'slide-fade'}
|
||||||
activeKey={isConnecting ? Infinity : currentTransitionKey}
|
activeKey={currentTransitionKey}
|
||||||
>
|
>
|
||||||
{renderInfo}
|
{renderInfo}
|
||||||
</Transition>
|
</Transition>
|
||||||
@ -478,6 +465,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1),
|
currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1),
|
||||||
connectionState: global.connectionState,
|
connectionState: global.connectionState,
|
||||||
|
isSyncing: global.isSyncing,
|
||||||
};
|
};
|
||||||
|
|
||||||
const messagesById = selectChatMessages(global, chatId);
|
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 {
|
&.dark {
|
||||||
background-color: rgba(0, 0, 0, 0.75);
|
background-color: rgba(0, 0, 0, 0.75);
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@ -16,7 +16,9 @@ export type OwnProps = {
|
|||||||
type?: 'button' | 'submit' | 'reset';
|
type?: 'button' | 'submit' | 'reset';
|
||||||
children: any;
|
children: any;
|
||||||
size?: 'default' | 'smaller' | 'tiny';
|
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;
|
backgroundImage?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
round?: boolean;
|
round?: boolean;
|
||||||
@ -159,7 +161,7 @@ const Button: FC<OwnProps> = ({
|
|||||||
<span dir={isRtl ? 'auto' : undefined}>Please wait...</span>
|
<span dir={isRtl ? 'auto' : undefined}>Please wait...</span>
|
||||||
<Spinner color={isText ? 'blue' : 'white'} />
|
<Spinner color={isText ? 'blue' : 'white'} />
|
||||||
</div>
|
</div>
|
||||||
) : children }
|
) : children}
|
||||||
{!disabled && ripple && (
|
{!disabled && ripple && (
|
||||||
<RippleEffect />
|
<RippleEffect />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -4,6 +4,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
&.interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.Spinner {
|
.Spinner {
|
||||||
--spinner-size: 2.75rem;
|
--spinner-size: 2.75rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,20 @@
|
|||||||
import React, { FC, memo } from '../../lib/teact/teact';
|
import React, { FC, memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
import './Loading.scss';
|
import './Loading.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
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 (
|
return (
|
||||||
<div className="Loading">
|
<div className={buildClassName('Loading', onClick && 'interactive')} onClick={onClick}>
|
||||||
<Spinner color={color} withBackground={color === 'white'} />
|
<Spinner color={color} backgroundColor={backgroundColor} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import useInputFocusOnOpen from '../../hooks/useInputFocusOnOpen';
|
|||||||
|
|
||||||
import Loading from './Loading';
|
import Loading from './Loading';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
|
import ShowTransition from './ShowTransition';
|
||||||
|
|
||||||
import './SearchInput.scss';
|
import './SearchInput.scss';
|
||||||
|
|
||||||
@ -22,6 +23,8 @@ type OwnProps = {
|
|||||||
value?: string;
|
value?: string;
|
||||||
focused?: boolean;
|
focused?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
spinnerColor?: 'yellow';
|
||||||
|
spinnerBackgroundColor?: 'light';
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
autoComplete?: string;
|
autoComplete?: string;
|
||||||
@ -31,6 +34,7 @@ type OwnProps = {
|
|||||||
onReset?: NoneToVoidFunction;
|
onReset?: NoneToVoidFunction;
|
||||||
onFocus?: NoneToVoidFunction;
|
onFocus?: NoneToVoidFunction;
|
||||||
onBlur?: NoneToVoidFunction;
|
onBlur?: NoneToVoidFunction;
|
||||||
|
onSpinnerClick?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchInput: FC<OwnProps> = ({
|
const SearchInput: FC<OwnProps> = ({
|
||||||
@ -42,6 +46,8 @@ const SearchInput: FC<OwnProps> = ({
|
|||||||
className,
|
className,
|
||||||
focused,
|
focused,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
spinnerColor,
|
||||||
|
spinnerBackgroundColor,
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
autoComplete,
|
autoComplete,
|
||||||
@ -51,6 +57,7 @@ const SearchInput: FC<OwnProps> = ({
|
|||||||
onReset,
|
onReset,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
|
onSpinnerClick,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
let inputRef = useRef<HTMLInputElement>(null);
|
let inputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -126,9 +133,11 @@ const SearchInput: FC<OwnProps> = ({
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
<i className="icon-search" />
|
<i className="icon-search" />
|
||||||
{isLoading && (
|
<ShowTransition isOpen={Boolean(isLoading)} className="slow">
|
||||||
<Loading />
|
{() => (
|
||||||
)}
|
<Loading color={spinnerColor} backgroundColor={spinnerBackgroundColor} onClick={onSpinnerClick} />
|
||||||
|
)}
|
||||||
|
</ShowTransition>
|
||||||
{!isLoading && (value || canClose) && onReset && (
|
{!isLoading && (value || canClose) && onReset && (
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
|
|||||||
@ -34,7 +34,14 @@
|
|||||||
bottom: -0.125rem;
|
bottom: -0.125rem;
|
||||||
right: -0.125rem;
|
right: -0.125rem;
|
||||||
border-radius: 50%;
|
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 {
|
&.blue {
|
||||||
> div {
|
> div {
|
||||||
background-image: var(--spinner-blue-data);
|
background-image: var(--spinner-blue-data);
|
||||||
|
|
||||||
.theme-dark & {
|
.theme-dark & {
|
||||||
background-image: var(--spinner-dark-blue-data);
|
background-image: var(--spinner-dark-blue-data);
|
||||||
}
|
}
|
||||||
@ -76,12 +84,19 @@
|
|||||||
background-image: var(--spinner-gray-data);
|
background-image: var(--spinner-gray-data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.yellow {
|
||||||
|
> div {
|
||||||
|
background-image: var(--spinner-yellow-data);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,14 +5,14 @@ import buildClassName from '../../util/buildClassName';
|
|||||||
import './Spinner.scss';
|
import './Spinner.scss';
|
||||||
|
|
||||||
const Spinner: FC<{
|
const Spinner: FC<{
|
||||||
color?: 'blue' | 'white' | 'black' | 'green' | 'gray';
|
color?: 'blue' | 'white' | 'black' | 'green' | 'gray' | 'yellow';
|
||||||
withBackground?: boolean;
|
backgroundColor?: 'light' | 'dark';
|
||||||
}> = ({
|
}> = ({
|
||||||
color = 'blue',
|
color = 'blue',
|
||||||
withBackground,
|
backgroundColor,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName('Spinner', color, withBackground && 'with-background')}>
|
<div className={buildClassName('Spinner', color, backgroundColor && 'with-background', `bg-${backgroundColor}`)}>
|
||||||
<div />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -168,6 +168,7 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
language: 'en',
|
language: 'en',
|
||||||
timeFormat: '24h',
|
timeFormat: '24h',
|
||||||
wasTimeFormatSetManually: false,
|
wasTimeFormatSetManually: false,
|
||||||
|
isConnectionStatusMinimized: false,
|
||||||
},
|
},
|
||||||
themes: {
|
themes: {
|
||||||
light: {
|
light: {
|
||||||
|
|||||||
@ -115,6 +115,7 @@ export type GlobalState = {
|
|||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
connectionState?: ApiUpdateConnectionStateType;
|
connectionState?: ApiUpdateConnectionStateType;
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
|
isSyncing?: boolean;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
serverTimeOffset: number;
|
serverTimeOffset: number;
|
||||||
leftColumnWidth?: 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');
|
console.log('>>> START SYNC');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setGlobal({ ...getGlobal(), isSyncing: true });
|
||||||
|
|
||||||
await callApi('fetchCurrentUser');
|
await callApi('fetchCurrentUser');
|
||||||
|
|
||||||
// This fetches only active chats and clears archived chats, which will be fetched in `afterSync`
|
// 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({
|
setGlobal({
|
||||||
...getGlobal(),
|
...getGlobal(),
|
||||||
lastSyncTime: Date.now(),
|
lastSyncTime: Date.now(),
|
||||||
|
isSyncing: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
|||||||
@ -218,6 +218,7 @@ $color-message-reaction-own-hover: #b5e0a4;
|
|||||||
--spinner-black-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzJlMzkzOSIvPjwvc3ZnPg==);
|
--spinner-black-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzJlMzkzOSIvPjwvc3ZnPg==);
|
||||||
--spinner-green-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzRmYWU0ZSIvPjwvc3ZnPg==);
|
--spinner-green-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzRmYWU0ZSIvPjwvc3ZnPg==);
|
||||||
--spinner-gray-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzcwNzU3OSIvPjwvc3ZnPg==);
|
--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: 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");
|
--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;
|
canChangeSensitive?: boolean;
|
||||||
timeFormat: TimeFormat;
|
timeFormat: TimeFormat;
|
||||||
wasTimeFormatSetManually: boolean;
|
wasTimeFormatSetManually: boolean;
|
||||||
|
isConnectionStatusMinimized: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiPrivacySettings {
|
export interface ApiPrivacySettings {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user